From 0afdb54e5c350e373008af38bb77e9fd6f55e19c Mon Sep 17 00:00:00 2001 From: gooker_young Date: Thu, 25 Jun 2026 12:36:47 +0800 Subject: [PATCH] ~ --- pyproject.toml | 2 +- src/pyflowx/__init__.py | 2 +- src/pyflowx/cli/gittool.py | 1 + src/pyflowx/cli/pymake.py | 8 +- src/pyflowx/runner.py | 36 +++-- tests/test_command_refs.py | 303 +++++++++++++++++++++++++++++++++++++ 6 files changed, 334 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f0a725..1ea65f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ license = { text = "MIT" } name = "pyflowx" readme = "README.md" requires-python = ">=3.8" -version = "0.1.10" +version = "0.1.12" [project.scripts] autofmt = "pyflowx.cli.autofmt:main" diff --git a/src/pyflowx/__init__.py b/src/pyflowx/__init__.py index 7612115..f3f6828 100644 --- a/src/pyflowx/__init__.py +++ b/src/pyflowx/__init__.py @@ -84,7 +84,7 @@ from .runner import CliExitCode, CliRunner from .storage import JSONBackend, MemoryBackend, StateBackend from .task import TaskCmd, TaskEvent, TaskResult, TaskSpec, TaskStatus -__version__ = "0.1.10" +__version__ = "0.1.12" __all__ = [ "IS_LINUX", diff --git a/src/pyflowx/cli/gittool.py b/src/pyflowx/cli/gittool.py index fd03e97..066301a 100644 --- a/src/pyflowx/cli/gittool.py +++ b/src/pyflowx/cli/gittool.py @@ -23,6 +23,7 @@ EXCLUDE_DIRS = [ ".tox", ".pytest_cache", "node_modules", + ".ruff_cache", ] EXCLUDE_CMDS = [arg for d in EXCLUDE_DIRS for arg in ["-e", d]] diff --git a/src/pyflowx/cli/pymake.py b/src/pyflowx/cli/pymake.py index f8d980e..81a738f 100644 --- a/src/pyflowx/cli/pymake.py +++ b/src/pyflowx/cli/pymake.py @@ -49,6 +49,7 @@ test_coverage: px.TaskSpec = px.TaskSpec( ruff_lint: px.TaskSpec = px.TaskSpec("lint", cmd=["ruff", "check", "--fix", "--unsafe-fixes"]) ruff_format: px.TaskSpec = px.TaskSpec("format", cmd=["ruff", "format", "."], depends_on=("lint",)) typecheck: px.TaskSpec = px.TaskSpec("pyrefly_check", cmd=["pyrefly", "check", "."]) +git_add_all: px.TaskSpec = px.TaskSpec("git_add_all", cmd=["git", "add", "-A"]) bump: px.TaskSpec = px.TaskSpec("bumpversion", cmd=["bumpversion", "-t"]) doc: px.TaskSpec = px.TaskSpec("doc", cmd=["sphinx-build", "-b", "html", "docs", "docs/_build"]) git_push: px.TaskSpec = px.TaskSpec("git_push", cmd=["git", "push"]) @@ -86,7 +87,10 @@ def main(): 📦 发布命令: pymake pb - 发布到 PyPI (twine + hatch) - 💡 常用工作流: + � 版本管理: + pymake bump - 自动升级版本号并提交修改 (清理 + 检查 + 格式化 + git add + bumpversion) + + �💡 常用工作流: 1. 日常开发: pymake lint && pymake t 2. 构建发布包: pymake ba 3. 多版本兼容性测试: pymake tox @@ -113,7 +117,7 @@ def main(): # 清理命令 "c": px.Graph.from_specs([git_clean]), # 开发工具 - "bump": px.Graph.from_specs(["c", "tc", bump]), + "bump": px.Graph.from_specs(["c", "tc", git_add_all, bump]), "cov": px.Graph.from_specs([git_clean, test_coverage]), "doc": px.Graph.from_specs([doc]), "lint": px.Graph.from_specs([ruff_lint, ruff_format]), diff --git a/src/pyflowx/runner.py b/src/pyflowx/runner.py index 80a1b67..c9e3ad0 100644 --- a/src/pyflowx/runner.py +++ b/src/pyflowx/runner.py @@ -163,15 +163,13 @@ class CliRunner: if not pending_refs: return graph - # 收集所有TaskSpec(包括原始图中的) + # 收集所有TaskSpec(按正确顺序:先引用,后原始TaskSpec) all_specs: list[TaskSpec[Any]] = [] - for spec in graph.all_specs().values(): - all_specs.append(spec) # 记录每个引用展开后的所有任务名,用于建立依赖链 previous_ref_last_task: str | None = None - # 解析每个引用,并建立依赖关系 + # 先解析每个引用,并建立依赖关系 for ref in pending_refs: expanded_specs = self._parse_ref(ref, current_cmd) @@ -190,17 +188,27 @@ class CliRunner: all_specs.extend(expanded_specs) - # 如果原始图中有TaskSpec,让它们依赖于最后一个引用的任务 + # 然后添加原始图中的TaskSpec,并让它们按顺序执行 original_specs = list(graph.all_specs().values()) - if previous_ref_last_task and original_specs: - # 为每个原始TaskSpec添加依赖 - for i, original_task in enumerate(original_specs): - # 只为第一个原始任务添加依赖,或者为没有依赖的任务添加依赖 - if i == 0 or not original_task.depends_on: - updated_task = replace( - original_task, depends_on=tuple({*original_task.depends_on, previous_ref_last_task}) - ) - all_specs[all_specs.index(original_task)] = updated_task + if original_specs: + # 第一个原始TaskSpec依赖于最后一个引用的任务 + if previous_ref_last_task: + first_original = original_specs[0] + updated_first = replace( + first_original, depends_on=tuple({*first_original.depends_on, previous_ref_last_task}) + ) + all_specs.append(updated_first) + else: + # 如果没有引用,直接添加第一个原始TaskSpec + all_specs.append(original_specs[0]) + + # 后续的原始TaskSpec依赖于前一个原始TaskSpec + for i in range(1, len(original_specs)): + current_task = original_specs[i] + previous_task_name = original_specs[i - 1].name + # 更新依赖,确保顺序执行 + updated_task = replace(current_task, depends_on=tuple({*current_task.depends_on, previous_task_name})) + all_specs.append(updated_task) # 创建新的图(不包含引用) return Graph.from_specs(all_specs) diff --git a/tests/test_command_refs.py b/tests/test_command_refs.py index 7af3c50..72563cc 100644 --- a/tests/test_command_refs.py +++ b/tests/test_command_refs.py @@ -194,3 +194,306 @@ class TestCommandReferences: # Verify total layers assert len(layers) == 4 + + def test_execution_order_multiple_original_tasks(self) -> None: + """Should execute multiple original TaskSpecs in correct order.""" + task1 = px.TaskSpec("task1", cmd=["echo", "1"]) + task2 = px.TaskSpec("task2", cmd=["echo", "2"]) + task3 = px.TaskSpec("task3", cmd=["echo", "3"]) + task4 = px.TaskSpec("task4", cmd=["echo", "4"]) + task5 = px.TaskSpec("task5", cmd=["echo", "5"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "cmd1": px.Graph.from_specs([task1]), + "cmd2": px.Graph.from_specs([task2]), + "all": px.Graph.from_specs(["cmd1", "cmd2", task3, task4, task5]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["all"].layers() + + # Layer 1: task1 (cmd1) + assert "task1" in layers[0] + + # Layer 2: task2 (cmd2) + assert "task2" in layers[1] + + # Layer 3: task3 (first original TaskSpec) + assert "task3" in layers[2] + + # Layer 4: task4 (second original TaskSpec) + assert "task4" in layers[3] + + # Layer 5: task5 (third original TaskSpec) + assert "task5" in layers[4] + + # Verify total layers + assert len(layers) == 5 + + def test_execution_order_with_internal_dependencies(self) -> None: + """Should preserve internal dependencies within referenced commands.""" + task1 = px.TaskSpec("task1", cmd=["echo", "1"]) + task2 = px.TaskSpec("task2", cmd=["echo", "2"], depends_on=("task1",)) + task3 = px.TaskSpec("task3", cmd=["echo", "3"]) + task4 = px.TaskSpec("task4", cmd=["echo", "4"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "cmd1": px.Graph.from_specs([task1, task2]), + "cmd2": px.Graph.from_specs([task3]), + "all": px.Graph.from_specs(["cmd1", "cmd2", task4]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["all"].layers() + + # Layer 1: task1 + assert "task1" in layers[0] + + # Layer 2: task2 (depends on task1) + assert "task2" in layers[1] + + # Layer 3: task3 (cmd2, depends on task2) + assert "task3" in layers[2] + + # Layer 4: task4 (original TaskSpec, depends on task3) + assert "task4" in layers[3] + + # Verify total layers + assert len(layers) == 4 + + def test_execution_order_pymake_bump_scenario(self) -> None: + """Should execute pymake bump command in correct order.""" + # Simulate pymake bump scenario + git_clean = px.TaskSpec("git_clean", cmd=["echo", "clean"]) + typecheck = px.TaskSpec("typecheck", cmd=["echo", "typecheck"]) + lint = px.TaskSpec("lint", cmd=["echo", "lint"]) + format_task = px.TaskSpec("format", cmd=["echo", "format"], depends_on=("lint",)) + git_add_all = px.TaskSpec("git_add_all", cmd=["echo", "git add -A"]) + bump = px.TaskSpec("bumpversion", cmd=["echo", "bumpversion -t"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "c": px.Graph.from_specs([git_clean]), + "tc": px.Graph.from_specs([typecheck, "lint"]), + "lint": px.Graph.from_specs([lint, format_task]), + "bump": px.Graph.from_specs(["c", "tc", git_add_all, bump]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["bump"].layers() + + # Layer 1: git_clean (c) + assert "git_clean" in layers[0] + + # Layer 2: lint (tc.lint, depends on git_clean) + assert "lint" in layers[1] + + # Layer 3: format (tc.lint.format, depends on lint) + assert "format" in layers[2] + + # Layer 4: typecheck (tc.typecheck, depends on format) + assert "typecheck" in layers[3] + + # Layer 5: git_add_all (original TaskSpec, depends on typecheck) + assert "git_add_all" in layers[4] + + # Layer 6: bumpversion (original TaskSpec, depends on git_add_all) + assert "bumpversion" in layers[5] + + # Verify total layers + assert len(layers) == 6 + + def test_execution_order_only_references(self) -> None: + """Should execute only references without original TaskSpecs.""" + task1 = px.TaskSpec("task1", cmd=["echo", "1"]) + task2 = px.TaskSpec("task2", cmd=["echo", "2"]) + task3 = px.TaskSpec("task3", cmd=["echo", "3"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "cmd1": px.Graph.from_specs([task1]), + "cmd2": px.Graph.from_specs([task2]), + "cmd3": px.Graph.from_specs([task3]), + "all": px.Graph.from_specs(["cmd1", "cmd2", "cmd3"]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["all"].layers() + + # Layer 1: task1 (cmd1) + assert "task1" in layers[0] + + # Layer 2: task2 (cmd2, depends on task1) + assert "task2" in layers[1] + + # Layer 3: task3 (cmd3, depends on task2) + assert "task3" in layers[2] + + # Verify total layers + assert len(layers) == 3 + + def test_execution_order_only_original_tasks(self) -> None: + """Should execute only original TaskSpecs without references.""" + task1 = px.TaskSpec("task1", cmd=["echo", "1"]) + task2 = px.TaskSpec("task2", cmd=["echo", "2"]) + task3 = px.TaskSpec("task3", cmd=["echo", "3"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "all": px.Graph.from_specs([task1, task2, task3]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["all"].layers() + + # All tasks should be in layer 1 (no dependencies) + assert "task1" in layers[0] + assert "task2" in layers[0] + assert "task3" in layers[0] + + # Verify total layers + assert len(layers) == 1 + + def test_execution_order_single_reference(self) -> None: + """Should execute single reference correctly.""" + task1 = px.TaskSpec("task1", cmd=["echo", "1"]) + task2 = px.TaskSpec("task2", cmd=["echo", "2"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "cmd1": px.Graph.from_specs([task1, task2]), + "all": px.Graph.from_specs(["cmd1"]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["all"].layers() + + # Should have the same structure as cmd1 + assert "task1" in layers[0] + assert "task2" in layers[0] + + # Verify total layers + assert len(layers) == 1 + + def test_execution_order_deep_nesting(self) -> None: + """Should execute deeply nested references correctly.""" + task1 = px.TaskSpec("task1", cmd=["echo", "1"]) + task2 = px.TaskSpec("task2", cmd=["echo", "2"]) + task3 = px.TaskSpec("task3", cmd=["echo", "3"]) + task4 = px.TaskSpec("task4", cmd=["echo", "4"]) + task5 = px.TaskSpec("task5", cmd=["echo", "5"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "cmd1": px.Graph.from_specs([task1]), + "cmd2": px.Graph.from_specs(["cmd1", task2]), + "cmd3": px.Graph.from_specs(["cmd2", task3]), + "cmd4": px.Graph.from_specs(["cmd3", task4]), + "cmd5": px.Graph.from_specs(["cmd4", task5]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["cmd5"].layers() + + # Should execute in order: task1 -> task2 -> task3 -> task4 -> task5 + assert "task1" in layers[0] + assert "task2" in layers[1] + assert "task3" in layers[2] + assert "task4" in layers[3] + assert "task5" in layers[4] + + # Verify total layers + assert len(layers) == 5 + + def test_execution_order_with_parallel_tasks_in_reference(self) -> None: + """Should handle parallel tasks within referenced commands.""" + task1 = px.TaskSpec("task1", cmd=["echo", "1"]) + task2 = px.TaskSpec("task2", cmd=["echo", "2"]) + task3 = px.TaskSpec("task3", cmd=["echo", "3"]) + task4 = px.TaskSpec("task4", cmd=["echo", "4"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "cmd1": px.Graph.from_specs([task1, task2]), # Parallel tasks + "cmd2": px.Graph.from_specs([task3, task4]), # Parallel tasks + "all": px.Graph.from_specs(["cmd1", "cmd2"]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["all"].layers() + + # Layer 1: task1 and task2 (cmd1, parallel) + assert "task1" in layers[0] + assert "task2" in layers[0] + + # Layer 2: task3 and task4 (cmd2, depends on cmd1's last task) + # Note: Both task3 and task4 should depend on the last task of cmd1 + assert "task3" in layers[1] + assert "task4" in layers[1] + + # Verify total layers + assert len(layers) == 2 + + def test_execution_order_complex_mixed_scenario(self) -> None: + """Should handle complex mixed scenario with references and TaskSpecs.""" + # Create a complex scenario + clean = px.TaskSpec("clean", cmd=["echo", "clean"]) + build1 = px.TaskSpec("build1", cmd=["echo", "build1"]) + build2 = px.TaskSpec("build2", cmd=["echo", "build2"], depends_on=("build1",)) + test1 = px.TaskSpec("test1", cmd=["echo", "test1"]) + test2 = px.TaskSpec("test2", cmd=["echo", "test2"]) + package = px.TaskSpec("package", cmd=["echo", "package"]) + deploy = px.TaskSpec("deploy", cmd=["echo", "deploy"]) + + runner = px.CliRunner( + strategy="sequential", + graphs={ + "clean": px.Graph.from_specs([clean]), + "build": px.Graph.from_specs([build1, build2]), + "test": px.Graph.from_specs([test1, test2]), + "release": px.Graph.from_specs(["clean", "build", "test", package, deploy]), + }, + ) + + # Check execution order through layers + layers = runner.graphs["release"].layers() + + # Layer 1: clean + assert "clean" in layers[0] + + # Layer 2: build1 (depends on clean) + assert "build1" in layers[1] + + # Layer 3: build2 (depends on build1) + assert "build2" in layers[2] + + # Layer 4: test1 and test2 (depends on build2) + assert "test1" in layers[3] + assert "test2" in layers[3] + + # Layer 5: package (depends on test1/test2) + assert "package" in layers[4] + + # Layer 6: deploy (depends on package) + assert "deploy" in layers[5] + + # Verify total layers + assert len(layers) == 6