From b62a5445697607f12843d9f25749400844d19636 Mon Sep 17 00:00:00 2001 From: gooker_young Date: Sat, 27 Jun 2026 20:47:29 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E8=B0=83=E6=95=B4Python=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=B8=8E=E4=BE=9D=E8=B5=96=E9=80=82=E9=85=8D=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=80=A7=E8=83=BD=E6=8A=A5=E5=91=8A=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=B8=8E=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 将Python版本从3.13降级到3.11 2. 为typing-extensions添加版本适配标记 3. 简化dev依赖组,移除pysnooper 4. 重构perf_timer,提取_generate_report独立函数 5. 新增性能报告生成与测试用例 --- .python-version | 2 +- pyproject.toml | 7 ++----- src/pyflowx/utils.py | 49 ++++++++++++++++++++++++++++++++++++++------ tests/test_utils.py | 24 ++++++++++++++++++++++ uv.lock | 13 +----------- 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/.python-version b/.python-version index 24ee5b1..2c07333 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13 +3.11 diff --git a/pyproject.toml b/pyproject.toml index 7e323ef..a5507c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ classifiers = [ ] dependencies = [ "graphlib_backport >= 1.0.0; python_version < '3.9'", - "typing-extensions>=4.13.2", + "typing-extensions>=4.13.2; python_version < '3.10'", ] description = "Lightweight, type-safe DAG task scheduler with multi-strategy execution." keywords = ["async", "dag", "scheduler", "task", "workflow"] @@ -93,10 +93,7 @@ packages = ["src/pyflowx"] pyflowx = { workspace = true } [dependency-groups] -dev = [ - "pyflowx[dev,office,llm]", - "pysnooper>=1.2.3", -] +dev = ["pyflowx[dev,office,llm]"] [tool.coverage.run] branch = true diff --git a/src/pyflowx/utils.py b/src/pyflowx/utils.py index e1303b8..4b90f95 100644 --- a/src/pyflowx/utils.py +++ b/src/pyflowx/utils.py @@ -1,15 +1,19 @@ """常用工具函数.""" -__all__ = ["perf_timer"] +from __future__ import annotations +__all__ = ["perf_timer"] import functools import logging import time from collections import defaultdict -from typing import Callable, ParamSpec, TypedDict +from typing import Callable, TypedDict -from typing_extensions import TypeVar +try: + from typing_extensions import ParamSpec, TypeVar +except ImportError: + from typing import ParamSpec, TypeVar P = ParamSpec("P") R = TypeVar("R") @@ -30,6 +34,37 @@ _perf_metrics: defaultdict[str, _PerformanceMetrics] = defaultdict( ) +def _generate_report(unit: str, precision: int) -> str: + """生成性能指标报告,返回报告字符串.""" + if not _perf_metrics: + return "" + + lines: list[str] = [] + lines.append("=" * 50) + lines.append("性能指标报告 (Performance Metrics Report)") + lines.append("-" * 50) + + # 按总耗时排序,最耗时的函数排在前面 + sorted_metrics = sorted(_perf_metrics.items(), key=lambda x: x[1]["total_time"], reverse=True) + + for name, metrics in sorted_metrics: + avg_time = metrics["total_time"] / metrics["count"] if metrics["count"] > 0 else 0 + lines.append( + f"{name}: " + f"调用次数={metrics['count']}, " + f"总耗时={metrics['total_time']:.{precision}f}{unit}, " + f"平均耗时={avg_time:.{precision}f}{unit}" + ) + + lines.append("=" * 50) + report_str = "\n".join(lines) + + # 同时输出到日志 + logging.info("\n".join(lines)) + + return report_str + + def perf_timer(unit: str = "ms", precision: int = 4, report: bool = False): """性能计时器装饰器.""" scale: dict[str, float] = { @@ -63,8 +98,10 @@ def perf_timer(unit: str = "ms", precision: int = 4, report: bool = False): logging.info(f"Performance metrics report enabled with unit {unit} and precision {precision}") @atexit.register - def _() -> None: - for name, metrics in _perf_metrics.items(): - logging.info(f"{name}: {metrics['count']} times, {metrics['total_time']:.{precision}f}{unit}") + def _report_at_exit() -> None: + """在程序退出时报告性能指标.""" + _generate_report(unit, precision) + + # 将报告生成逻辑提取为独立函数,便于测试 return decorator diff --git a/tests/test_utils.py b/tests/test_utils.py index d7b3f2e..c677ff2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -39,3 +39,27 @@ class TestPerformanceTimer: assert _perf_metrics["test_func"]["total_time"] >= 0.1 assert mock_log.call_count == 1 + + def test_generate_report(self, mocker: MockerFixture, caplog: pytest.LogCaptureFixture): + mock_log = mocker.patch("logging.info") + + from pyflowx.utils import _generate_report + + @perf_timer(report=True, unit="ms", precision=3) + def test_func(): + time.sleep(0.1) + + @perf_timer(report=True, unit="ms", precision=3) + def test_func2(): + time.sleep(0.2) + + test_func() + test_func2() + + _generate_report("ms", 3) + + assert mock_log.call_count == 3 + assert _perf_metrics["test_func"]["count"] == 1 + assert _perf_metrics["test_func"]["total_time"] >= 0.1 + assert _perf_metrics["test_func2"]["count"] == 1 + assert _perf_metrics["test_func2"]["total_time"] >= 0.2 diff --git a/uv.lock b/uv.lock index 07d0620..af5a4ad 100644 --- a/uv.lock +++ b/uv.lock @@ -5607,8 +5607,6 @@ version = "0.2.10" source = { editable = "." } dependencies = [ { name = "graphlib-backport", marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.9'" }, ] [package.optional-dependencies] @@ -5682,7 +5680,7 @@ requires-dist = [ { name = "sglang", extras = ["all"], marker = "python_full_version >= '3.10' and sys_platform == 'linux' and extra == 'llm'", specifier = "==0.5.10rc0" }, { name = "tox", marker = "extra == 'dev'", specifier = ">=4.25.0" }, { name = "tox-uv", marker = "extra == 'dev'", specifier = ">=1.13.1" }, - { name = "typing-extensions", specifier = ">=4.13.2" }, + { name = "typing-extensions", marker = "python_full_version < '3.8'", specifier = ">=4.13.2" }, ] provides-extras = ["dev", "llm", "office"] @@ -8439,22 +8437,13 @@ name = "typing-extensions" version = "4.15.0" source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'darwin'", "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version >= '3.15' and sys_platform == 'emscripten'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.14.*' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "(python_full_version == '3.14.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'",