~
Release / Pre-release Check (push) Failing after 1m31s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped

This commit is contained in:
2026-06-25 12:36:47 +08:00
parent 9e99a1f1ba
commit 0afdb54e5c
6 changed files with 334 additions and 18 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ license = { text = "MIT" }
name = "pyflowx" name = "pyflowx"
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
version = "0.1.10" version = "0.1.12"
[project.scripts] [project.scripts]
autofmt = "pyflowx.cli.autofmt:main" autofmt = "pyflowx.cli.autofmt:main"
+1 -1
View File
@@ -84,7 +84,7 @@ from .runner import CliExitCode, CliRunner
from .storage import JSONBackend, MemoryBackend, StateBackend from .storage import JSONBackend, MemoryBackend, StateBackend
from .task import TaskCmd, TaskEvent, TaskResult, TaskSpec, TaskStatus from .task import TaskCmd, TaskEvent, TaskResult, TaskSpec, TaskStatus
__version__ = "0.1.10" __version__ = "0.1.12"
__all__ = [ __all__ = [
"IS_LINUX", "IS_LINUX",
+1
View File
@@ -23,6 +23,7 @@ EXCLUDE_DIRS = [
".tox", ".tox",
".pytest_cache", ".pytest_cache",
"node_modules", "node_modules",
".ruff_cache",
] ]
EXCLUDE_CMDS = [arg for d in EXCLUDE_DIRS for arg in ["-e", d]] EXCLUDE_CMDS = [arg for d in EXCLUDE_DIRS for arg in ["-e", d]]
+6 -2
View File
@@ -49,6 +49,7 @@ test_coverage: px.TaskSpec = px.TaskSpec(
ruff_lint: px.TaskSpec = px.TaskSpec("lint", cmd=["ruff", "check", "--fix", "--unsafe-fixes"]) 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",)) ruff_format: px.TaskSpec = px.TaskSpec("format", cmd=["ruff", "format", "."], depends_on=("lint",))
typecheck: px.TaskSpec = px.TaskSpec("pyrefly_check", cmd=["pyrefly", "check", "."]) 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"]) bump: px.TaskSpec = px.TaskSpec("bumpversion", cmd=["bumpversion", "-t"])
doc: px.TaskSpec = px.TaskSpec("doc", cmd=["sphinx-build", "-b", "html", "docs", "docs/_build"]) 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"]) git_push: px.TaskSpec = px.TaskSpec("git_push", cmd=["git", "push"])
@@ -86,7 +87,10 @@ def main():
📦 发布命令: 📦 发布命令:
pymake pb - 发布到 PyPI (twine + hatch) pymake pb - 发布到 PyPI (twine + hatch)
💡 常用工作流: 版本管理:
pymake bump - 自动升级版本号并提交修改 (清理 + 检查 + 格式化 + git add + bumpversion)
💡 常用工作流:
1. 日常开发: pymake lint && pymake t 1. 日常开发: pymake lint && pymake t
2. 构建发布包: pymake ba 2. 构建发布包: pymake ba
3. 多版本兼容性测试: pymake tox 3. 多版本兼容性测试: pymake tox
@@ -113,7 +117,7 @@ def main():
# 清理命令 # 清理命令
"c": px.Graph.from_specs([git_clean]), "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]), "cov": px.Graph.from_specs([git_clean, test_coverage]),
"doc": px.Graph.from_specs([doc]), "doc": px.Graph.from_specs([doc]),
"lint": px.Graph.from_specs([ruff_lint, ruff_format]), "lint": px.Graph.from_specs([ruff_lint, ruff_format]),
+22 -14
View File
@@ -163,15 +163,13 @@ class CliRunner:
if not pending_refs: if not pending_refs:
return graph return graph
# 收集所有TaskSpec包括原始图中的 # 收集所有TaskSpec按正确顺序:先引用,后原始TaskSpec
all_specs: list[TaskSpec[Any]] = [] all_specs: list[TaskSpec[Any]] = []
for spec in graph.all_specs().values():
all_specs.append(spec)
# 记录每个引用展开后的所有任务名,用于建立依赖链 # 记录每个引用展开后的所有任务名,用于建立依赖链
previous_ref_last_task: str | None = None previous_ref_last_task: str | None = None
# 解析每个引用,并建立依赖关系 # 解析每个引用,并建立依赖关系
for ref in pending_refs: for ref in pending_refs:
expanded_specs = self._parse_ref(ref, current_cmd) expanded_specs = self._parse_ref(ref, current_cmd)
@@ -190,17 +188,27 @@ class CliRunner:
all_specs.extend(expanded_specs) all_specs.extend(expanded_specs)
# 如果原始图中TaskSpec,让它们依赖于最后一个引用的任务 # 然后添加原始图中TaskSpec让它们按顺序执行
original_specs = list(graph.all_specs().values()) original_specs = list(graph.all_specs().values())
if previous_ref_last_task and original_specs: if original_specs:
# 为每个原始TaskSpec添加依赖 # 第一个原始TaskSpec依赖于最后一个引用的任务
for i, original_task in enumerate(original_specs): if previous_ref_last_task:
# 只为第一个原始任务添加依赖,或者为没有依赖的任务添加依赖 first_original = original_specs[0]
if i == 0 or not original_task.depends_on: updated_first = replace(
updated_task = replace( first_original, depends_on=tuple({*first_original.depends_on, previous_ref_last_task})
original_task, depends_on=tuple({*original_task.depends_on, previous_ref_last_task}) )
) all_specs.append(updated_first)
all_specs[all_specs.index(original_task)] = updated_task 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) return Graph.from_specs(all_specs)
+303
View File
@@ -194,3 +194,306 @@ class TestCommandReferences:
# Verify total layers # Verify total layers
assert len(layers) == 4 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