diff --git a/.trae/docs/iter-01-typer-rich-migration.md b/.trae/docs/iter-01-typer-rich-migration.md deleted file mode 100644 index 8f97c71..0000000 --- a/.trae/docs/iter-01-typer-rich-migration.md +++ /dev/null @@ -1,62 +0,0 @@ -# 迭代 01: typer/rich 迁移 - -## 本轮目标 - -采用 typer + rich 简化 CLI 设计, 提高显示效果: -1. 修复 Python 3.8 兼容性测试失败 -2. 全量重写 tools.py 为 typer 内核 (保留 @px.tool 装饰器 API) -3. 重写 pf.py 为 rich 驱动主入口 -4. rich 深度集成 (执行生命周期/错误/工具列表/dry-run) -5. 放弃 Python 3.8/3.9 支持 (requires-python >=3.10) - -## 改动文件清单 - -### 依赖与配置 -- `pyproject.toml`: requires-python >=3.10, 添加 typer>=0.24.0 + rich>=13.7.0 依赖, ruff target-version=py310, pyrefly python-version=3.10 -- `tox.ini`: envlist 改为 py310-py314 - -### 核心重写 -- `src/pyflowx/tools.py`: 全量重写 — argparse → typer.Typer; _make_dispatcher 构建命令 dispatcher; _build_typer_app 单命令/多命令路由; _execute_dag DAG 编排; standalone_mode=False + 捕获 typer._click 异常处理退出码; _handle_list_rich rich 表格 --list -- `src/pyflowx/cli/pf.py`: 重写 — rich Panel+Table 工具列表 (含别名/说明列), --help/-h/--version/-V 全局选项, 未知工具模糊匹配建议 (difflib), rich Console stderr 错误 - -### rich 深度集成 -- `src/pyflowx/executors.py`: _make_verbose_callback 用 ✓/✗/▸/○ 符号 + 颜色替代 [verbose] 前缀; _print_dry_run rich styled -- `src/pyflowx/command.py`: 执行命令/返回码用 rich Console + 颜色 (green=0/red=非0) - -### Python 3.10+ 现代化 -- `src/pyflowx/graph.py`: 移除 graphlib_backport, 直接 import graphlib -- `src/pyflowx/task.py`: Union → X|Y, typing.ContextManager → contextlib.AbstractContextManager -- `src/pyflowx/storage.py`: 同上 -- `src/pyflowx/command.py`: typing.List → list, Union → X|Y -- `src/pyflowx/cli/emlmanager.py`: zip() 加 strict= 参数 -- `src/pyflowx/executors.py`: zip() 加 strict=True - -### 测试 -- `tests/test_tools.py`: 全量重写 (84 测试) — 覆盖 typer 内核 (_make_dispatcher/_build_typer_app/_execute_dag/退出码三态) -- `tests/test_runner.py`: verbose 断言更新 (✓ 替代 [verbose]) - -## 关键决策与依据 - -1. **保留 @px.tool 装饰器 API**: 用户既有 ops/ 模块全部使用 @px.tool, 改 API 会破坏所有工具. 内部替换 argparse→typer, 外部 API 不变. - -2. **standalone_mode=False**: typer 默认 standalone_mode=True 会 sys.exit() 退出进程, 测试中无法捕获. 用 False + 手动捕获 typer._click 异常 (NoArgsIsHelpError/ClickException/Abort) 处理退出码. - -3. **typer._click 私有导入**: typer 内置 click (typer._click) 与公共 click 是两套独立类层次, 必须从 typer._click.exceptions 导入 NoArgsIsHelpError/ClickException 才能捕获. - -4. **--list 预处理而非 typer callback**: typer 的 no_args_is_help=True 在多命令工具中会拦截 --list (要求先给 subcommand). 在 run_tool 中预处理 --list (任意位置出现即列出任务) 绕过此限制. - -5. **单命令 vs 多命令**: 单命令工具 (仅 subcommand=None) 用 callback(invoke_without_command=True), no_args_is_help=False; 多命令工具用各 subcommand 注册为 command, no_args_is_help=True. - -6. **legacy 工具不迁移**: emlmanager.py 是 Web 应用 (sqlite3+threading+HTTP), profiler.py 用 runpy.run_path, 均不适配 @px.tool DAG 模型. 保留 pf._run_legacy() 路由是更简方案. - -## 验证结果 - -- ruff check: 0 errors -- ruff format: 96 files already formatted -- pyrefly: 0 errors (12 suppressed) -- pytest: 1003 passed -- coverage: 97.00% (门槛 95%) - -## 遗留事项 - -- 无 diff --git a/.trae/docs/iter-02-sqlite-backend.md b/.trae/docs/iter-02-sqlite-backend.md deleted file mode 100644 index 1588976..0000000 --- a/.trae/docs/iter-02-sqlite-backend.md +++ /dev/null @@ -1,67 +0,0 @@ -# 迭代 02:SQLite 状态后端 (SQLiteBackend) - -## 本轮目标 - -在 `src/pyflowx/storage.py` 新增 `SQLiteBackend`,扩展 `StateBackend` ABC, -解决 `JSONBackend` 全量加载/落盘在大规模任务(10k+)下的性能瓶颈。 - -使用标准库 `sqlite3`,零新依赖。作为 `JSONBackend` 的 drop-in 替换, -支持 TTL、断点续跑、`batch()` 批量提交。 - -## 改动文件清单 - -- `src/pyflowx/storage.py` — 新增 `SQLiteBackend` 类 -- `src/pyflowx/__init__.py` — 导出 `SQLiteBackend` -- `tests/test_storage.py` — 新增 SQLiteBackend 测试用例 -- `docs/api.rst` — 补充 SQLiteBackend 文档(如需) - -## 关键决策与依据 - -### 1. 复用 `_TTLStateBackendMixin` - -`SQLiteBackend` 继承 `_TTLStateBackendMixin`,只需实现 4 个原语 -(`_get_raw`/`_put_raw`/`_iter_raw`/`_clear_raw`),TTL 逻辑由 mixin 提供。 -与 `MemoryBackend`/`JSONBackend` 保持一致架构。 - -### 2. 值序列化策略 - -值以 JSON 文本存入 `value` 列(`json.dumps`/`json.loads`),与 `JSONBackend` -保持相同契约(结果必须可 JSON 序列化)。这样: -- 三种后端行为一致,用户可无缝切换 -- 复用现有 `StorageError` 序列化错误处理 -- 无需引入 pickle(安全风险) - -### 3. 线程安全 - -sqlite3 连接对象本身非线程安全。使用 `threading.RLock` 序列化所有 -`execute`/`commit` 调用,支持 `thread`/`async`/`dependency` 策略下的 -并发存取。`check_same_thread=False` 允许跨线程使用同一连接。 - -### 4. batch() 语义 - -`batch()` 不持有锁整个 duration(否则阻塞并发),仅设置 `_in_batch` 标志。 -各 `_put_raw` 调用各自加锁做 INSERT(不 commit),退出时单次 commit。 -与 `JSONBackend.batch()` 模式一致。 - -### 5. WAL 模式 - -启用 `PRAGMA journal_mode=WAL` + `synchronous=NORMAL`,提升并发读写性能。 -WAL 模式下读不阻塞写。 - -### 6. 不实现 `__del__` - -依赖 sqlite3 连接自身的 `__del__` 关闭;提供 `close()` 方法供显式清理。 -`__del__` + RLock 有死锁风险,避免。 - -## 验证结果 - -- ruff / pyrefly / pytest 全通过 -- 覆盖率 ≥ 95% -- 作为 JSONBackend drop-in 替换,现有测试通过 -- 并发线程安全测试通过 - -## 遗留事项 - -- P2 实时进度监控(下一迭代) -- P3 任务通知系统 -- P4 结果流式获取 diff --git a/.trae/docs/iter-03-progress-monitor.md b/.trae/docs/iter-03-progress-monitor.md deleted file mode 100644 index 5a2cda2..0000000 --- a/.trae/docs/iter-03-progress-monitor.md +++ /dev/null @@ -1,57 +0,0 @@ -# 迭代 03:实时进度监控 - -## 本轮目标 - -提供两种进度监控形式: -1. `run(progress=True)` 开箱即用的 rich 进度条 -2. `ProgressCallback` 协议供程序化集成 - -复用现有 `rich` 依赖与 `EventCallback`/`TaskEvent` 事件机制。 - -## 改动文件清单 - -- `src/pyflowx/progress.py` — 新增:ProgressCallback Protocol + RichProgressMonitor -- `src/pyflowx/executors.py` — run() 新增 progress 参数,组合回调链 -- `src/pyflowx/__init__.py` — 导出 ProgressCallback、RichProgressMonitor -- `tests/test_progress.py` — 新增测试 - -## 关键决策与依据 - -### 1. ProgressCallback Protocol 带生命周期 - -普通 `EventCallback`(Callable[[TaskEvent], None])无法表达 start/finish 生命周期, -而 rich.Progress 需要显式启动/停止。因此 Protocol 定义三个方法: -`start(total)` / `on_event(event)` / `finish()`。 - -### 2. progress 参数接受 bool | ProgressCallback - -- `False`(默认):无进度监控 -- `True`:使用默认 `RichProgressMonitor` -- `ProgressCallback` 实例:自定义监控器 - -与现有 `on_event` 独立——`on_event` 是简单逐事件函数, -`progress` 是带生命周期的监控器。两者可共存。 - -### 3. 回调组合 - -用 `_compose_callbacks` 将 verbose 打印、monitor.on_event、用户 on_event -组合为单个 effective_callback,避免多层包装。 - -### 4. RichProgressMonitor 设计 - -- 使用 rich.Progress 的 Live 显示,列:spinner + 描述 + 进度条 + M/N + 百分比 + 耗时 -- 运行中任务名显示在描述栏(最多 3 个 + 折叠) -- SUCCESS/FAILED/SKIPPED 均推进进度(终态即完成) -- console 可注入,便于测试 - -## 验证结果 - -- ruff / pyrefly / pytest 全通过 -- 覆盖率 ≥ 95% -- run(progress=True) 在终端显示进度条 -- 自定义 ProgressCallback 可订阅事件 - -## 遗留事项 - -- P3 任务通知系统(下一迭代) -- P4 结果流式获取 diff --git a/.trae/docs/iter-04-notification.md b/.trae/docs/iter-04-notification.md deleted file mode 100644 index ff07f57..0000000 --- a/.trae/docs/iter-04-notification.md +++ /dev/null @@ -1,52 +0,0 @@ -# 迭代 04:任务通知系统 - -## 本轮目标 - -全生命周期任务通知:RUNNING/SUCCESS/FAILED/SKIPPED 均可配置。 -支持 webhook sink(飞书/钉钉/企业微信)+ Python 回调 sink。 -不改变核心执行流程,通过回调链集成。 - -## 改动文件清单 - -- `src/pyflowx/notification.py` — 新增:Notifier Protocol + WebhookNotifier + 平台实现 -- `src/pyflowx/executors.py` — run() 新增 notifiers 参数,_resolve_callbacks 统一处理 -- `src/pyflowx/__init__.py` — 导出通知类 -- `tests/test_notification.py` — 新增测试 - -## 关键决策与依据 - -### 1. 独立模块而非扩展 TaskHooks - -TaskHooks 是 per-task 的(每个 TaskSpec 独立配置)。通知是 graph-level 的 -(对所有任务事件发送)。架构层级不同,独立 Notifier 系统更合适。 - -### 2. 零依赖 webhook - -用 stdlib `urllib.request` 发送 HTTP POST,不引入 requests/httpx。 -项目运行时依赖仅 rich/typer/typing-extensions,保持零新增。 - -### 3. 事件过滤在 Notifier 内部 - -每个 Notifier 持有 `levels` 集合,notify() 内部判断是否处理该事件。 -避免在 run() 中做过滤逻辑,保持执行流程简洁。 - -### 4. 回调链集成 - -notifier.notify 作为 EventCallback 加入 effective_callback 链, -与 on_event/progress/verbose 共存。close() 在 run() finally 中调用。 - -### 5. 消息格式共享 - -飞书/钉钉/企业微信消息文本格式相同(图标 + 任务名 + 状态 + 耗时 + 错误), -仅 payload 结构不同。提取 _format_message 为共享方法。 - -## 验证结果 - -- ruff / pyrefly / pytest 全通过 -- 覆盖率 ≥ 95% -- 各平台 payload 格式正确 -- webhook 发送失败不影响主流程 - -## 遗留事项 - -- P4 结果流式获取(下一迭代) diff --git a/.trae/docs/iter-05-run-iter.md b/.trae/docs/iter-05-run-iter.md deleted file mode 100644 index e4da7f8..0000000 --- a/.trae/docs/iter-05-run-iter.md +++ /dev/null @@ -1,44 +0,0 @@ -# 迭代 05:结果流式获取(run_iter) - -## 目标 - -新增 `run_iter()` 函数,以生成器形式逐个 yield 完成任务的结果, -适用于大量任务逐个处理、内存敏感场景。保留 `run()` 返回 `RunReport`, -完全向后兼容。 - -## 设计 - -### 方案:线程 + 队列 - -`run()` 是阻塞函数,内部可能调用 `asyncio.run()`,无法直接改为生成器。 -采用线程 + 队列方案: - -1. `run()` 新增内部参数 `_report: RunReport | None`(下划线前缀,内部用) -2. `run_iter()` 创建 `RunReport`,传给 `run(_report=report)` -3. `on_event` 回调将终态事件(SUCCESS/FAILED/SKIPPED)的任务名放入队列 -4. 后台线程执行 `run()`,生成器从队列取名,读 `report.results[name]` yield -5. `None` 哨兵标记执行结束 - -### 关键点 - -- **结果可见性**:`_store_result` 先写 `report.results[name]` 再 `_emit`, - 队列提供内存屏障,yield 时结果必然可见 -- **去重**:skipped 任务可能 double-emit(`_prepare_for_execution` + `_store_result`), - 用 `yielded` set 去重 -- **错误传播**:`run()` 抛异常时存入 `error_box`,生成器耗尽后 re-raise -- **早退**:daemon 线程,生成器 `finally` 中 join(timeout=0) 不阻塞 - -## 改动文件 - -- `src/pyflowx/executors.py` — 新增 `run_iter()`,`run()` 加 `_report` 参数 -- `src/pyflowx/__init__.py` — 导出 `run_iter` -- `tests/test_run_iter.py` — 新增测试 - -## 验收标准 - -- `run_iter()` 逐个 yield 所有任务的 `(name, TaskResult)` -- 支持 sequential/thread/async/dependency 四种策略 -- 失败任务也 yield(FAILED 状态),之后 re-raise `TaskFailedError` -- 缓存命中的 SKIPPED 任务也 yield -- `run()` 行为不变,现有测试全过 -- 覆盖率 ≥ 95% diff --git a/.trae/docs/iter-06-cleanup-docs.md b/.trae/docs/iter-06-cleanup-docs.md new file mode 100644 index 0000000..0e54c0d --- /dev/null +++ b/.trae/docs/iter-06-cleanup-docs.md @@ -0,0 +1,65 @@ +# 迭代 06:代码清理与文档对齐 + +## 本轮目标 + +对齐 README 与实际代码状态,清理过期文档,归档历史迭代记录。 + +原始计划包含"移除 3.8 兼容代码(`from __future__ import annotations` / +`typing_extensions` 版本守卫 / `sys.version_info` 分支)",但调研后发现 +该前提不准确: + +- `typing_extensions` 守卫针对 **3.12/3.13 前向兼容**(`override` 是 3.12+, + `TypeVar(default=)` 是 3.13+),项目 `requires-python >= 3.10`,守卫**不可移除** +- `from __future__ import annotations` 在 3.10+ 仍有 PEP 563 价值(前向引用、 + 延迟求值),移除需逐文件审计前向引用,风险高收益低 +- `sys.version_info` 分支即上述 `typing_extensions` 守卫,同上 + +因此本轮聚焦真正安全的清理项。 + +## 改动文件清单 + +### 文档对齐 +- `README.md` — 修正覆盖率徽章 100% → 97%;移除未实现的 "YAML 任务编排" + 特性声明与整章;修正 "最小依赖" 描述(实际依赖 rich + typer + + typing-extensions,无 PyYAML);修正 CLI 模块路径(profiler/emlmanager 已 + 迁移至 cli/legacy/);修正对比表 "运行时依赖 | 仅标准库" +- `docs/installation.rst` — 移除 3.8 / graphlib_backport / PyYAML 提法 + +### 归档与清理 +- `.trae/docs/iter-01..05-*.md` — 归档至 project_memory.md(关键设计决策已 + 记录在 memory 系统的 Engineering Conventions 小节),删除原文件 +- `.trae/documents/项目结构重构*.md` — 重构已完成,删除过期计划文档 + +### 保留(不动) +- `typing_extensions` 版本守卫(3.12/3.13 前向兼容,非 3.8 死代码) +- `from __future__ import annotations`(PEP 563 仍有价值,移除风险高) +- `.trae/rules/python-standards.md` 中 "最低 Python 3.8" 描述(规则变更需 + 用户授权,本轮不修改 rules/) + +## 关键决策与依据 + +### 1. 不移除 typing_extensions 守卫 + +`pyproject.toml` 声明 `typing-extensions>=4.13.2; python_version < '3.13'`。 +`override`(3.12+)与 `TypeVar(default=)`(3.13+)在 3.10/3.11/3.12 上仍需 +`typing_extensions` 回退。守卫是前向兼容,不是 3.8 死代码。 + +### 2. 不移除 from __future__ import annotations + +PEP 563 在 3.10+ 仍提供前向引用与延迟求值价值。50 个文件统一移除需逐文件 +审计前向引用、自引用类型,风险高收益低。保留是更稳妥的选择。 + +### 3. 不修改 python-standards.md + +按 `.trae/rules/dev-workflow.md` 规则,`.trae/rules/` 下文件修改必须先获 +用户授权。该规则中 "最低 Python 3.8" 描述已过时(实际 >=3.10),但本轮 +不擅自修改,留待用户确认。 + +## 验收标准 + +- README 无未实现特性声明(YAML 编排章节移除) +- 覆盖率徽章与实际一致(97%) +- 依赖描述与 pyproject.toml 一致 +- `.trae/docs/` 仅保留 iter-06 +- `.trae/documents/` 过期文件清理 +- 全套测试通过,覆盖率不下降 diff --git a/.trae/documents/项目结构重构-Phase2收尾计划.md b/.trae/documents/项目结构重构-Phase2收尾计划.md deleted file mode 100644 index cc4b2b8..0000000 --- a/.trae/documents/项目结构重构-Phase2收尾计划.md +++ /dev/null @@ -1,194 +0,0 @@ -# PyFlowX 项目结构重构 — Phase 2 收尾计划 - -## Summary - -承接上一会话已批准的 [项目结构重构计划](file:///home/zhou/pyflowx/.trae/documents/项目结构重构计划.md)。Phase 1(cli/legacy/ 归位)已完成并验证;Phase 2(ops/ 按功能分组)的文件移动、`_TOOL_MODULES` 注册表、`pf.py` 动态导入改造、`ops/__init__.py` docstring 更新均已落地,**仅剩 3 项收尾工作**:测试导入路径更新、注册表完整性测试、全量验证 + 提交。 - -本计划仅覆盖 Phase 2 剩余收尾,不引入新重构范围。 - -## Current State Analysis - -### 已完成(无需再动) - -| 项 | 状态 | 证据 | -|----|------|------| -| `cli/legacy/` 子包 | ✅ | [cli/legacy/__init__.py](file:///home/zhou/pyflowx/src/pyflowx/cli/legacy/__init__.py)、emlmanager.py、profiler.py 均在位 | -| `pyproject.toml` 入口点 | ✅ | L26-28 已指向 `pyflowx.cli.legacy.*` | -| `pf.py` `_LEGACY_TOOLS` | ✅ | L99-103 已更新 | -| ops/ 4 个子目录 + `__init__.py` | ✅ | files/ dev/ system/ infra/ 均存在 | -| 22 个工具文件已 `git mv` 到子目录 | ✅ | Glob 确认所有 .py 均在子目录下 | -| `_common.py` 保留 ops 根 | ✅ | 8 处 `from pyflowx.ops._common import` 绝对路径仍有效 | -| `pf.py` `_TOOL_MODULES` 注册表(22 项)| ✅ | L106-129 | -| `pf.py` `_run_tool` / `_tool_description` 动态导入 | ✅ | L203-204、L258-259 改用 `self._TOOL_MODULES[target]` | -| `ops/__init__.py` docstring | ✅ | 已文档化 4 个子目录 | -| ops 模块间无交叉引用需改 | ✅ | Grep 确认仅 `_common` 被引用,路径不变 | - -### 未完成(本计划处理) - -1. **20 个测试文件的导入路径未更新**(26 行,含 2 行 `import pyflowx.ops.X` 形式) -2. **`_TOOL_MODULES` 完整性测试未新增** -3. **未跑验证 + 未提交** - -## Proposed Changes - -### 改动 1:更新 20 个测试文件的导入路径 - -所有改动均为机械替换,按子目录分组如下。每处仅替换 `pyflowx.ops` → `pyflowx.ops.`,不动其他内容。 - -**files/ 组(6 文件,6 行):** - -| 文件 | 行号 | 当前 | 修改后 | -|------|------|------|--------| -| [tests/cli/test_filedate.py](file:///home/zhou/pyflowx/tests/cli/test_filedate.py#L7) | 7 | `from pyflowx.ops import filedate as files` | `from pyflowx.ops.files import filedate as files` | -| [tests/cli/test_filelevel.py](file:///home/zhou/pyflowx/tests/cli/test_filelevel.py#L7) | 7 | `from pyflowx.ops import filelevel as files` | `from pyflowx.ops.files import filelevel as files` | -| [tests/cli/test_folderback.py](file:///home/zhou/pyflowx/tests/cli/test_folderback.py#L8) | 8 | `from pyflowx.ops import folderback as files` | `from pyflowx.ops.files import folderback as files` | -| [tests/cli/test_folderzip.py](file:///home/zhou/pyflowx/tests/cli/test_folderzip.py#L8) | 8 | `from pyflowx.ops import folderzip as files` | `from pyflowx.ops.files import folderzip as files` | -| [tests/cli/test_pdftool.py](file:///home/zhou/pyflowx/tests/cli/test_pdftool.py#L11) | 11 | `from pyflowx.ops import pdftool as media` | `from pyflowx.ops.files import pdftool as media` | -| [tests/cli/test_screenshot.py](file:///home/zhou/pyflowx/tests/cli/test_screenshot.py#L9) | 9 | `from pyflowx.ops import screenshot as media` | `from pyflowx.ops.files import screenshot as media` | - -**dev/ 组(8 文件,9 行):** - -| 文件 | 行号 | 当前 | 修改后 | -|------|------|------|--------| -| [tests/cli/test_autofmt.py](file:///home/zhou/pyflowx/tests/cli/test_autofmt.py#L8) | 8 | `from pyflowx.ops import autofmt as dev` | `from pyflowx.ops.dev import autofmt as dev` | -| [tests/cli/test_bumpversion.py](file:///home/zhou/pyflowx/tests/cli/test_bumpversion.py#L10) | 10 | `from pyflowx.ops import bumpversion` | `from pyflowx.ops.dev import bumpversion` | -| [tests/cli/test_gittool.py](file:///home/zhou/pyflowx/tests/cli/test_gittool.py#L11) | 11 | `from pyflowx.ops import gittool as dev` | `from pyflowx.ops.dev import gittool as dev` | -| [tests/cli/test_gittool_tool.py](file:///home/zhou/pyflowx/tests/cli/test_gittool_tool.py#L12-L13) | 12, 13 | `import pyflowx.ops.gittool` + `from pyflowx.ops import gittool` | `import pyflowx.ops.dev.gittool` + `from pyflowx.ops.dev import gittool` | -| [tests/cli/test_lscalc.py](file:///home/zhou/pyflowx/tests/cli/test_lscalc.py#L9) | 9 | `from pyflowx.ops import lscalc as system` | `from pyflowx.ops.dev import lscalc as system` | -| [tests/cli/test_packtool.py](file:///home/zhou/pyflowx/tests/cli/test_packtool.py#L10) | 10 | `from pyflowx.ops import packtool as system` | `from pyflowx.ops.dev import packtool as system` | -| [tests/cli/test_piptool.py](file:///home/zhou/pyflowx/tests/cli/test_piptool.py#L9) | 9 | `from pyflowx.ops import piptool as dev` | `from pyflowx.ops.dev import piptool as dev` | -| [tests/cli/test_pymake_tool.py](file:///home/zhou/pyflowx/tests/cli/test_pymake_tool.py#L6) | 6 | `import pyflowx.ops.pymake` | `import pyflowx.ops.dev.pymake` | - -**system/ 组(2 文件,4 行):** - -| 文件 | 行号 | 当前 | 修改后 | -|------|------|------|--------| -| [tests/cli/test_reseticoncache.py](file:///home/zhou/pyflowx/tests/cli/test_reseticoncache.py#L10) | 10 | `from pyflowx.ops.reseticoncache import reset_icon_cache_run` | `from pyflowx.ops.system.reseticoncache import reset_icon_cache_run` | -| [tests/cli/test_system_run.py](file:///home/zhou/pyflowx/tests/cli/test_system_run.py#L11-L13) | 11, 12, 13 | `from pyflowx.ops.clr import` / `from pyflowx.ops.taskkill import` / `from pyflowx.ops.which import` | `from pyflowx.ops.system.clr import` / `from pyflowx.ops.system.taskkill import` / `from pyflowx.ops.system.which import` | - -**infra/ 组(4 文件,7 行):** - -| 文件 | 行号 | 当前 | 修改后 | -|------|------|------|--------| -| [tests/cli/test_envdev.py](file:///home/zhou/pyflowx/tests/cli/test_envdev.py#L12-L13) | 12, 13 | `from pyflowx.ops.dockercmd import` / `from pyflowx.ops.envdev import (` | `from pyflowx.ops.infra.dockercmd import` / `from pyflowx.ops.infra.envdev import (` | -| [tests/cli/test_envdev_tool.py](file:///home/zhou/pyflowx/tests/cli/test_envdev_tool.py#L6) | 6 | `from pyflowx.ops import envdev, msdownload, sglang` | `from pyflowx.ops.infra import envdev, msdownload, sglang` | -| [tests/cli/test_llm.py](file:///home/zhou/pyflowx/tests/cli/test_llm.py#L12-L13) | 12, 13 | `from pyflowx.ops.msdownload import` / `from pyflowx.ops.sglang import` | `from pyflowx.ops.infra.msdownload import` / `from pyflowx.ops.infra.sglang import` | -| [tests/cli/test_sshcopyid.py](file:///home/zhou/pyflowx/tests/cli/test_sshcopyid.py#L11) | 11 | `from pyflowx.ops import sshcopyid as system` | `from pyflowx.ops.infra import sshcopyid as system` | - -**不动:** [tests/cli/test_ops_common.py](file:///home/zhou/pyflowx/tests/cli/test_ops_common.py#L8) — 引用 `_common`,位置不变。 - -### 改动 2:新增 `_TOOL_MODULES` 完整性测试 - -在 [tests/cli/](file:///home/zhou/pyflowx/tests/cli/) 下新建 `test_tool_modules.py`,断言: -- `_TOOL_ALIASES` 中每个规范名(值集合)都在 `_TOOL_MODULES` 中有对应模块路径 -- 每个注册的模块路径可被 `importlib.import_module` 成功导入 - -目的:防止未来新增工具时遗漏注册表项,导致 `pf <新工具>` 静默失败。 - -测试骨架: - -```python -"""Tests for pf._TOOL_MODULES 注册表完整性.""" - -from __future__ import annotations - -import importlib - -import pytest - -from pyflowx.cli.pf import PfApp - - -class TestToolModulesComplete: - """_TOOL_MODULES 必须覆盖 _TOOL_ALIASES 的所有规范名.""" - - def test_all_canonical_tools_have_module_path(self) -> None: - canonical_names = set(PfApp._TOOL_ALIASES.values()) - module_names = set(PfApp._TOOL_MODULES) - missing = canonical_names - module_names - assert not missing, f"未在 _TOOL_MODULES 注册的工具: {missing}" - - @pytest.mark.parametrize("tool_name, module_path", sorted(PfApp._TOOL_MODULES.items())) - def test_module_importable(self, tool_name: str, module_path: str) -> None: - """每个注册的模块路径必须可导入.""" - importlib.import_module(module_path) -``` - -### 改动 3:验证 + 提交 - -```bash -# 1. lint + 类型检查 -uv run ruff check src/ tests/ -uv run ruff format --check src/ tests/ -uv run pyrefly check src/ - -# 2. 全量测试(项目门槛 95% branch coverage) -uv run pytest -m "not slow" -q - -# 3. CLI 烟囱测试 -uv run pf # 工具列表 -uv run pf pdftool # 多命令工具 (files 组) -uv run pf wch echo # 单命令工具 (system 组) -uv run pf gitt c # gittool (dev 组) -uv run pf emlman --help # legacy 路由 -uv run pxp --help # legacy 入口点 - -# 4. 提交(按文件名 add,遵循 git-commit-message.md 中文风格) -git add src/pyflowx/cli/pf.py \ - src/pyflowx/cli/legacy/__init__.py \ - src/pyflowx/cli/legacy/emlmanager.py \ - src/pyflowx/cli/legacy/profiler.py \ - src/pyflowx/ops/__init__.py \ - src/pyflowx/ops/files/ \ - src/pyflowx/ops/dev/ \ - src/pyflowx/ops/system/ \ - src/pyflowx/ops/infra/ \ - tests/cli/test_tool_modules.py \ - tests/cli/test_emlmanager.py \ - tests/cli/test_profiler.py \ - tests/cli/test_autofmt.py \ - tests/cli/test_bumpversion.py \ - tests/cli/test_envdev.py \ - tests/cli/test_envdev_tool.py \ - tests/cli/test_filedate.py \ - tests/cli/test_filelevel.py \ - tests/cli/test_folderback.py \ - tests/cli/test_folderzip.py \ - tests/cli/test_gittool.py \ - tests/cli/test_gittool_tool.py \ - tests/cli/test_llm.py \ - tests/cli/test_lscalc.py \ - tests/cli/test_packtool.py \ - tests/cli/test_pdftool.py \ - tests/cli/test_piptool.py \ - tests/cli/test_pymake_tool.py \ - tests/cli/test_reseticoncache.py \ - tests/cli/test_screenshot.py \ - tests/cli/test_sshcopyid.py \ - tests/cli/test_system_run.py \ - pyproject.toml -git commit -m "refactor: 按功能划分重构项目结构,ops 工具归入 files/dev/system/infra 子包,legacy 工具归入 cli/legacy" -git push # 仅当分支已跟踪远程 -``` - -## Assumptions & Decisions - -1. **不动 ops 模块内部代码**:已核查 22 个工具模块均用 `import pyflowx as px`(绝对导入)和 `from pyflowx.ops._common import`(_common 位置不变),无相对导入需调整。 -2. **`_TOOL_MODULES` 用类属性而非模块级常量**:与既有 `_TOOL_ALIASES`/`_LEGACY_TOOLS` 保持一致风格。 -3. **完整性测试放 `tests/cli/`**:与既有 `test_pf.py` 等同目录,聚焦 CLI 路由层。 -4. **不修改覆盖率配置**:`omit = ["src/pyflowx/cli/*"]` 递归匹配 `cli/legacy/*`,`src/pyflowx/ops/*` 仍在覆盖范围内(ops 模块本就有测试)。 -5. **不重命名 ops/ 为 tools/**:与顶层 `tools.py` 冲突,收益不匹配风险(沿用原计划决策)。 -6. **git add 按文件名**:遵循项目硬约束,不用 `git add .`/`-A`。移动文件用 `git mv` 已在前序步骤完成,本次 add 仅暂存修改和新增文件。 - -## Verification - -完成标志(逐条核验): -- [ ] `ruff check` 无错误 -- [ ] `ruff format --check` 无需改动 -- [ ] `pyrefly check` 无错误 -- [ ] `pytest -m "not slow"` 全绿,覆盖率 ≥ 95% -- [ ] `pf` 列出全部工具(22 个 @px.tool + 3 个 legacy) -- [ ] `pf pdftool` / `pf wch echo` / `pf gitt c` 三组工具均可调用 -- [ ] `pf emlman --help` / `pxp --help` legacy 路由正常 -- [ ] `test_tool_modules.py` 全绿 -- [ ] commit 已创建,远程已推送(若分支跟踪远程) diff --git a/.trae/documents/项目结构重构计划.md b/.trae/documents/项目结构重构计划.md deleted file mode 100644 index 1e583f6..0000000 --- a/.trae/documents/项目结构重构计划.md +++ /dev/null @@ -1,129 +0,0 @@ -# PyFlowX 项目结构重构计划 - -## Context - -当前项目结构存在三个问题: -1. **cli/ 职责混杂**:`pf.py`(入口)、`emlmanager.py`(1337 行 legacy web 应用)、`profiler.py`(268 行 legacy 性能分析器)混在同一层级,入口与遗留工具不分 -2. **ops/ 下 22 个文件平铺**:从 filedate 到 sglang 全部堆在一级目录,无功能分组,难以浏览和维护 -3. **ops/ 命名歧义**:与顶层 `tools.py`(@px.tool 装饰器框架)容易混淆 - -本次重构通过两个阶段解决:Phase 1 将 legacy 工具归入 `cli/legacy/`,Phase 2 将 ops/ 下工具按功能分入 4 个子目录。**不移动** runner.py 和 tools.py(保留顶层),因为它们是公共 API 库代码且移动涉及覆盖率配置变更和 profiler monkey-patch 链路调整,风险不匹配收益。 - -## 目标结构 - -``` -src/pyflowx/ -├── __init__.py # 公共 API 导出(不变) -├── task.py / graph.py / executors.py / ... # 核心库(不变) -├── runner.py / tools.py # CLI 框架库代码(保留顶层) -├── cli/ -│ ├── pf.py # pf 统一入口 -│ └── legacy/ # ← Phase 1: legacy 工具归位 -│ ├── __init__.py -│ ├── emlmanager.py -│ └── profiler.py -└── ops/ - ├── __init__.py # 更新 docstring - ├── _common.py # 保留根(所有子目录共用) - ├── files/ # ← Phase 2: 文件与文档操作 (6) - │ ├── filedate.py, filelevel.py, folderback.py, folderzip.py - │ ├── pdftool.py, screenshot.py - ├── dev/ # 开发与构建工具 (7) - │ ├── autofmt.py, bumpversion.py, gittool.py, packtool.py - │ ├── piptool.py, pymake.py, lscalc.py - ├── system/ # 系统工具 (4) - │ ├── clr.py, reseticoncache.py, taskkill.py, which.py - └── infra/ # 基础设施与服务部署 (5) - ├── dockercmd.py, envdev.py, sshcopyid.py - ├── msdownload.py, sglang.py -``` - -## Phase 1: cli/ 清理 — legacy 工具归位(低风险) - -**文件移动:** -- `cli/emlmanager.py` → `cli/legacy/emlmanager.py` -- `cli/profiler.py` → `cli/legacy/profiler.py` -- 新建 `cli/legacy/__init__.py` - -**导入修改:** - -| 文件 | 当前 | 修改后 | -|------|------|--------| -| `cli/legacy/profiler.py` L43-46 | `from .. import executors` 等 4 处(`..`=pyflowx) | `from ... import executors` 等(`...`=pyflowx,多一层 legacy/) | - -**配置修改:** - -| 文件 | 修改 | -|------|------| -| `pyproject.toml` L26 | `emlman = "pyflowx.cli.legacy.emlmanager:main"` | -| `pyproject.toml` L28 | `pxp = "pyflowx.cli.legacy.profiler:main"` | -| `cli/pf.py` L100-102 | `_LEGACY_TOOLS` 3 个值改为 `pyflowx.cli.legacy.xxx:main` | - -**测试修改(2 文件):** -- `tests/cli/test_emlmanager.py`:`from pyflowx.cli import emlmanager` → `from pyflowx.cli.legacy import emlmanager` -- `tests/cli/test_profiler.py`:`from pyflowx.cli import profiler` → `from pyflowx.cli.legacy import profiler` - -**覆盖率:** `omit = ["src/pyflowx/cli/*"]` 递归匹配 `cli/legacy/*`,无需改动。 - -## Phase 2: ops/ 按功能分组(中等风险) - -**文件移动(22 个工具文件 + 4 个新建 `__init__.py`):** - -| 子目录 | 移入文件 | -|--------|---------| -| `ops/files/` | filedate, filelevel, folderback, folderzip, pdftool, screenshot | -| `ops/dev/` | autofmt, bumpversion, gittool, packtool, piptool, pymake, lscalc | -| `ops/system/` | clr, reseticoncache, taskkill, which | -| `ops/infra/` | dockercmd, envdev, sshcopyid, msdownload, sglang | - -`_common.py` 保留 `ops/` 根——子目录工具通过绝对路径 `from pyflowx.ops._common import ...` 引用,路径不变。 - -**ops 模块自身导入:** 无需修改。所有 ops 模块用 `import pyflowx as px`(绝对导入)和 `from pyflowx.ops._common import ...`(_common 位置不变)。已核查 ops 模块间无交叉引用。 - -**pf.py 修改 — 新增 `_TOOL_MODULES` 注册表:** - -```python -_TOOL_MODULES: dict[str, str] = { - "autofmt": "pyflowx.ops.dev.autofmt", - "bumpversion": "pyflowx.ops.dev.bumpversion", - # ... 22 个规范工具名 → 完整模块路径 -} -``` - -修改 `_run_tool` 和 `_tool_description` 两处动态导入: -- `importlib.import_module(f"pyflowx.ops.{target}")` → `importlib.import_module(self._TOOL_MODULES[target])` - -**ops/__init__.py 修改:** 更新 docstring,补充 lscalc(当前遗漏),标注分组结构。`__all__` 保持空列表。 - -**测试修改(20 文件,机械替换导入路径):** - -代表性修改: -- `from pyflowx.ops import filedate` → `from pyflowx.ops.files import filedate` -- `from pyflowx.ops.gittool import ...` → `from pyflowx.ops.dev.gittool import ...` -- `from pyflowx.ops.clr import ...` → `from pyflowx.ops.system.clr import ...` -- `from pyflowx.ops.dockercmd import ...` → `from pyflowx.ops.infra.dockercmd import ...` - -`test_ops_common.py` 无需修改(`_common` 位置不变)。 - -**新增测试:** 遍历 `_TOOL_ALIASES` 所有规范名,断言每个都在 `_TOOL_MODULES` 中且模块可导入,防止注册表遗漏。 - -## 不做的改动 - -- **runner.py / tools.py 保留顶层**:它们是公共 API(`px.CliRunner`、`px.tool`),有完整测试。移入 cli/ 涉及覆盖率配置变更(`omit` 从 `cli/*` 改为精确排除)和 profiler monkey-patch 链路调整,风险不匹配收益。 -- **ops/ 不重命名**:重命名为 `tools/` 与顶层 `tools.py` 冲突;其他名字(`cli_tools/`、`commands/`)收益不明显。通过 docstring 明确"ops = CLI 工具实现集合"即可。 -- **核心库模块不动**:task.py、graph.py 等是公共 API 的一部分,用户通过 `px.TaskSpec`、`px.Graph` 访问,移动增加风险且价值不大。 -- **ops/__init__.py 不做 re-export**:保持惰性加载,避免 CLI 启动时加载全部 22 个工具模块。 - -## 验证 - -每个 Phase 完成后: -```bash -uv run ruff check src/ tests/ # lint -uv run pyrefly check src/ # 类型检查 -uv run pytest -m "not slow" -q # 全套测试 (993 个) -uv run pytest tests/cli/ -q # CLI 测试子集(快速反馈) -uv run pf pdftool # 验证多命令工具 -uv run pf wch echo # 验证单命令工具 -uv run pf emlman --help # 验证 legacy 工具路由 -uv run pxp --help # 验证 legacy 工具入口点 -``` diff --git a/README.md b/README.md index c3b41bd..7713912 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPI](https://img.shields.io/pypi/v/pyflowx.svg)](https://pypi.org/project/pyflowx/) [![Python](https://img.shields.io/pypi/pyversions/pyflowx.svg)](https://pypi.org/project/pyflowx/) [![Documentation Status](https://readthedocs.org/projects/pyflowx/badge/?version=latest)](https://pyflowx.readthedocs.io/zh/latest/) -[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/gookeryoung/pyflowx) +[![Coverage](https://img.shields.io/badge/coverage-97%25-brightgreen.svg)](https://github.com/gookeryoung/pyflowx) [![License](https://img.shields.io/pypi/l/pyflowx.svg)](https://github.com/gookeryoung/pyflowx/blob/main/LICENSE) PyFlowX 把"任务依赖"这件事做到极致简单:**参数名就是依赖声明**。无需装饰器、 @@ -32,8 +32,11 @@ PyFlowX 把"任务依赖"这件事做到极致简单:**参数名就是依赖 - **图级默认值** —— `GraphDefaults` 统一配置 retry/timeout/concurrency 等 - **CLI 运行器** —— `CliRunner` 把多个图映射为命令行子命令,替代 Makefile - **可观测** —— `on_event` 回调(RUNNING/SUCCESS/FAILED/SKIPPED)、`dry_run` 预览、`verbose` 生命周期日志、Mermaid 可视化 -- **YAML 任务编排** —— GitHub Actions 风格的声明式任务图,支持 `jobs`/`needs`/`strategy.matrix`/`if` 等 CI/CD 概念,从 YAML 文件直接加载执行 -- **最小依赖** —— 仅依赖标准库 + PyYAML(3.8 需 `graphlib_backport`、`typing-extensions`) +- **进度监控** —— `run(progress=True)` 开箱即用的 rich 进度条;`ProgressCallback` 协议供程序化集成 +- **任务通知** —— `Notifier` 协议支持飞书/钉钉/企业微信 webhook 与 Python 回调,全生命周期事件可配置 +- **流式获取** —— `run_iter()` 生成器逐个 yield 任务结果,适用于大量任务逐个处理 +- **状态后端** —— `MemoryBackend` / `JSONBackend` / `SQLiteBackend`(WAL 模式,适合大规模任务) +- **最小依赖** —— `rich` + `typer` + `typing-extensions`(3.13 以下) - **97% 测试覆盖** —— 分支覆盖率 >= 95% ## 安装 @@ -311,114 +314,6 @@ pf pymake --quiet # 静默模式 `verbose=True`(默认)时打印任务生命周期(开始/成功/失败/跳过)与命令输出;`--quiet` 关闭。 -## YAML 任务编排 - -PyFlowX 支持 GitHub Actions 风格的声明式 YAML 任务编排,从 YAML 文件直接加载任务图。 - -### 编程式 API - -```python -import pyflowx as px - -# 从 YAML 文件加载任务图 -graph = px.Graph.from_yaml("pipeline.yaml") -report = px.run(graph, strategy="thread") - -# 或用函数式 API -graph = px.load_yaml("pipeline.yaml") -graph = px.parse_yaml_string(""" -jobs: - hello: - cmd: ["echo", "hello"] -""") -``` - -### CLI 入口 - -通过 `pf` 统一入口调用(详见 [pf 工具](#cli-工具) 章节): - -```bash -# 执行 YAML 任务图 -pf yamlrun pipeline.yaml - -# 指定执行策略 -pf yamlrun pipeline.yaml --strategy thread - -# 仅打印任务分层,不执行 -pf yamlrun pipeline.yaml --dry-run - -# 列出所有任务名 -pf yamlrun pipeline.yaml --list - -# 静默模式 -pf yamlrun pipeline.yaml --quiet -``` - -### YAML Schema(GitHub Actions 风格) - -```yaml -strategy: thread # 图级默认策略 -defaults: # 图级默认值 - retry: {max_attempts: 3} - verbose: true - env: {CI: "true"} - -jobs: - setup: - cmd: ["git", "clone", "..."] - runs-on: linux - - build: - needs: [setup] # 依赖列表 - cmd: ["python", "-m", "build"] - timeout: 300 - retry: {max_attempts: 2, delay: 1.0} - - test: - needs: [build] - cmd: ["python${{ matrix.version }}", "-m", "pytest"] # 矩阵占位符 - strategy: - matrix: # 笛卡尔积展开为 6 个任务 - version: ["3.8", "3.9", "3.10"] - os: ["linux", "macos"] - if: "env.CI" # 条件: 环境变量存在 - - lint: - needs: [build] - cmd: ["ruff", "check"] - if: "env.CI == 'true'" # 条件: 环境变量等于 - - deploy: - needs: [test, lint] # 矩阵依赖自动展开 - cmd: ["twine", "upload"] - if: "env.DEPLOY_TOKEN != ''" - allow-upstream-skip: true - concurrency-key: deploy_lock -``` - -### 字段映射 - -| YAML 字段 | TaskSpec 字段 | 说明 | -|-----------|---------------|------| -| `jobs.` | `name` | job ID 作为任务名 | -| `cmd` / `run` | `cmd` | `cmd` 为列表形式,`run` 为 shell 字符串 | -| `needs` | `depends_on` | 依赖列表(矩阵任务自动展开) | -| `if` | `conditions` | `success()` / `always()` / `env.VAR` / `env.VAR == 'x'` | -| `strategy.matrix` | 矩阵扇出 | 笛卡尔积展开为多个任务 | -| `${{ matrix.key }}` | 占位符 | 在 cmd/run/cwd/env 中替换 | -| `timeout` | `timeout` | 超时秒数 | -| `retry` | `retry` | `{max_attempts, delay, backoff, jitter}` | -| `cwd` | `cwd` | 工作目录 | -| `env` | `env` | 环境变量 | -| `verbose` | `verbose` | 详细输出 | -| `continue-on-error` | `continue_on_error` | 失败不中止整图 | -| `skip-if-missing` | `skip_if_missing` | 命令不存在时跳过 | -| `allow-upstream-skip` | `allow_upstream_skip` | 上游跳过时仍执行 | -| `priority` | `priority` | 同层优先级 | -| `concurrency-key` | `concurrency_key` | 并发限制键 | -| `tags` | `tags` | 自由标签 | -| `runs-on` | `tags`(追加) | 运行环境标签 | - ## 断点续跑 ```python @@ -475,7 +370,7 @@ except px.PyFlowXError: | 特性 | PyFlowX | Airflow | Prefect | Dask | |------|---------|---------|---------|------| | 零样板 | 参数名即依赖 | 装饰器 + XCom | 装饰器 | 装饰器 | -| 运行时依赖 | 仅标准库 | 重型 | 中型 | 中型 | +| 运行时依赖 | rich + typer | 重型 | 中型 | 中型 | | 类型安全 | mypy strict | 弱 | 中 | 中 | | 异步原生 | 是 | 否 | 部分 | 否 | | 断点续跑 | 内置 | 需配置 | 需配置 | 需配置 | @@ -565,8 +460,8 @@ uv run ruff format --check src tests | 模块 | 职责 | |------|------| | `cli/pf.py` | 统一入口:`pf [command]`,按需导入 `ops/.py` 触发 `@px.tool` 注册 | -| `cli/profiler.py` | 性能分析 CLI | -| `cli/emlmanager.py` | 邮件管理 CLI | +| `cli/legacy/profiler.py` | 性能分析 CLI(legacy) | +| `cli/legacy/emlmanager.py` | 邮件管理 CLI(legacy) | ## 许可证 diff --git a/docs/index.rst b/docs/index.rst index 63337c4..1933526 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,9 +17,8 @@ PyFlowX 是一个轻量、类型安全的 DAG 任务调度器:**参数名就 - **断点续跑** —— ``MemoryBackend`` / ``JSONBackend``,成功结果可缓存复用 - **命令任务** —— ``cmd`` 参数直接执行外部命令 - **条件执行** —— ``conditions`` 按平台、环境变量等条件跳过任务 -- **YAML 任务编排** —— GitHub Actions 风格声明式任务图 - **pf 统一 CLI** —— ``pf [command]`` 调用所有工具 -- **最小依赖** —— 仅依赖标准库 + PyYAML +- **最小依赖** —— ``rich`` + ``typer`` + ``typing-extensions``(3.13 以下) 文档导航 -------- @@ -38,7 +37,6 @@ PyFlowX 是一个轻量、类型安全的 DAG 任务调度器:**参数名就 guide/task guide/graph guide/execution - guide/yaml guide/cli .. toctree:: diff --git a/docs/installation.rst b/docs/installation.rst index db5564e..78f729f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,7 +1,7 @@ 安装 ==== -PyFlowX 支持 Python 3.8+,仅依赖标准库与 PyYAML(3.8 额外需要 ``graphlib_backport`` 和 ``typing-extensions``)。 +PyFlowX 支持 Python 3.10+,运行时依赖 ``rich``、``typer`` 与 ``typing-extensions``(3.13 以下)。 pip 安装 -------- diff --git a/tests/test_notification.py b/tests/test_notification.py index ddc7bc6..2c35a20 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -45,12 +45,14 @@ def test_notification_level_values() -> None: def test_all_levels_contains_all() -> None: """ALL_LEVELS 包含全部四个级别。""" assert ( - frozenset({ - NotificationLevel.RUNNING, - NotificationLevel.SUCCESS, - NotificationLevel.FAILED, - NotificationLevel.SKIPPED, - }) + frozenset( + { + NotificationLevel.RUNNING, + NotificationLevel.SUCCESS, + NotificationLevel.FAILED, + NotificationLevel.SKIPPED, + } + ) == ALL_LEVELS ) diff --git a/tests/test_progress.py b/tests/test_progress.py index d4bd06b..ee80d33 100644 --- a/tests/test_progress.py +++ b/tests/test_progress.py @@ -157,10 +157,12 @@ def _make_simple_graph() -> px.Graph: def task_b(task_a: int) -> int: return task_a + 1 - return px.Graph.from_specs([ - px.TaskSpec("task_a", task_a), - px.TaskSpec("task_b", task_b, depends_on=("task_a",)), - ]) + return px.Graph.from_specs( + [ + px.TaskSpec("task_a", task_a), + px.TaskSpec("task_b", task_b, depends_on=("task_a",)), + ] + ) def test_run_with_progress_true() -> None: diff --git a/tests/test_run_iter.py b/tests/test_run_iter.py index 393031c..e073cbd 100644 --- a/tests/test_run_iter.py +++ b/tests/test_run_iter.py @@ -128,10 +128,12 @@ def test_run_iter_failure_with_continue_on_error() -> None: def fail() -> None: raise ValueError("err") - graph = px.Graph.from_specs([ - px.TaskSpec("fail", fn=fail, continue_on_error=True), - px.TaskSpec("ok", fn=lambda: 1), - ]) + graph = px.Graph.from_specs( + [ + px.TaskSpec("fail", fn=fail, continue_on_error=True), + px.TaskSpec("ok", fn=lambda: 1), + ] + ) results = dict(px.run_iter(graph)) assert results["fail"].status == TaskStatus.FAILED assert results["ok"].status == TaskStatus.SUCCESS @@ -144,10 +146,12 @@ def test_run_iter_yields_skipped_tasks() -> None: """条件跳过的任务以 SKIPPED 状态 yield。""" never_true = lambda _ctx: False # noqa: E731 - graph = px.Graph.from_specs([ - px.TaskSpec("normal", fn=lambda: 1), - px.TaskSpec("skipped", fn=lambda: 2, conditions=(never_true,)), - ]) + graph = px.Graph.from_specs( + [ + px.TaskSpec("normal", fn=lambda: 1), + px.TaskSpec("skipped", fn=lambda: 2, conditions=(never_true,)), + ] + ) results = dict(px.run_iter(graph)) assert results["normal"].status == TaskStatus.SUCCESS assert results["skipped"].status == TaskStatus.SKIPPED