refactor: 全面迁移至 Python 3.9+ 原生泛型类型语法

- 将所有 `Optional[T]` 替换为 `T | None`
- 将所有 `List[T]`/`Dict[K, V]`/`Tuple[Ts, ...]` 替换为对应原生泛型
- 调整类型导入,移除冗余的 typing 导入项
- 更新项目依赖,添加 typing-extensions 兼容旧版本 Python
- 重构部分函数签名与内部实现以匹配新类型语法
This commit is contained in:
2026-06-20 17:52:42 +08:00
parent c06d0284c4
commit 08eb743ea9
18 changed files with 962 additions and 177 deletions
+165
View File
@@ -0,0 +1,165 @@
"""Tests for pymake CLI."""
from pyflowx.cli.pymake import _build_graphs, _get_maturin_build_command, conf
def test_pymake_config_attributes():
"""Test PymakeConfig has expected attributes."""
assert hasattr(conf, "PROJECT_ROOT")
assert hasattr(conf, "BUILD_TOOL")
assert hasattr(conf, "BUILD_COMMAND")
assert hasattr(conf, "MATURIN_TOOL")
assert hasattr(conf, "MATURIN_BUILD_COMMAND")
assert hasattr(conf, "MATURIN_DEV_COMMAND")
assert hasattr(conf, "TIMEOUT")
def test_pymake_config_values():
"""Test PymakeConfig values are correct."""
assert conf.BUILD_TOOL == "uv"
assert conf.BUILD_COMMAND == ["uv", "build"]
assert conf.MATURIN_TOOL == "maturin"
assert conf.TIMEOUT == 600
def test_get_maturin_build_command_basic():
"""Test _get_maturin_build_command returns base command."""
cmd = _get_maturin_build_command()
assert "maturin" in cmd
assert "build" in cmd
assert "-r" in cmd
def test_build_graphs_returns_dict():
"""Test _build_graphs returns a dictionary."""
graphs = _build_graphs()
assert isinstance(graphs, dict)
assert len(graphs) > 0
def test_build_graphs_has_expected_commands():
"""Test _build_graphs has expected command keys."""
graphs = _build_graphs()
expected_commands = [
"b",
"bc",
"ba",
"ic",
"ip",
"ia",
"cp",
"cc",
"ca",
"t",
"lint",
]
for cmd in expected_commands:
assert cmd in graphs, f"Expected command '{cmd}' not found in graphs"
def test_build_graphs_values_are_graphs():
"""Test _build_graphs values are Graph instances."""
import pyflowx as px
graphs = _build_graphs()
for name, graph in graphs.items():
assert isinstance(graph, px.Graph), (
f"Graph for command '{name}' is not a Graph instance"
)
def test_build_command_graph_structure():
"""Test 'b' command graph has correct structure."""
graphs = _build_graphs()
graph = graphs["b"]
assert len(graph.all_specs()) == 1
spec = graph.spec("uv_build")
assert spec.cmd == conf.BUILD_COMMAND
def test_build_all_command_graph_structure():
"""Test 'ba' command graph has correct dependencies."""
graphs = _build_graphs()
graph = graphs["ba"]
specs = graph.all_specs()
assert len(specs) == 2
# Check dependency
uv_build_spec = graph.spec("uv_build")
assert "maturin_build" in uv_build_spec.depends_on
def test_maturin_build_command_graph_structure():
"""Test 'bc' command graph has correct structure."""
graphs = _build_graphs()
graph = graphs["bc"]
specs = graph.all_specs()
assert len(specs) == 1
spec = graph.spec("maturin_build")
assert spec.cmd == _get_maturin_build_command()
def test_install_all_command_graph_structure():
"""Test 'ia' command graph has correct dependencies."""
graphs = _build_graphs()
graph = graphs["ia"]
specs = graph.all_specs()
assert len(specs) == 2
uv_install_spec = graph.spec("uv_install")
assert "maturin_dev" in uv_install_spec.depends_on
def test_clean_all_command_graph_structure():
"""Test 'ca' command graph has correct structure."""
graphs = _build_graphs()
graph = graphs["ca"]
specs = graph.all_specs()
assert len(specs) == 2
def test_test_command_graph_structure():
"""Test 't' command graph has correct structure."""
graphs = _build_graphs()
graph = graphs["t"]
specs = graph.all_specs()
assert len(specs) == 1
spec = graph.spec("pytest")
assert "pytest" in spec.cmd
def test_lint_command_graph_structure():
"""Test 'lint' command graph has correct structure."""
graphs = _build_graphs()
graph = graphs["lint"]
specs = graph.all_specs()
assert len(specs) == 1
spec = graph.spec("ruff_check")
assert "ruff" in spec.cmd
def test_pymake_config_dirs_to_ignore():
"""Test PymakeConfig has correct dirs to ignore."""
assert ".venv" in conf.DIRS_TO_IGNORE
assert ".git" in conf.DIRS_TO_IGNORE
assert ".tox" in conf.DIRS_TO_IGNORE
def test_pymake_config_python_build_dirs():
"""Test PymakeConfig has correct Python build dirs."""
assert "dist" in conf.PYTHON_BUILD_DIRS
assert "build" in conf.PYTHON_BUILD_DIRS
def test_maturin_build_options_win7():
"""Test MATURIN_BUILD_OPTIONS_WIN7 has expected options."""
assert "--target" in conf.MATURIN_BUILD_OPTIONS_WIN7
assert "x86_64-win7-windows-msvc" in conf.MATURIN_BUILD_OPTIONS_WIN7
assert "-Zbuild-std" in conf.MATURIN_BUILD_OPTIONS_WIN7
def test_doc_build_command():
"""Test DOC_BUILD_COMMAND has expected structure."""
assert "sphinx-build" in conf.DOC_BUILD_COMMAND
assert "-b" in conf.DOC_BUILD_COMMAND
assert "html" in conf.DOC_BUILD_COMMAND
+178
View File
@@ -0,0 +1,178 @@
"""Tests for conditions module."""
import os
import sys
from unittest.mock import patch
from pyflowx.conditions import (
IS_LINUX,
IS_MACOS,
IS_POSIX,
IS_WINDOWS,
BuiltinConditions,
Constants,
)
def test_constants_is_windows():
"""Test Constants.IS_WINDOWS is correct."""
assert (sys.platform == "win32") == Constants.IS_WINDOWS
def test_constants_is_linux():
"""Test Constants.IS_LINUX is correct."""
assert (sys.platform == "linux") == Constants.IS_LINUX
def test_constants_is_macos():
"""Test Constants.IS_MACOS is correct."""
assert (sys.platform == "darwin") == Constants.IS_MACOS
def test_constants_is_posix():
"""Test Constants.IS_POSIX is correct."""
assert (sys.platform != "win32") == Constants.IS_POSIX
def test_builtin_conditions_is_windows():
"""Test BuiltinConditions.IS_WINDOWS."""
result = BuiltinConditions.IS_WINDOWS()
assert result == Constants.IS_WINDOWS
def test_builtin_conditions_is_linux():
"""Test BuiltinConditions.IS_LINUX."""
result = BuiltinConditions.IS_LINUX()
assert result == Constants.IS_LINUX
def test_builtin_conditions_is_macos():
"""Test BuiltinConditions.IS_MACOS."""
result = BuiltinConditions.IS_MACOS()
assert result == Constants.IS_MACOS
def test_builtin_conditions_is_posix():
"""Test BuiltinConditions.IS_POSIX."""
result = BuiltinConditions.IS_POSIX()
assert result == Constants.IS_POSIX
def test_builtin_conditions_python_version_major_only():
"""Test BuiltinConditions.PYTHON_VERSION with major only."""
# Test with current Python version
current_major = sys.version_info.major
assert BuiltinConditions.PYTHON_VERSION(current_major) is True
assert BuiltinConditions.PYTHON_VERSION(current_major + 1) is False
def test_builtin_conditions_python_version_with_minor():
"""Test BuiltinConditions.PYTHON_VERSION with major and minor."""
current_major = sys.version_info.major
current_minor = sys.version_info.minor
assert BuiltinConditions.PYTHON_VERSION(current_major, current_minor) is True
assert BuiltinConditions.PYTHON_VERSION(current_major, current_minor + 1) is False
def test_builtin_conditions_python_version_at_least():
"""Test BuiltinConditions.PYTHON_VERSION_AT_LEAST."""
current_major = sys.version_info.major
current_minor = sys.version_info.minor
# Current version should be at least itself
assert (
BuiltinConditions.PYTHON_VERSION_AT_LEAST(current_major, current_minor) is True
)
# Current version should be at least an older version
assert BuiltinConditions.PYTHON_VERSION_AT_LEAST(current_major - 1, 0) is True
# Current version should NOT be at least a newer version
assert BuiltinConditions.PYTHON_VERSION_AT_LEAST(current_major + 1, 0) is False
def test_builtin_conditions_has_app_installed_true():
"""Test BuiltinConditions.HAS_APP_INSTALLED when app exists."""
# Python should always be available
condition = BuiltinConditions.HAS_APP_INSTALLED("python")
assert condition() is True
def test_builtin_conditions_has_app_installed_false():
"""Test BuiltinConditions.HAS_APP_INSTALLED when app doesn't exist."""
condition = BuiltinConditions.HAS_APP_INSTALLED("nonexistent_app_12345")
assert condition() is False
def test_builtin_conditions_env_var_exists_true():
"""Test BuiltinConditions.ENV_VAR_EXISTS when variable exists."""
with patch.dict(os.environ, {"TEST_VAR": "value"}):
condition = BuiltinConditions.ENV_VAR_EXISTS("TEST_VAR")
assert condition() is True
def test_builtin_conditions_env_var_exists_false():
"""Test BuiltinConditions.ENV_VAR_EXISTS when variable doesn't exist."""
condition = BuiltinConditions.ENV_VAR_EXISTS("NONEXISTENT_VAR_12345")
assert condition() is False
def test_builtin_conditions_env_var_equals_true():
"""Test BuiltinConditions.ENV_VAR_EQUALS when value matches."""
with patch.dict(os.environ, {"TEST_VAR": "expected_value"}):
condition = BuiltinConditions.ENV_VAR_EQUALS("TEST_VAR", "expected_value")
assert condition() is True
def test_builtin_conditions_env_var_equals_false():
"""Test BuiltinConditions.ENV_VAR_EQUALS when value doesn't match."""
with patch.dict(os.environ, {"TEST_VAR": "different_value"}):
condition = BuiltinConditions.ENV_VAR_EQUALS("TEST_VAR", "expected_value")
assert condition() is False
def test_builtin_conditions_not():
"""Test BuiltinConditions.NOT."""
true_condition = lambda: True # noqa: E731
false_condition = lambda: False # noqa: E731
not_true = BuiltinConditions.NOT(true_condition)
assert not_true() is False
not_false = BuiltinConditions.NOT(false_condition)
assert not_false() is True
def test_builtin_conditions_and_all_true():
"""Test BuiltinConditions.AND when all conditions are true."""
true_condition = lambda: True # noqa: E731
condition = BuiltinConditions.AND(true_condition, true_condition, true_condition)
assert condition() is True
def test_builtin_conditions_and_one_false():
"""Test BuiltinConditions.AND when one condition is false."""
true_condition = lambda: True # noqa: E731
false_condition = lambda: False # noqa: E731
condition = BuiltinConditions.AND(true_condition, false_condition, true_condition)
assert condition() is False
def test_builtin_conditions_or_all_false():
"""Test BuiltinConditions.OR when all conditions are false."""
false_condition = lambda: False # noqa: E731
condition = BuiltinConditions.OR(false_condition, false_condition, false_condition)
assert condition() is False
def test_builtin_conditions_or_one_true():
"""Test BuiltinConditions.OR when one condition is true."""
true_condition = lambda: True # noqa: E731
false_condition = lambda: False # noqa: E731
condition = BuiltinConditions.OR(false_condition, true_condition, false_condition)
assert condition() is True
def test_exported_conditions():
"""Test exported condition functions."""
assert IS_WINDOWS() == Constants.IS_WINDOWS
assert IS_LINUX() == Constants.IS_LINUX
assert IS_MACOS() == Constants.IS_MACOS
assert IS_POSIX() == Constants.IS_POSIX
+189
View File
@@ -0,0 +1,189 @@
"""Tests for executors module edge cases."""
import asyncio
import sys
import pytest
import pyflowx as px
from pyflowx.task import TaskStatus
# 跨平台的 echo 命令
if sys.platform == "win32":
ECHO_CMD = ["cmd", "/c", "echo"]
else:
ECHO_CMD = ["echo"]
def test_execute_sync_with_timeout():
"""Test execute task with timeout correctly."""
# Note: timeout for Python functions only works in async strategy
# For sync functions, timeout is not enforced in sequential strategy
# This test verifies that the task runs without timeout error
spec = px.TaskSpec("quick", fn=lambda: "result", timeout=10)
graph = px.Graph.from_specs([spec])
# Should succeed without timeout error
report = px.run(graph, strategy="sequential")
assert report.success
def test_execute_async_with_timeout():
"""Test execute async task with timeout correctly."""
async def slow_async_function():
await asyncio.sleep(2)
return "result"
spec = px.TaskSpec("slow_async", fn=slow_async_function, timeout=0.5)
graph = px.Graph.from_specs([spec])
# This should timeout
with pytest.raises(px.TaskFailedError):
px.run(graph, strategy="async")
def test_verbose_event_callback_running():
"""Test verbose event callback for RUNNING status."""
# Create a graph with verbose callback
spec = px.TaskSpec("test", fn=lambda: "result", verbose=True)
graph = px.Graph.from_specs([spec])
report = px.run(graph, strategy="sequential")
# Should print without error
assert report.success
def test_verbose_event_callback_success():
"""Test verbose event callback for SUCCESS status."""
# Create a graph with verbose callback
spec = px.TaskSpec("test", fn=lambda: "result", verbose=True)
graph = px.Graph.from_specs([spec])
report = px.run(graph, strategy="sequential")
# Should print without error
assert report.success
def test_verbose_event_callback_failed():
"""Test verbose event callback for FAILED status."""
# Create a graph with verbose callback and failing task
def raise_error():
raise ValueError("test error")
spec = px.TaskSpec("test", fn=raise_error, verbose=True)
graph = px.Graph.from_specs([spec])
# Should print without error
with pytest.raises(px.TaskFailedError):
px.run(graph, strategy="sequential")
def test_verbose_event_callback_skipped():
"""Test verbose event callback for SKIPPED status."""
# Create a graph with verbose callback and skipped task
spec = px.TaskSpec(
"test",
fn=lambda: "result",
conditions=(lambda: False,),
verbose=True,
)
graph = px.Graph.from_specs([spec])
report = px.run(graph, strategy="sequential")
# Should print without error
assert report.success
def test_execute_sync_with_retries():
"""Test execute task with retries."""
call_count = 0
def failing_function():
nonlocal call_count
call_count += 1
if call_count < 3:
raise ValueError("temporary error")
return "success"
spec = px.TaskSpec("retry_test", fn=failing_function, retries=3)
graph = px.Graph.from_specs([spec])
# Should succeed after retries
report = px.run(graph, strategy="sequential")
assert report.success
assert report.results["retry_test"].attempts == 3
def test_execute_async_with_retries():
"""Test execute async task with retries."""
call_count = 0
async def failing_async_function():
nonlocal call_count
call_count += 1
if call_count < 3:
raise ValueError("temporary error")
return "success"
spec = px.TaskSpec("retry_async_test", fn=failing_async_function, retries=3)
graph = px.Graph.from_specs([spec])
# Should succeed after retries
report = px.run(graph, strategy="async")
assert report.success
assert report.results["retry_async_test"].attempts == 3
def test_execute_sync_skip_on_condition():
"""Test execute task skips task when condition is false."""
spec = px.TaskSpec(
"skip_test",
fn=lambda: "result",
conditions=(lambda: False,),
)
graph = px.Graph.from_specs([spec])
report = px.run(graph, strategy="sequential")
assert report.success
assert report.results["skip_test"].status == TaskStatus.SKIPPED
def test_execute_async_skip_on_condition():
"""Test execute async task skips task when condition is false."""
spec = px.TaskSpec(
"skip_async_test",
fn=lambda: "result",
conditions=(lambda: False,),
)
graph = px.Graph.from_specs([spec])
report = px.run(graph, strategy="async")
assert report.success
assert report.results["skip_async_test"].status == TaskStatus.SKIPPED
def test_execute_sync_with_error():
"""Test execute task handles errors correctly."""
def error_function():
raise ValueError("test error")
spec = px.TaskSpec("error_test", fn=error_function)
graph = px.Graph.from_specs([spec])
with pytest.raises(px.TaskFailedError):
px.run(graph, strategy="sequential")
def test_execute_async_with_error():
"""Test execute async task handles errors correctly."""
async def error_async_function():
raise ValueError("test error")
spec = px.TaskSpec("error_async_test", fn=error_async_function)
graph = px.Graph.from_specs([spec])
with pytest.raises(px.TaskFailedError):
px.run(graph, strategy="async")
+156
View File
@@ -0,0 +1,156 @@
"""Tests for task module edge cases."""
import sys
import tempfile
import pytest
import pyflowx as px
from pyflowx.task import TaskSpec
# 跨平台的 echo 命令
if sys.platform == "win32":
ECHO_CMD = ["cmd", "/c", "echo"]
else:
ECHO_CMD = ["echo"]
def test_taskspec_wrap_cmd_with_list():
"""Test TaskSpec._wrap_cmd with command list."""
spec = TaskSpec("test", cmd=[*ECHO_CMD, "hello"])
wrapped_fn = spec.effective_fn
assert wrapped_fn is not None
assert wrapped_fn.__name__ == "test"
def test_taskspec_wrap_cmd_with_string():
"""Test TaskSpec._wrap_cmd with command string."""
if sys.platform == "win32":
cmd_str = "cmd /c echo hello"
else:
cmd_str = "echo hello"
spec = TaskSpec("test", cmd=cmd_str)
wrapped_fn = spec.effective_fn
assert wrapped_fn is not None
assert wrapped_fn.__name__ == "test"
def test_taskspec_wrap_cmd_with_timeout():
"""Test TaskSpec._wrap_cmd with timeout."""
spec = TaskSpec("test", cmd=[*ECHO_CMD, "hello"], timeout=0.1)
wrapped_fn = spec.effective_fn
# Should not raise timeout error for quick command
result = wrapped_fn()
assert result is None
def test_taskspec_wrap_cmd_with_cwd():
"""Test TaskSpec._wrap_cmd with working directory."""
with tempfile.TemporaryDirectory() as tmpdir:
spec = TaskSpec("test", cmd=[*ECHO_CMD, "hello"], cwd=tmpdir)
wrapped_fn = spec.effective_fn
result = wrapped_fn()
assert result is None
def test_taskspec_wrap_cmd_verbose():
"""Test TaskSpec._wrap_cmd with verbose=True."""
spec = TaskSpec("test", cmd=[*ECHO_CMD, "hello"], verbose=True)
wrapped_fn = spec.effective_fn
# Should print verbose output
result = wrapped_fn()
assert result is None
def test_taskspec_wrap_cmd_error():
"""Test TaskSpec._wrap_cmd handles command error."""
spec = TaskSpec("test", cmd=["python", "-c", "import sys; sys.exit(1)"])
wrapped_fn = spec.effective_fn
with pytest.raises(RuntimeError, match="命令执行失败"):
wrapped_fn()
def test_taskspec_wrap_cmd_file_not_found():
"""Test TaskSpec._wrap_cmd handles file not found."""
spec = TaskSpec("test", cmd=["nonexistent_command"])
wrapped_fn = spec.effective_fn
with pytest.raises(RuntimeError, match="命令未找到"):
wrapped_fn()
def test_taskspec_wrap_cmd_shell_file_not_found():
"""Test TaskSpec._wrap_cmd handles shell command file not found."""
spec = TaskSpec("test", cmd="nonexistent_shell_command")
wrapped_fn = spec.effective_fn
# Shell commands don't raise FileNotFoundError
# They just return non-zero exit code
with pytest.raises(RuntimeError):
wrapped_fn()
def test_taskspec_no_fn_no_cmd():
"""Test TaskSpec raises error when no fn or cmd."""
with pytest.raises(ValueError, match="必须提供 fn 或 cmd 参数"):
TaskSpec("test")
def test_taskspec_cmd_overrides_fn():
"""Test TaskSpec cmd overrides fn."""
def my_fn():
return "fn_result"
spec = TaskSpec("test", fn=my_fn, cmd=[*ECHO_CMD, "hello"])
wrapped_fn = spec.effective_fn
# cmd should override fn
assert wrapped_fn.__name__ == "test"
def test_taskspec_conditions_check():
"""Test TaskSpec.should_execute with conditions."""
spec = px.TaskSpec(
"test",
fn=lambda: "result",
conditions=(lambda: True,),
)
assert spec.should_execute() is True
def test_taskspec_conditions_false():
"""Test TaskSpec.should_execute with false conditions."""
spec = px.TaskSpec(
"test",
fn=lambda: "result",
conditions=(lambda: False,),
)
assert spec.should_execute() is False
def test_taskspec_conditions_multiple():
"""Test TaskSpec.should_execute with multiple conditions."""
spec = px.TaskSpec(
"test",
fn=lambda: "result",
conditions=(lambda: True, lambda: True, lambda: True),
)
assert spec.should_execute() is True
def test_taskspec_conditions_multiple_one_false():
"""Test TaskSpec.should_execute with one false condition."""
spec = px.TaskSpec(
"test",
fn=lambda: "result",
conditions=(lambda: True, lambda: False, lambda: True),
)
assert spec.should_execute() is False