feat: 添加上游任务跳过豁免、进程检查条件及相关优化
1. 新增allow_upstream_skip参数支持任务不跟随上游跳过 2. 新增IS_RUNNING内置条件检查进程运行状态 3. 调整skip_if_missing默认值为False 4. 补充跳过任务的事件上报和verbose打印 5. 优化reset_icon_cache示例任务使用新特性 6. 更新测试用例匹配默认参数变更
This commit is contained in:
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@@ -66,6 +67,43 @@ class BuiltinConditions:
|
|||||||
"""
|
"""
|
||||||
return sys.version_info >= (major, minor)
|
return sys.version_info >= (major, minor)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def IS_RUNNING(app_name: str) -> Condition:
|
||||||
|
"""检查指定应用是否正在运行.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
app_name : str
|
||||||
|
应用名称 (如 "explorer", "chrome", "python").
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Condition
|
||||||
|
条件判断函数.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _check() -> bool:
|
||||||
|
if Constants.IS_WINDOWS:
|
||||||
|
# Windows: 使用 tasklist 命令
|
||||||
|
result = subprocess.run(
|
||||||
|
["tasklist", "/nh", "/fi", f"imagename eq {app_name}"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return app_name.lower() in result.stdout.lower()
|
||||||
|
else:
|
||||||
|
# Linux/macOS: 使用 pgrep 命令
|
||||||
|
result = subprocess.run(
|
||||||
|
["pgrep", "-x", app_name],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
_check.__name__ = f"IS_RUNNING({app_name!r})"
|
||||||
|
return _check
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def HAS_INSTALLED(app_name: str) -> Condition:
|
def HAS_INSTALLED(app_name: str) -> Condition:
|
||||||
"""检查指定应用是否已安装.
|
"""检查指定应用是否已安装.
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ def _check_upstream_skipped(
|
|||||||
if report is None:
|
if report is None:
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
# 若任务允许上游跳过,则不检查上游状态
|
||||||
|
if spec.allow_upstream_skip:
|
||||||
|
return False, None
|
||||||
|
|
||||||
for dep in spec.depends_on:
|
for dep in spec.depends_on:
|
||||||
if dep in report.results and report.results[dep].status == TaskStatus.SKIPPED:
|
if dep in report.results and report.results[dep].status == TaskStatus.SKIPPED:
|
||||||
return True, f"上游任务 '{dep}' 被跳过"
|
return True, f"上游任务 '{dep}' 被跳过"
|
||||||
@@ -155,6 +159,9 @@ def _run_sync_with_retry(
|
|||||||
result.status = TaskStatus.SKIPPED
|
result.status = TaskStatus.SKIPPED
|
||||||
result.finished_at = datetime.now()
|
result.finished_at = datetime.now()
|
||||||
result.reason = skip_reason
|
result.reason = skip_reason
|
||||||
|
_emit(on_event, result)
|
||||||
|
if spec.verbose:
|
||||||
|
print(f"[skip] 任务 '{spec.name}' 跳过: {skip_reason}", flush=True)
|
||||||
logger.info("task %r skipped (上游任务被跳过)", spec.name)
|
logger.info("task %r skipped (上游任务被跳过)", spec.name)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -164,6 +171,9 @@ def _run_sync_with_retry(
|
|||||||
result.status = TaskStatus.SKIPPED
|
result.status = TaskStatus.SKIPPED
|
||||||
result.finished_at = datetime.now()
|
result.finished_at = datetime.now()
|
||||||
result.reason = skip_reason
|
result.reason = skip_reason
|
||||||
|
_emit(on_event, result)
|
||||||
|
if spec.verbose:
|
||||||
|
print(f"[skip] 任务 '{spec.name}' 跳过: {skip_reason}", flush=True)
|
||||||
logger.info("task %r skipped (条件不满足)", spec.name)
|
logger.info("task %r skipped (条件不满足)", spec.name)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -232,6 +242,9 @@ async def _run_async_with_retry(
|
|||||||
result.status = TaskStatus.SKIPPED
|
result.status = TaskStatus.SKIPPED
|
||||||
result.finished_at = datetime.now()
|
result.finished_at = datetime.now()
|
||||||
result.reason = skip_reason
|
result.reason = skip_reason
|
||||||
|
_emit(on_event, result)
|
||||||
|
if spec.verbose:
|
||||||
|
print(f"[skip] 任务 '{spec.name}' 跳过: {skip_reason}", flush=True)
|
||||||
logger.info("task %r skipped (上游任务被跳过)", spec.name)
|
logger.info("task %r skipped (上游任务被跳过)", spec.name)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -241,6 +254,9 @@ async def _run_async_with_retry(
|
|||||||
result.status = TaskStatus.SKIPPED
|
result.status = TaskStatus.SKIPPED
|
||||||
result.finished_at = datetime.now()
|
result.finished_at = datetime.now()
|
||||||
result.reason = skip_reason
|
result.reason = skip_reason
|
||||||
|
_emit(on_event, result)
|
||||||
|
if spec.verbose:
|
||||||
|
print(f"[skip] 任务 '{spec.name}' 跳过: {skip_reason}", flush=True)
|
||||||
logger.info("task %r skipped (条件不满足)", spec.name)
|
logger.info("task %r skipped (条件不满足)", spec.name)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -123,6 +123,10 @@ class TaskSpec(Generic[T]):
|
|||||||
(标记为 SKIPPED)而非失败。适用于构建工具场景,避免因未安装
|
(标记为 SKIPPED)而非失败。适用于构建工具场景,避免因未安装
|
||||||
某些工具(如 maturin、tox)而导致整个图执行失败。
|
某些工具(如 maturin、tox)而导致整个图执行失败。
|
||||||
对于 ``str`` (shell) 和 ``Callable`` 类型的 ``cmd``,此参数无效。
|
对于 ``str`` (shell) 和 ``Callable`` 类型的 ``cmd``,此参数无效。
|
||||||
|
allow_upstream_skip:
|
||||||
|
若为 ``True``,当上游任务因条件不满足被跳过时,本任务仍会执行
|
||||||
|
(而非跟随跳过)。适用于清理类任务:即使某些删除操作因目标不存在
|
||||||
|
而跳过,后续操作(如重启服务)仍应执行。默认为 ``False``。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
@@ -137,7 +141,8 @@ class TaskSpec(Generic[T]):
|
|||||||
conditions: Tuple[Condition, ...] = ()
|
conditions: Tuple[Condition, ...] = ()
|
||||||
cwd: Optional[Path] = None
|
cwd: Optional[Path] = None
|
||||||
verbose: bool = False
|
verbose: bool = False
|
||||||
skip_if_missing: bool = True
|
skip_if_missing: bool = False
|
||||||
|
allow_upstream_skip: bool = False
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not self.name:
|
if not self.name:
|
||||||
|
|||||||
@@ -27,30 +27,40 @@ def reset_icon_cache() -> list[px.TaskSpec]:
|
|||||||
print("reset_icon_cache: 仅在 Windows 上支持")
|
print("reset_icon_cache: 仅在 Windows 上支持")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
local_app_data = os.environ.get("LOCALAPPDATA", "")
|
||||||
|
icon_cache_db = Path(local_app_data) / "IconCache.db"
|
||||||
|
explorer_cache_dir = Path(local_app_data) / "Microsoft" / "Windows" / "Explorer"
|
||||||
|
|
||||||
return [
|
return [
|
||||||
px.TaskSpec(
|
px.TaskSpec(
|
||||||
"kill_explorer",
|
"kill_explorer",
|
||||||
fn=lambda: subprocess.run(["taskkill", "/f", "/im", "explorer.exe"], check=False),
|
cmd=["taskkill", "/f", "/im", "explorer.exe"],
|
||||||
|
conditions=(BuiltinConditions.IS_RUNNING("explorer.exe"),),
|
||||||
verbose=True,
|
verbose=True,
|
||||||
),
|
),
|
||||||
px.TaskSpec(
|
px.TaskSpec(
|
||||||
"delete_icon_cache",
|
"delete_icon_cache",
|
||||||
fn=lambda: subprocess.run(["del", "/a", "/q", r"%localappdata%\IconCache.db"], check=False),
|
cmd=["cmd", "/c", "del", "/a", "/q", str(icon_cache_db)],
|
||||||
conditions=(BuiltinConditions.DIR_EXISTS(Path(r"%localappdata%\IconCache.db")),),
|
conditions=(BuiltinConditions.DIR_EXISTS(icon_cache_db),),
|
||||||
|
depends_on=("kill_explorer",),
|
||||||
verbose=True,
|
verbose=True,
|
||||||
),
|
),
|
||||||
px.TaskSpec(
|
px.TaskSpec(
|
||||||
"delete_icon_cache_all",
|
"delete_icon_cache_all",
|
||||||
fn=lambda: subprocess.run(
|
cmd=["cmd", "/c", "del", "/a", "/q", str(explorer_cache_dir / "iconcache*")],
|
||||||
["del", "/a", "/q", r"%localappdata%\Microsoft\Windows\Explorer\iconcache*"], check=False
|
conditions=(BuiltinConditions.DIR_EXISTS(explorer_cache_dir),),
|
||||||
),
|
depends_on=("kill_explorer",),
|
||||||
conditions=(BuiltinConditions.DIR_EXISTS(Path(r"%localappdata%\Microsoft\Windows\Explorer")),),
|
|
||||||
verbose=True,
|
verbose=True,
|
||||||
),
|
),
|
||||||
px.TaskSpec(
|
px.TaskSpec(
|
||||||
"restart_explorer",
|
"restart_explorer",
|
||||||
fn=lambda: subprocess.run(["explorer.exe"], check=False),
|
cmd=["cmd", "/c", "start", "explorer.exe"],
|
||||||
conditions=(BuiltinConditions.HAS_INSTALLED("explorer.exe"),),
|
conditions=(
|
||||||
|
BuiltinConditions.HAS_INSTALLED("explorer.exe"),
|
||||||
|
BuiltinConditions.NOT(BuiltinConditions.IS_RUNNING("explorer.exe")),
|
||||||
|
),
|
||||||
|
depends_on=("delete_icon_cache", "delete_icon_cache_all"),
|
||||||
|
allow_upstream_skip=True,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -77,25 +77,25 @@ class TestTaskSpecDefinitions:
|
|||||||
"""uv_build spec should be properly defined."""
|
"""uv_build spec should be properly defined."""
|
||||||
assert pymake.uv_build.name == "uv_build"
|
assert pymake.uv_build.name == "uv_build"
|
||||||
assert pymake.uv_build.cmd == ["uv", "build"]
|
assert pymake.uv_build.cmd == ["uv", "build"]
|
||||||
assert pymake.uv_build.skip_if_missing is True
|
assert pymake.uv_build.skip_if_missing is False
|
||||||
|
|
||||||
def test_maturin_build_spec(self) -> None:
|
def test_maturin_build_spec(self) -> None:
|
||||||
"""maturin_build spec should be properly defined."""
|
"""maturin_build spec should be properly defined."""
|
||||||
assert pymake.maturin_build.name == "maturin_build"
|
assert pymake.maturin_build.name == "maturin_build"
|
||||||
assert isinstance(pymake.maturin_build.cmd, list)
|
assert isinstance(pymake.maturin_build.cmd, list)
|
||||||
assert pymake.maturin_build.skip_if_missing is True
|
assert pymake.maturin_build.skip_if_missing is False
|
||||||
|
|
||||||
def test_uv_sync_spec(self) -> None:
|
def test_uv_sync_spec(self) -> None:
|
||||||
"""uv_sync spec should be properly defined."""
|
"""uv_sync spec should be properly defined."""
|
||||||
assert pymake.uv_sync.name == "uv_sync"
|
assert pymake.uv_sync.name == "uv_sync"
|
||||||
assert pymake.uv_sync.cmd == ["uv", "sync"]
|
assert pymake.uv_sync.cmd == ["uv", "sync"]
|
||||||
assert pymake.uv_sync.skip_if_missing is True
|
assert pymake.uv_sync.skip_if_missing is False
|
||||||
|
|
||||||
def test_git_clean_spec(self) -> None:
|
def test_git_clean_spec(self) -> None:
|
||||||
"""git_clean spec should be properly defined."""
|
"""git_clean spec should be properly defined."""
|
||||||
assert pymake.git_clean.name == "git_clean"
|
assert pymake.git_clean.name == "git_clean"
|
||||||
assert pymake.git_clean.cmd == ["gitt", "c"]
|
assert pymake.git_clean.cmd == ["gitt", "c"]
|
||||||
assert pymake.git_clean.skip_if_missing is True
|
assert pymake.git_clean.skip_if_missing is False
|
||||||
|
|
||||||
def test_test_spec(self) -> None:
|
def test_test_spec(self) -> None:
|
||||||
"""test spec should be properly defined."""
|
"""test spec should be properly defined."""
|
||||||
@@ -135,13 +135,13 @@ class TestTaskSpecDefinitions:
|
|||||||
assert pymake.doc.name == "doc"
|
assert pymake.doc.name == "doc"
|
||||||
assert isinstance(pymake.doc.cmd, list)
|
assert isinstance(pymake.doc.cmd, list)
|
||||||
assert "sphinx-build" in pymake.doc.cmd
|
assert "sphinx-build" in pymake.doc.cmd
|
||||||
assert pymake.doc.skip_if_missing is True
|
assert pymake.doc.skip_if_missing is False
|
||||||
|
|
||||||
def test_hatch_publish_spec(self) -> None:
|
def test_hatch_publish_spec(self) -> None:
|
||||||
"""hatch_publish spec should be properly defined."""
|
"""hatch_publish spec should be properly defined."""
|
||||||
assert pymake.hatch_publish.name == "publish_python"
|
assert pymake.hatch_publish.name == "publish_python"
|
||||||
assert pymake.hatch_publish.cmd == ["hatch", "publish"]
|
assert pymake.hatch_publish.cmd == ["hatch", "publish"]
|
||||||
assert pymake.hatch_publish.skip_if_missing is True
|
assert pymake.hatch_publish.skip_if_missing is False
|
||||||
|
|
||||||
def test_twine_publish_spec(self) -> None:
|
def test_twine_publish_spec(self) -> None:
|
||||||
"""twine_publish spec should be properly defined."""
|
"""twine_publish spec should be properly defined."""
|
||||||
@@ -149,13 +149,13 @@ class TestTaskSpecDefinitions:
|
|||||||
assert isinstance(pymake.twine_publish.cmd, list)
|
assert isinstance(pymake.twine_publish.cmd, list)
|
||||||
assert "twine" in pymake.twine_publish.cmd
|
assert "twine" in pymake.twine_publish.cmd
|
||||||
assert "upload" in pymake.twine_publish.cmd
|
assert "upload" in pymake.twine_publish.cmd
|
||||||
assert pymake.twine_publish.skip_if_missing is True
|
assert pymake.twine_publish.skip_if_missing is False
|
||||||
|
|
||||||
def test_tox_spec(self) -> None:
|
def test_tox_spec(self) -> None:
|
||||||
"""tox spec should be properly defined."""
|
"""tox spec should be properly defined."""
|
||||||
assert pymake.tox.name == "tox"
|
assert pymake.tox.name == "tox"
|
||||||
assert pymake.tox.cmd == ["tox", "-p", "auto"]
|
assert pymake.tox.cmd == ["tox", "-p", "auto"]
|
||||||
assert pymake.tox.skip_if_missing is True
|
assert pymake.tox.skip_if_missing is False
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------- #
|
# ---------------------------------------------------------------------- #
|
||||||
|
|||||||
Reference in New Issue
Block a user