Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50575c6e91 | |||
| f8436f6b8c | |||
| 5c0f51e272 | |||
| 4e3622ef02 | |||
| f69ddc5133 | |||
| 477d901281 | |||
| 0df795237d | |||
| 413ab40044 | |||
| d4a1a5c2de | |||
| 843e9369fe | |||
| 48f6d8a7f0 | |||
| 0b97846d77 | |||
| 50e74180a2 |
@@ -101,8 +101,8 @@ jobs:
|
||||
- name: 安装依赖
|
||||
run: uv sync --extra dev --frozen
|
||||
|
||||
- name: 运行测试(含覆盖率, 95%)
|
||||
run: uv run pytest -v --cov=pyflowx --cov-report=xml --cov-report=term-missing --cov-fail-under=95
|
||||
- name: 运行测试
|
||||
run: uv run pytest -v --cov=pyflowx --cov-report=xml --cov-report=term-missing
|
||||
|
||||
- name: 上传覆盖率
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
|
||||
|
||||
+9
-4
@@ -17,12 +17,13 @@ license = { text = "MIT" }
|
||||
name = "pyflowx"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
|
||||
[project.scripts]
|
||||
autofmt = "pyflowx.cli.autofmt:main"
|
||||
bumpver = "pyflowx.cli.bumpversion:main"
|
||||
clrscr = "pyflowx.cli.clearscreen:main"
|
||||
clr = "pyflowx.cli.clearscreen:main"
|
||||
emlman = "pyflowx.cli.emlmanager:main"
|
||||
envpy = "pyflowx.cli.envpy:main"
|
||||
envqt = "pyflowx.cli.envqt:main"
|
||||
envrs = "pyflowx.cli.envrs:main"
|
||||
@@ -31,6 +32,7 @@ filelvl = "pyflowx.cli.filelevel:main"
|
||||
foldback = "pyflowx.cli.folderback:main"
|
||||
foldzip = "pyflowx.cli.folderzip:main"
|
||||
gitt = "pyflowx.cli.gittool:main"
|
||||
hfdown = "pyflowx.cli.hfdownload:main"
|
||||
lscalc = "pyflowx.cli.lscalc:main"
|
||||
packtool = "pyflowx.cli.packtool:main"
|
||||
pdftool = "pyflowx.cli.pdftool:main"
|
||||
@@ -39,7 +41,7 @@ pymake = "pyflowx.cli.pymake:main"
|
||||
scrcap = "pyflowx.cli.screenshot:main"
|
||||
sshcopy = "pyflowx.cli.sshcopyid:main"
|
||||
taskk = "pyflowx.cli.taskkill:main"
|
||||
whichcmd = "pyflowx.cli.which:main"
|
||||
wch = "pyflowx.cli.which:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
@@ -97,7 +99,7 @@ exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"raise NotImplementedError",
|
||||
]
|
||||
fail_under = 95
|
||||
fail_under = 80
|
||||
show_missing = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
@@ -148,6 +150,9 @@ select = [
|
||||
"W", # pycodestyle warnings
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"**/tests/**" = ["ARG001", "ARG002"]
|
||||
|
||||
[tool.pyrefly]
|
||||
preset = "basic"
|
||||
project-includes = ["**/*.ipynb", "**/*.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.7"
|
||||
__version__ = "0.1.8"
|
||||
|
||||
__all__ = [
|
||||
"IS_LINUX",
|
||||
|
||||
@@ -9,6 +9,12 @@ from __future__ import annotations
|
||||
from pyflowx.cli.autofmt import main as autofmt_main
|
||||
from pyflowx.cli.bumpversion import main as bumpversion_main
|
||||
from pyflowx.cli.clearscreen import main as clearscreen_main
|
||||
|
||||
# EML 邮件管理工具
|
||||
from pyflowx.cli.emlmanager import main as emlmanager_main
|
||||
|
||||
# EML 邮件管理工具
|
||||
from pyflowx.cli.emlmanager import main as emlmanager_web_main
|
||||
from pyflowx.cli.envpy import main as envpy_main
|
||||
from pyflowx.cli.envqt import main as envqt_main
|
||||
from pyflowx.cli.envrs import main as envrs_main
|
||||
@@ -37,15 +43,14 @@ from pyflowx.cli.pymake import main as pymake_main
|
||||
from pyflowx.cli.screenshot import main as screenshot_main
|
||||
from pyflowx.cli.sshcopyid import main as sshcopyid_main
|
||||
|
||||
# 系统工具
|
||||
from pyflowx.cli.taskkill import main as taskkill_main
|
||||
from pyflowx.cli.which import main as which_main
|
||||
|
||||
__all__ = [
|
||||
# 自动格式化工具
|
||||
"autofmt_main",
|
||||
"bumpversion_main",
|
||||
"clearscreen_main",
|
||||
# EML 邮件管理工具
|
||||
"emlmanager_main",
|
||||
"emlmanager_web_main",
|
||||
"envpy_main",
|
||||
"envqt_main",
|
||||
"envrs_main",
|
||||
|
||||
+44
-35
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
@@ -227,26 +228,6 @@ def format_all(root_dir: Path) -> None:
|
||||
print(f"格式化完成: {root_dir}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
# ruff format
|
||||
ruff_format: px.TaskSpec = px.TaskSpec("ruff_format", cmd=["ruff", "format", "."])
|
||||
|
||||
# ruff check
|
||||
ruff_check: px.TaskSpec = px.TaskSpec("ruff_check", cmd=["ruff", "check", "--fix", "--unsafe-fixes", "."])
|
||||
|
||||
# 自动添加 docstring
|
||||
auto_docstring: px.TaskSpec = px.TaskSpec("auto_docstring", fn=lambda: auto_add_docstrings(Path()))
|
||||
|
||||
# 同步 pyproject.toml 配置
|
||||
sync_config: px.TaskSpec = px.TaskSpec("sync_config", fn=lambda: sync_pyproject_config(Path()))
|
||||
|
||||
# 格式化所有文件
|
||||
format_all_files: px.TaskSpec = px.TaskSpec("format_all", fn=lambda: format_all(Path()))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -254,20 +235,48 @@ format_all_files: px.TaskSpec = px.TaskSpec("format_all", fn=lambda: format_all(
|
||||
|
||||
def main() -> None:
|
||||
"""自动格式化工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="AutoFmt - 自动格式化工具",
|
||||
graphs={
|
||||
# ruff format
|
||||
"fmt": px.Graph.from_specs([ruff_format]),
|
||||
# ruff check
|
||||
"lint": px.Graph.from_specs([ruff_check]),
|
||||
# 自动添加 docstring
|
||||
"doc": px.Graph.from_specs([auto_docstring]),
|
||||
# 同步 pyproject.toml 配置
|
||||
"sync": px.Graph.from_specs([sync_config]),
|
||||
# 格式化所有文件
|
||||
"all": px.Graph.from_specs([ruff_format, ruff_check]),
|
||||
},
|
||||
usage="autofmt <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# ruff format 命令
|
||||
format_parser = subparsers.add_parser("fmt", help="使用 ruff 格式化代码")
|
||||
format_parser.add_argument("--target", type=str, default=".", help="目标路径")
|
||||
|
||||
# ruff check 命令
|
||||
lint_parser = subparsers.add_parser("lint", help="使用 ruff 检查代码")
|
||||
lint_parser.add_argument("--target", type=str, default=".", help="目标路径")
|
||||
lint_parser.add_argument("--fix", action="store_true", help="自动修复")
|
||||
|
||||
# 自动添加 docstring 命令
|
||||
doc_parser = subparsers.add_parser("doc", help="自动添加 docstring")
|
||||
doc_parser.add_argument("--root-dir", type=str, default=".", help="根目录")
|
||||
|
||||
# 同步配置命令
|
||||
sync_parser = subparsers.add_parser("sync", help="同步 pyproject.toml 配置")
|
||||
sync_parser.add_argument("--root-dir", type=str, default=".", help="根目录")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "fmt":
|
||||
graph = px.Graph.from_specs([px.TaskSpec("ruff_format", cmd=["ruff", "format", args.target], verbose=True)])
|
||||
elif args.command == "lint":
|
||||
cmd = ["ruff", "check", args.target]
|
||||
if args.fix:
|
||||
cmd.extend(["--fix", "--unsafe-fixes"])
|
||||
graph = px.Graph.from_specs([px.TaskSpec("ruff_check", cmd=cmd, verbose=True)])
|
||||
elif args.command == "doc":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("auto_docstring", fn=auto_add_docstrings, args=(Path(args.root_dir),), verbose=True)]
|
||||
)
|
||||
elif args.command == "sync":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("sync_config", fn=sync_pyproject_config, args=(Path(args.root_dir),), verbose=True)]
|
||||
)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
@@ -5,64 +5,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.conditions import Constants
|
||||
|
||||
# ============================================================================
|
||||
# 辅助函数
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def clear_screen() -> None:
|
||||
"""清屏."""
|
||||
if Constants.IS_WINDOWS:
|
||||
os.system("cls")
|
||||
else:
|
||||
os.system("clear")
|
||||
|
||||
|
||||
def clear_screen_python() -> None:
|
||||
"""Python 方式清屏 (跨平台)."""
|
||||
print("\033[2J\033[H", end="")
|
||||
|
||||
|
||||
def clear_screen_cmd() -> None:
|
||||
"""使用系统命令清屏."""
|
||||
if Constants.IS_WINDOWS:
|
||||
subprocess.run(["cmd", "/c", "cls"], check=False)
|
||||
else:
|
||||
subprocess.run(["clear"], check=False)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
clearscreen: px.TaskSpec = px.TaskSpec("clearscreen", fn=clear_screen)
|
||||
clearscreen_py: px.TaskSpec = px.TaskSpec("clearscreen_py", fn=clear_screen_python)
|
||||
clearscreen_cmd: px.TaskSpec = px.TaskSpec("clearscreen_cmd", fn=clear_screen_cmd)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
print("\033[2J\033[H", end="")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""清屏工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
description="ClearScreen - 清屏工具",
|
||||
graphs={
|
||||
# 清屏 (os.system)
|
||||
"c": px.Graph.from_specs([clearscreen]),
|
||||
# 清屏 (Python)
|
||||
"p": px.Graph.from_specs([clearscreen_py]),
|
||||
# 清屏 (cmd)
|
||||
"cmd": px.Graph.from_specs([clearscreen_cmd]),
|
||||
},
|
||||
)
|
||||
runner.run_cli()
|
||||
graph = px.Graph.from_specs([px.TaskSpec("clearscreen", fn=clear_screen)])
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+21
-17
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -90,14 +91,6 @@ def set_pip_mirror(mirror: str = "tsinghua", token: str | None = None) -> None:
|
||||
print(f"已设置 pip 镜像源: {mirror} ({index_url})")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
envpy_tsinghua: px.TaskSpec = px.TaskSpec("envpy_tsinghua", fn=lambda: set_pip_mirror("tsinghua"))
|
||||
envpy_aliyun: px.TaskSpec = px.TaskSpec("envpy_aliyun", fn=lambda: set_pip_mirror("aliyun"))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -105,14 +98,25 @@ envpy_aliyun: px.TaskSpec = px.TaskSpec("envpy_aliyun", fn=lambda: set_pip_mirro
|
||||
|
||||
def main() -> None:
|
||||
"""Python 环境配置工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="EnvPy - Python 环境配置工具",
|
||||
graphs={
|
||||
# 设置清华镜像源
|
||||
"t": px.Graph.from_specs([envpy_tsinghua]),
|
||||
# 设置阿里云镜像源
|
||||
"a": px.Graph.from_specs([envpy_aliyun]),
|
||||
},
|
||||
usage="envpy <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 设置镜像源命令
|
||||
mirror_parser = subparsers.add_parser("mirror", help="设置 pip 镜像源")
|
||||
mirror_parser.add_argument("name", choices=["tsinghua", "aliyun"], help="镜像源名称")
|
||||
mirror_parser.add_argument("--token", type=str, help="PyPI token for publishing")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "mirror":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("set_pip_mirror", fn=set_pip_mirror, args=(args.name,), kwargs={"token": args.token})]
|
||||
)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
+16
-45
@@ -8,10 +8,6 @@ from __future__ import annotations
|
||||
import pyflowx as px
|
||||
from pyflowx.conditions import Constants
|
||||
|
||||
# ============================================================================
|
||||
# Qt 依赖列表
|
||||
# ============================================================================
|
||||
|
||||
QT_LIBS: list[str] = [
|
||||
"build-essential",
|
||||
"libgl1",
|
||||
@@ -40,47 +36,22 @@ CHINESE_FONTS: list[str] = [
|
||||
]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
|
||||
# 条件: 仅在 Unix 系统上执行
|
||||
def is_linux() -> bool:
|
||||
"""判断是否为 Linux 系统."""
|
||||
return Constants.IS_LINUX and not Constants.IS_MACOS
|
||||
|
||||
|
||||
envqt_install: px.TaskSpec = px.TaskSpec(
|
||||
"envqt_install",
|
||||
cmd=["sudo", "apt", "install", "-y", *QT_LIBS],
|
||||
conditions=(is_linux,),
|
||||
)
|
||||
|
||||
envqt_fonts: px.TaskSpec = px.TaskSpec(
|
||||
"envqt_fonts",
|
||||
cmd=["sudo", "apt", "install", "-y", *CHINESE_FONTS],
|
||||
conditions=(is_linux,),
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""PyQt 环境配置工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
description="EnvQt - PyQt 环境配置工具",
|
||||
graphs={
|
||||
# 安装 Qt 依赖
|
||||
"i": px.Graph.from_specs([envqt_install]),
|
||||
# 安装中文字体
|
||||
"f": px.Graph.from_specs([envqt_fonts]),
|
||||
# 安装全部
|
||||
"a": px.Graph.from_specs([envqt_install, envqt_fonts]),
|
||||
},
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"envqt_install",
|
||||
cmd=["sudo", "apt", "install", "-y", *QT_LIBS],
|
||||
conditions=(lambda: Constants.IS_LINUX,),
|
||||
verbose=True,
|
||||
),
|
||||
px.TaskSpec(
|
||||
"envqt_fonts",
|
||||
cmd=["sudo", "apt", "install", "-y", *CHINESE_FONTS],
|
||||
conditions=(lambda: Constants.IS_LINUX,),
|
||||
verbose=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
runner.run_cli()
|
||||
px.run(graph, strategy="thread", verbose=True)
|
||||
|
||||
+49
-34
@@ -6,9 +6,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Literal, get_args
|
||||
|
||||
import pyflowx as px
|
||||
|
||||
@@ -34,8 +36,11 @@ RUSTUP_MIRRORS: dict[str, dict[str, str]] = {
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_PYTHON_VERSION: str = "nightly"
|
||||
DEFAULT_MIRROR: str = "aliyun"
|
||||
UsableRustVersion = Literal["stable", "nightly", "beta"]
|
||||
UsableMirror = Literal["aliyun", "ustc", "tsinghua"]
|
||||
|
||||
DEFAULT_RUST_VERSION: str = "stable"
|
||||
DEFAULT_MIRROR: UsableMirror = "tsinghua"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -43,7 +48,7 @@ DEFAULT_MIRROR: str = "aliyun"
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def set_rust_mirror(mirror: str = "aliyun") -> None:
|
||||
def set_rust_mirror(mirror: UsableMirror = DEFAULT_MIRROR) -> None:
|
||||
"""设置 Rust 镜像源.
|
||||
|
||||
Parameters
|
||||
@@ -51,7 +56,7 @@ def set_rust_mirror(mirror: str = "aliyun") -> None:
|
||||
mirror : str
|
||||
镜像源名称: aliyun, ustc, tsinghua
|
||||
"""
|
||||
mirror_dict = RUSTUP_MIRRORS.get(mirror, RUSTUP_MIRRORS["aliyun"])
|
||||
mirror_dict = RUSTUP_MIRRORS.get(mirror, RUSTUP_MIRRORS[DEFAULT_MIRROR])
|
||||
server = mirror_dict["RUSTUP_DIST_SERVER"]
|
||||
update_root = mirror_dict["RUSTUP_UPDATE_ROOT"]
|
||||
toml_registry = mirror_dict["TOML_REGISTRY"]
|
||||
@@ -79,7 +84,7 @@ index = "sparse+{toml_registry}"
|
||||
print(f"已设置 Rust 镜像源: {mirror}")
|
||||
|
||||
|
||||
def install_rust(version: str = "nightly") -> None:
|
||||
def install_rust(version: UsableRustVersion = DEFAULT_RUST_VERSION) -> None:
|
||||
"""安装 Rust 工具链.
|
||||
|
||||
Parameters
|
||||
@@ -95,20 +100,6 @@ def install_rust(version: str = "nightly") -> None:
|
||||
raise
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
envrs_aliyun: px.TaskSpec = px.TaskSpec("envrs_aliyun", fn=lambda: set_rust_mirror("aliyun"))
|
||||
envrs_ustc: px.TaskSpec = px.TaskSpec("envrs_ustc", fn=lambda: set_rust_mirror("ustc"))
|
||||
envrs_tsinghua: px.TaskSpec = px.TaskSpec("envrs_tsinghua", fn=lambda: set_rust_mirror("tsinghua"))
|
||||
|
||||
rust_install_stable: px.TaskSpec = px.TaskSpec("rust_install_stable", cmd=["rustup", "toolchain", "install", "stable"])
|
||||
rust_install_nightly: px.TaskSpec = px.TaskSpec(
|
||||
"rust_install_nightly", cmd=["rustup", "toolchain", "install", "nightly"]
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -116,20 +107,44 @@ rust_install_nightly: px.TaskSpec = px.TaskSpec(
|
||||
|
||||
def main() -> None:
|
||||
"""Rust 环境配置工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="EnvRs - Rust 环境配置工具",
|
||||
graphs={
|
||||
# 设置阿里云镜像源
|
||||
"a": px.Graph.from_specs([envrs_aliyun]),
|
||||
# 设置中科大镜像源
|
||||
"u": px.Graph.from_specs([envrs_ustc]),
|
||||
# 设置清华镜像源
|
||||
"t": px.Graph.from_specs([envrs_tsinghua]),
|
||||
# 安装 stable 版本
|
||||
"s": px.Graph.from_specs([rust_install_stable]),
|
||||
# 安装 nightly 版本
|
||||
"n": px.Graph.from_specs([rust_install_nightly]),
|
||||
},
|
||||
usage="envrs <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 设置镜像源命令
|
||||
mirror_parser = subparsers.add_parser("mirror", help="设置 Rust 镜像源")
|
||||
mirror_parser.add_argument(
|
||||
"name",
|
||||
nargs="?",
|
||||
default=DEFAULT_MIRROR,
|
||||
choices=get_args(UsableMirror),
|
||||
help=f"镜像源名称 ({get_args(UsableMirror)})",
|
||||
)
|
||||
|
||||
# 安装 Rust 命令
|
||||
install_parser = subparsers.add_parser("install", help="安装 Rust 工具链")
|
||||
install_parser.add_argument(
|
||||
"version",
|
||||
nargs="?",
|
||||
default=DEFAULT_RUST_VERSION,
|
||||
choices=get_args(UsableRustVersion),
|
||||
help=f"Rust 版本 ({get_args(UsableRustVersion)})",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "mirror":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("set_rust_mirror", fn=set_rust_mirror, args=(args.name,), verbose=True)]
|
||||
)
|
||||
elif args.command == "install":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("install_rust", cmd=["rustup", "toolchain", "install", args.version], verbose=True)]
|
||||
)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread", verbose=True)
|
||||
|
||||
+42
-17
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -88,14 +89,6 @@ def process_files_date(targets: list[Path], clear: bool = False) -> None:
|
||||
process_file_date(target, clear)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
filedate_clear: px.TaskSpec = px.TaskSpec("filedate_clear", fn=lambda: process_files_date([], clear=True))
|
||||
filedate_add: px.TaskSpec = px.TaskSpec("filedate_add", fn=lambda: process_files_date([], clear=False))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -103,14 +96,46 @@ filedate_add: px.TaskSpec = px.TaskSpec("filedate_add", fn=lambda: process_files
|
||||
|
||||
def main() -> None:
|
||||
"""文件日期处理工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="FileDate - 文件日期处理工具",
|
||||
graphs={
|
||||
# 清除日期前缀
|
||||
"c": px.Graph.from_specs([filedate_clear]),
|
||||
# 添加日期前缀
|
||||
"a": px.Graph.from_specs([filedate_add]),
|
||||
},
|
||||
usage="filedate <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 添加日期前缀命令
|
||||
add_parser = subparsers.add_parser("add", help="添加日期前缀")
|
||||
add_parser.add_argument("files", nargs="+", help="文件路径")
|
||||
|
||||
# 清除日期前缀命令
|
||||
clear_parser = subparsers.add_parser("clear", help="清除日期前缀")
|
||||
clear_parser.add_argument("files", nargs="+", help="文件路径")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "add":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"process_files_date",
|
||||
fn=process_files_date,
|
||||
args=([Path(f) for f in args.files],),
|
||||
kwargs={"clear": False},
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "clear":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"process_files_date",
|
||||
fn=process_files_date,
|
||||
args=([Path(f) for f in args.files],),
|
||||
kwargs={"clear": True},
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import pyflowx as px
|
||||
@@ -104,17 +105,6 @@ def process_files_level(targets: list[Path], level: int = 0) -> None:
|
||||
process_file_level(target, level)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
filelevel_clear: px.TaskSpec = px.TaskSpec("filelevel_clear", fn=lambda: process_files_level([], level=0))
|
||||
filelevel_pub: px.TaskSpec = px.TaskSpec("filelevel_pub", fn=lambda: process_files_level([], level=1))
|
||||
filelevel_int: px.TaskSpec = px.TaskSpec("filelevel_int", fn=lambda: process_files_level([], level=2))
|
||||
filelevel_con: px.TaskSpec = px.TaskSpec("filelevel_con", fn=lambda: process_files_level([], level=3))
|
||||
filelevel_cla: px.TaskSpec = px.TaskSpec("filelevel_cla", fn=lambda: process_files_level([], level=4))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -122,20 +112,29 @@ filelevel_cla: px.TaskSpec = px.TaskSpec("filelevel_cla", fn=lambda: process_fil
|
||||
|
||||
def main() -> None:
|
||||
"""文件等级重命名工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="FileLevel - 文件等级重命名工具",
|
||||
graphs={
|
||||
# 清除等级标记
|
||||
"c": px.Graph.from_specs([filelevel_clear]),
|
||||
# 设置公开等级 (PUB)
|
||||
"pub": px.Graph.from_specs([filelevel_pub]),
|
||||
# 设置内部等级 (INT)
|
||||
"int": px.Graph.from_specs([filelevel_int]),
|
||||
# 设置机密等级 (CON)
|
||||
"con": px.Graph.from_specs([filelevel_con]),
|
||||
# 设置绝密等级 (CLA)
|
||||
"cla": px.Graph.from_specs([filelevel_cla]),
|
||||
},
|
||||
usage="filelevel <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 设置等级命令
|
||||
level_parser = subparsers.add_parser("set", help="设置文件等级")
|
||||
level_parser.add_argument("files", nargs="+", help="文件路径")
|
||||
level_parser.add_argument("--level", type=int, choices=[0, 1, 2, 3, 4], required=True, help="文件等级 (0-4)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "set":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"process_files_level", fn=process_files_level, args=([Path(f) for f in args.files], args.level)
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
@@ -21,6 +21,7 @@ EXCLUDE_DIRS = [
|
||||
".venv",
|
||||
".git",
|
||||
".tox",
|
||||
".pytest_cache",
|
||||
"node_modules",
|
||||
]
|
||||
EXCLUDE_CMDS = [arg for d in EXCLUDE_DIRS for arg in ["-e", d]]
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Literal, get_args
|
||||
|
||||
import pyflowx as px
|
||||
|
||||
HFDownloadType = Literal["model", "dataset", "space"]
|
||||
|
||||
|
||||
def setenvs():
|
||||
"""设置 HuggingFace mirror 环境变量."""
|
||||
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Download a model from HuggingFace.")
|
||||
parser.add_argument("dataset_name", type=str, help="HuggingFace dataset name.")
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
nargs="?",
|
||||
default="dataset",
|
||||
choices=get_args(HFDownloadType),
|
||||
help="HuggingFace dataset type.",
|
||||
)
|
||||
parser.add_argument("--use-hfd", action="store_true", help="Use HFD tool to download dataset.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.dataset_name:
|
||||
parser.error("dataset_name is required")
|
||||
|
||||
dataset_name = args.dataset_name
|
||||
|
||||
# 创建下载目录
|
||||
download_dir = Path.cwd() / dataset_name
|
||||
download_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.use_hfd:
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(name="setenvs", fn=setenvs, verbose=True),
|
||||
px.TaskSpec(
|
||||
name="download_hfd",
|
||||
cmd=["wget", "https://hf-mirror.com/hfd/hfd.sh"],
|
||||
depends_on=["setenvs"],
|
||||
verbose=True,
|
||||
),
|
||||
px.TaskSpec(
|
||||
name="chmod_hfd",
|
||||
cmd=["chmod", "a+x", "hfd.sh"],
|
||||
depends_on=["download_hfd"],
|
||||
verbose=True,
|
||||
),
|
||||
px.TaskSpec(
|
||||
name="run_hfd",
|
||||
cmd=["./hfd.sh", dataset_name, args.type],
|
||||
depends_on=["chmod_hfd"],
|
||||
verbose=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
else:
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(name="setenvs", fn=setenvs, verbose=True),
|
||||
px.TaskSpec(
|
||||
name="download",
|
||||
cmd=[
|
||||
"uvx",
|
||||
"hf",
|
||||
"download",
|
||||
"--repo-type",
|
||||
args.type,
|
||||
"--force-download",
|
||||
dataset_name,
|
||||
"--local-dir",
|
||||
str(Path.cwd() / dataset_name),
|
||||
],
|
||||
depends_on=["setenvs"],
|
||||
verbose=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
px.run(graph, strategy="thread", verbose=True)
|
||||
+35
-28
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -128,23 +129,6 @@ def check_ls_dyna_status() -> None:
|
||||
print(f"检查进程状态失败: {e}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
lscalc_default: px.TaskSpec = px.TaskSpec(
|
||||
"lscalc_default",
|
||||
fn=lambda: run_ls_dyna(DEFAULT_INPUT_FILE, DEFAULT_NCPU),
|
||||
)
|
||||
|
||||
lscalc_mpi: px.TaskSpec = px.TaskSpec(
|
||||
"lscalc_mpi",
|
||||
fn=lambda: run_ls_dyna_mpi(DEFAULT_INPUT_FILE, DEFAULT_NCPU),
|
||||
)
|
||||
|
||||
lscalc_status: px.TaskSpec = px.TaskSpec("lscalc_status", fn=check_ls_dyna_status)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -152,16 +136,39 @@ lscalc_status: px.TaskSpec = px.TaskSpec("lscalc_status", fn=check_ls_dyna_statu
|
||||
|
||||
def main() -> None:
|
||||
"""LS-DYNA 计算工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="LSCalc - LS-DYNA 计算工具",
|
||||
graphs={
|
||||
# 运行 LS-DYNA 计算
|
||||
"r": px.Graph.from_specs([lscalc_default]),
|
||||
# 运行 LS-DYNA MPI 计算
|
||||
"mpi": px.Graph.from_specs([lscalc_mpi]),
|
||||
# 检查进程状态
|
||||
"s": px.Graph.from_specs([lscalc_status]),
|
||||
},
|
||||
usage="lscalc <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 运行计算命令
|
||||
run_parser = subparsers.add_parser("run", help="运行 LS-DYNA 计算")
|
||||
run_parser.add_argument("input_file", help="输入文件路径")
|
||||
run_parser.add_argument("--ncpu", type=int, default=DEFAULT_NCPU, help="CPU 核心数")
|
||||
|
||||
# 运行 MPI 计算命令
|
||||
mpi_parser = subparsers.add_parser("mpi", help="运行 LS-DYNA MPI 计算")
|
||||
mpi_parser.add_argument("input_file", help="输入文件路径")
|
||||
mpi_parser.add_argument("--ncpu", type=int, default=DEFAULT_NCPU, help="CPU 核心数")
|
||||
|
||||
# 检查进程状态命令
|
||||
subparsers.add_parser("status", help="检查 LS-DYNA 进程状态")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "run":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("run_ls_dyna", fn=run_ls_dyna, args=(args.input_file,), kwargs={"ncpu": args.ncpu})]
|
||||
)
|
||||
elif args.command == "mpi":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("run_ls_dyna_mpi", fn=run_ls_dyna_mpi, args=(args.input_file,), kwargs={"ncpu": args.ncpu})]
|
||||
)
|
||||
elif args.command == "status":
|
||||
graph = px.Graph.from_specs([px.TaskSpec("check_ls_dyna_status", fn=check_ls_dyna_status)])
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
+92
-48
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
@@ -246,31 +247,6 @@ def clean_build_dir(build_dir: Path) -> None:
|
||||
print(f"目录不存在: {build_dir}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
# 源码打包
|
||||
pack_source_default: px.TaskSpec = px.TaskSpec("pack_source", fn=lambda: pack_source(Path(), Path(DEFAULT_BUILD_DIR)))
|
||||
|
||||
# 依赖打包
|
||||
pack_deps_default: px.TaskSpec = px.TaskSpec("pack_deps", fn=lambda: pack_dependencies(Path(DEFAULT_LIB_DIR), []))
|
||||
|
||||
# Wheel 打包
|
||||
pack_wheel_default: px.TaskSpec = px.TaskSpec("pack_wheel", fn=lambda: pack_wheel(Path(), Path(DEFAULT_DIST_DIR)))
|
||||
|
||||
# 嵌入式 Python 安装
|
||||
install_embed_default: px.TaskSpec = px.TaskSpec(
|
||||
"install_embed", fn=lambda: install_embed_python("3.10", Path("python"))
|
||||
)
|
||||
|
||||
# ZIP 打包
|
||||
create_zip_default: px.TaskSpec = px.TaskSpec("create_zip", fn=lambda: create_zip_package(Path(), Path("package.zip")))
|
||||
|
||||
# 清理构建目录
|
||||
clean_build: px.TaskSpec = px.TaskSpec("clean_build", fn=lambda: clean_build_dir(Path(DEFAULT_BUILD_DIR)))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -278,28 +254,96 @@ clean_build: px.TaskSpec = px.TaskSpec("clean_build", fn=lambda: clean_build_dir
|
||||
|
||||
def main() -> None:
|
||||
"""Python 打包工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="PackTool - Python 打包工具",
|
||||
graphs={
|
||||
# 源码打包
|
||||
"src": px.Graph.from_specs([pack_source_default]),
|
||||
# 依赖打包
|
||||
"deps": px.Graph.from_specs([pack_deps_default]),
|
||||
# Wheel 打包
|
||||
"wheel": px.Graph.from_specs([pack_wheel_default]),
|
||||
# 嵌入式 Python 安装
|
||||
"embed": px.Graph.from_specs([install_embed_default]),
|
||||
# ZIP 打包
|
||||
"zip": px.Graph.from_specs([create_zip_default]),
|
||||
# 清理构建目录
|
||||
"clean": px.Graph.from_specs([clean_build]),
|
||||
# 完整打包流程
|
||||
"all": px.Graph.from_specs([
|
||||
pack_source_default,
|
||||
pack_deps_default,
|
||||
pack_wheel_default,
|
||||
]),
|
||||
},
|
||||
usage="packtool <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 源码打包命令
|
||||
src_parser = subparsers.add_parser("src", help="打包项目源码")
|
||||
src_parser.add_argument("--project-dir", type=str, default=".", help="项目目录")
|
||||
src_parser.add_argument("--output-dir", type=str, default=DEFAULT_BUILD_DIR, help="输出目录")
|
||||
|
||||
# 依赖打包命令
|
||||
deps_parser = subparsers.add_parser("deps", help="打包项目依赖")
|
||||
deps_parser.add_argument("--lib-dir", type=str, default=DEFAULT_LIB_DIR, help="依赖库目录")
|
||||
deps_parser.add_argument("dependencies", nargs="*", help="依赖列表")
|
||||
|
||||
# Wheel 打包命令
|
||||
wheel_parser = subparsers.add_parser("wheel", help="打包项目为 wheel 文件")
|
||||
wheel_parser.add_argument("--project-dir", type=str, default=".", help="项目目录")
|
||||
wheel_parser.add_argument("--output-dir", type=str, default=DEFAULT_DIST_DIR, help="输出目录")
|
||||
|
||||
# 嵌入式 Python 安装命令
|
||||
embed_parser = subparsers.add_parser("embed", help="安装嵌入式 Python")
|
||||
embed_parser.add_argument("--version", type=str, default="3.10", help="Python 版本")
|
||||
embed_parser.add_argument("--output-dir", type=str, default="python", help="输出目录")
|
||||
|
||||
# ZIP 打包命令
|
||||
zip_parser = subparsers.add_parser("zip", help="创建 ZIP 打包文件")
|
||||
zip_parser.add_argument("--source-dir", type=str, default=".", help="源目录")
|
||||
zip_parser.add_argument("--output-file", type=str, default="package.zip", help="输出文件")
|
||||
|
||||
# 清理命令
|
||||
subparsers.add_parser("clean", help="清理构建目录")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "src":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pack_source",
|
||||
fn=pack_source,
|
||||
args=(Path(args.project_dir), Path(args.output_dir)),
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "deps":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pack_deps",
|
||||
fn=pack_dependencies,
|
||||
args=(Path(args.lib_dir), args.dependencies),
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "wheel":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pack_wheel",
|
||||
fn=pack_wheel,
|
||||
args=(Path(args.project_dir), Path(args.output_dir)),
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "embed":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"install_embed",
|
||||
fn=install_embed_python,
|
||||
args=(args.version, Path(args.output_dir)),
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "zip":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"create_zip",
|
||||
fn=create_zip_package,
|
||||
args=(Path(args.source_dir), Path(args.output_file)),
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "clean":
|
||||
graph = px.Graph.from_specs([px.TaskSpec("clean_build", fn=clean_build_dir, args=(Path(DEFAULT_BUILD_DIR),))])
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
+174
-108
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import pyflowx as px
|
||||
@@ -340,119 +341,184 @@ def pdf_repair(input_path: Path, output_path: Path) -> None:
|
||||
print(f"修复完成: {output_path}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
# PDF 合并
|
||||
pdf_merge_default: px.TaskSpec = px.TaskSpec("pdf_merge", fn=lambda: pdf_merge([], Path("merged.pdf")))
|
||||
|
||||
# PDF 拆分
|
||||
pdf_split_default: px.TaskSpec = px.TaskSpec("pdf_split", fn=lambda: pdf_split(Path("input.pdf"), Path("split")))
|
||||
|
||||
# PDF 压缩
|
||||
pdf_compress_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_compress", fn=lambda: pdf_compress(Path("input.pdf"), Path("compressed.pdf"))
|
||||
)
|
||||
|
||||
# PDF 加密
|
||||
pdf_encrypt_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_encrypt", fn=lambda: pdf_encrypt(Path("input.pdf"), Path("encrypted.pdf"), "password")
|
||||
)
|
||||
|
||||
# PDF 解密
|
||||
pdf_decrypt_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_decrypt", fn=lambda: pdf_decrypt(Path("input.pdf"), Path("decrypted.pdf"), "password")
|
||||
)
|
||||
|
||||
# PDF 提取文本
|
||||
pdf_extract_text_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_extract_text", fn=lambda: pdf_extract_text(Path("input.pdf"), Path("output.txt"))
|
||||
)
|
||||
|
||||
# PDF 提取图片
|
||||
pdf_extract_images_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_extract_images", fn=lambda: pdf_extract_images(Path("input.pdf"), Path("images"))
|
||||
)
|
||||
|
||||
# PDF 添加水印
|
||||
pdf_watermark_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_watermark", fn=lambda: pdf_add_watermark(Path("input.pdf"), Path("watermarked.pdf"))
|
||||
)
|
||||
|
||||
# PDF 旋转
|
||||
pdf_rotate_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_rotate", fn=lambda: pdf_rotate(Path("input.pdf"), Path("rotated.pdf"), 90)
|
||||
)
|
||||
|
||||
# PDF 裁剪
|
||||
pdf_crop_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_crop", fn=lambda: pdf_crop(Path("input.pdf"), Path("cropped.pdf"), (10, 10, 10, 10))
|
||||
)
|
||||
|
||||
# PDF 信息
|
||||
pdf_info_default: px.TaskSpec = px.TaskSpec("pdf_info", fn=lambda: pdf_info(Path("input.pdf")))
|
||||
|
||||
# PDF OCR
|
||||
pdf_ocr_default: px.TaskSpec = px.TaskSpec("pdf_ocr", fn=lambda: pdf_ocr(Path("input.pdf"), Path("ocr.pdf")))
|
||||
|
||||
# PDF 重排
|
||||
pdf_reorder_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_reorder", fn=lambda: pdf_reorder(Path("input.pdf"), Path("reordered.pdf"), [])
|
||||
)
|
||||
|
||||
# PDF 转图片
|
||||
pdf_to_images_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_to_images", fn=lambda: pdf_to_images(Path("input.pdf"), Path("images"))
|
||||
)
|
||||
|
||||
# PDF 修复
|
||||
pdf_repair_default: px.TaskSpec = px.TaskSpec(
|
||||
"pdf_repair", fn=lambda: pdf_repair(Path("input.pdf"), Path("repaired.pdf"))
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def main() -> None: # noqa: PLR0912
|
||||
"""PDF 工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="PDFTool - PDF 文件工具集",
|
||||
graphs={
|
||||
# 合并 PDF
|
||||
"m": px.Graph.from_specs([pdf_merge_default]),
|
||||
# 拆分 PDF
|
||||
"s": px.Graph.from_specs([pdf_split_default]),
|
||||
# 压缩 PDF
|
||||
"c": px.Graph.from_specs([pdf_compress_default]),
|
||||
# 加密 PDF
|
||||
"e": px.Graph.from_specs([pdf_encrypt_default]),
|
||||
# 解密 PDF
|
||||
"d": px.Graph.from_specs([pdf_decrypt_default]),
|
||||
# 提取文本
|
||||
"xt": px.Graph.from_specs([pdf_extract_text_default]),
|
||||
# 提取图片
|
||||
"xi": px.Graph.from_specs([pdf_extract_images_default]),
|
||||
# 添加水印
|
||||
"w": px.Graph.from_specs([pdf_watermark_default]),
|
||||
# 旋转 PDF
|
||||
"r": px.Graph.from_specs([pdf_rotate_default]),
|
||||
# 裁剪 PDF
|
||||
"crop": px.Graph.from_specs([pdf_crop_default]),
|
||||
# 显示信息
|
||||
"i": px.Graph.from_specs([pdf_info_default]),
|
||||
# OCR 识别
|
||||
"ocr": px.Graph.from_specs([pdf_ocr_default]),
|
||||
# 重排页面
|
||||
"order": px.Graph.from_specs([pdf_reorder_default]),
|
||||
# 转换图片
|
||||
"img": px.Graph.from_specs([pdf_to_images_default]),
|
||||
# 修复 PDF
|
||||
"repair": px.Graph.from_specs([pdf_repair_default]),
|
||||
},
|
||||
usage="pdftool <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 合并 PDF 命令
|
||||
merge_parser = subparsers.add_parser("m", help="合并 PDF 文件")
|
||||
merge_parser.add_argument("inputs", nargs="+", help="输入 PDF 文件路径")
|
||||
merge_parser.add_argument("--output", type=str, default="merged.pdf", help="输出文件路径")
|
||||
|
||||
# 拆分 PDF 命令
|
||||
split_parser = subparsers.add_parser("s", help="拆分 PDF 文件为单页")
|
||||
split_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
split_parser.add_argument("--output-dir", type=str, default="split", help="输出目录")
|
||||
|
||||
# 压缩 PDF 命令
|
||||
compress_parser = subparsers.add_parser("c", help="压缩 PDF 文件")
|
||||
compress_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
compress_parser.add_argument("--output", type=str, default="compressed.pdf", help="输出文件路径")
|
||||
|
||||
# 加密 PDF 命令
|
||||
encrypt_parser = subparsers.add_parser("e", help="加密 PDF 文件")
|
||||
encrypt_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
encrypt_parser.add_argument("--output", type=str, default="encrypted.pdf", help="输出文件路径")
|
||||
encrypt_parser.add_argument("--password", type=str, required=True, help="密码")
|
||||
|
||||
# 解密 PDF 命令
|
||||
decrypt_parser = subparsers.add_parser("d", help="解密 PDF 文件")
|
||||
decrypt_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
decrypt_parser.add_argument("--output", type=str, default="decrypted.pdf", help="输出文件路径")
|
||||
decrypt_parser.add_argument("--password", type=str, required=True, help="密码")
|
||||
|
||||
# 提取文本命令
|
||||
extract_text_parser = subparsers.add_parser("xt", help="提取 PDF 文本")
|
||||
extract_text_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
extract_text_parser.add_argument("--output", type=str, default="output.txt", help="输出文件路径")
|
||||
|
||||
# 提取图片命令
|
||||
extract_images_parser = subparsers.add_parser("xi", help="提取 PDF 图片")
|
||||
extract_images_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
extract_images_parser.add_argument("--output-dir", type=str, default="images", help="输出目录")
|
||||
|
||||
# 添加水印命令
|
||||
watermark_parser = subparsers.add_parser("w", help="添加 PDF 水印")
|
||||
watermark_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
watermark_parser.add_argument("--output", type=str, default="watermarked.pdf", help="输出文件路径")
|
||||
watermark_parser.add_argument("--text", type=str, default="CONFIDENTIAL", help="水印文本")
|
||||
|
||||
# 旋转 PDF 命令
|
||||
rotate_parser = subparsers.add_parser("r", help="旋转 PDF 页面")
|
||||
rotate_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
rotate_parser.add_argument("--output", type=str, default="rotated.pdf", help="输出文件路径")
|
||||
rotate_parser.add_argument("--rotation", type=int, default=90, help="旋转角度 (90, 180, 270)")
|
||||
|
||||
# 裁剪 PDF 命令
|
||||
crop_parser = subparsers.add_parser("crop", help="裁剪 PDF 页面")
|
||||
crop_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
crop_parser.add_argument("--output", type=str, default="cropped.pdf", help="输出文件路径")
|
||||
crop_parser.add_argument("--left", type=int, default=10, help="左边裁剪")
|
||||
crop_parser.add_argument("--top", type=int, default=10, help="顶部裁剪")
|
||||
crop_parser.add_argument("--right", type=int, default=10, help="右边裁剪")
|
||||
crop_parser.add_argument("--bottom", type=int, default=10, help="底部裁剪")
|
||||
|
||||
# 显示信息命令
|
||||
info_parser = subparsers.add_parser("i", help="显示 PDF 信息")
|
||||
info_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
|
||||
# OCR 识别命令
|
||||
ocr_parser = subparsers.add_parser("ocr", help="PDF OCR 识别")
|
||||
ocr_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
ocr_parser.add_argument("--output", type=str, default="ocr.pdf", help="输出文件路径")
|
||||
ocr_parser.add_argument("--lang", type=str, default="chi_sim+eng", help="OCR 语言")
|
||||
|
||||
# 转换图片命令
|
||||
to_images_parser = subparsers.add_parser("img", help="PDF 转图片")
|
||||
to_images_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
to_images_parser.add_argument("--output-dir", type=str, default="images", help="输出目录")
|
||||
to_images_parser.add_argument("--dpi", type=int, default=300, help="图片 DPI")
|
||||
|
||||
# 修复 PDF 命令
|
||||
repair_parser = subparsers.add_parser("repair", help="修复 PDF 文件")
|
||||
repair_parser.add_argument("input", help="输入 PDF 文件路径")
|
||||
repair_parser.add_argument("--output", type=str, default="repaired.pdf", help="输出文件路径")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "m":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_merge", fn=pdf_merge, args=([Path(p) for p in args.inputs], Path(args.output)))]
|
||||
)
|
||||
elif args.command == "s":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_split", fn=pdf_split, args=(Path(args.input), Path(args.output_dir)))]
|
||||
)
|
||||
elif args.command == "c":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_compress", fn=pdf_compress, args=(Path(args.input), Path(args.output)))]
|
||||
)
|
||||
elif args.command == "e":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_encrypt", fn=pdf_encrypt, args=(Path(args.input), Path(args.output), args.password))]
|
||||
)
|
||||
elif args.command == "d":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_decrypt", fn=pdf_decrypt, args=(Path(args.input), Path(args.output), args.password))]
|
||||
)
|
||||
elif args.command == "xt":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_extract_text", fn=pdf_extract_text, args=(Path(args.input), Path(args.output)))]
|
||||
)
|
||||
elif args.command == "xi":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_extract_images", fn=pdf_extract_images, args=(Path(args.input), Path(args.output_dir)))]
|
||||
)
|
||||
elif args.command == "w":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pdf_watermark",
|
||||
fn=pdf_add_watermark,
|
||||
args=(Path(args.input), Path(args.output)),
|
||||
kwargs={"text": args.text},
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "r":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pdf_rotate",
|
||||
fn=pdf_rotate,
|
||||
args=(Path(args.input), Path(args.output)),
|
||||
kwargs={"rotation": args.rotation},
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "crop":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pdf_crop",
|
||||
fn=pdf_crop,
|
||||
args=(Path(args.input), Path(args.output)),
|
||||
kwargs={"margins": (args.left, args.top, args.right, args.bottom)},
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "i":
|
||||
graph = px.Graph.from_specs([px.TaskSpec("pdf_info", fn=pdf_info, args=(Path(args.input),))])
|
||||
elif args.command == "ocr":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_ocr", fn=pdf_ocr, args=(Path(args.input), Path(args.output)), kwargs={"lang": args.lang})]
|
||||
)
|
||||
elif args.command == "img":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pdf_to_images",
|
||||
fn=pdf_to_images,
|
||||
args=(Path(args.input), Path(args.output_dir)),
|
||||
kwargs={"dpi": args.dpi},
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "repair":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pdf_repair", fn=pdf_repair, args=(Path(args.input), Path(args.output)))]
|
||||
)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
+72
-35
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
@@ -118,14 +119,6 @@ def pip_freeze() -> None:
|
||||
Path(REQUIREMENTS_FILE).write_text(result.stdout)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
pip_install: px.TaskSpec = px.TaskSpec("pip_install", cmd=["pip", "install", "."])
|
||||
pip_upgrade: px.TaskSpec = px.TaskSpec("pip_upgrade", cmd=["python", "-m", "pip", "install", "--upgrade", "pip"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -133,32 +126,76 @@ pip_upgrade: px.TaskSpec = px.TaskSpec("pip_upgrade", cmd=["python", "-m", "pip"
|
||||
|
||||
def main() -> None:
|
||||
"""pip 工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="PipTool - pip 包管理工具",
|
||||
graphs={
|
||||
# 安装包
|
||||
"i": px.Graph.from_specs([pip_install]),
|
||||
# 升级 pip
|
||||
"up": px.Graph.from_specs([pip_upgrade]),
|
||||
# 卸载包 (需要参数)
|
||||
"u": px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec("pip_uninstall", fn=lambda: pip_uninstall([])),
|
||||
]
|
||||
),
|
||||
# 下载包
|
||||
"d": px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec("pip_download", fn=lambda: pip_download([])),
|
||||
]
|
||||
),
|
||||
# 冻结依赖
|
||||
"f": px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec("pip_freeze", fn=pip_freeze),
|
||||
]
|
||||
),
|
||||
},
|
||||
usage="piptool <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 安装命令
|
||||
install_parser = subparsers.add_parser("i", help="安装包")
|
||||
install_parser.add_argument("packages", nargs="+", help="要安装的包名")
|
||||
|
||||
# 卸载命令
|
||||
uninstall_parser = subparsers.add_parser("u", help="卸载包")
|
||||
uninstall_parser.add_argument("packages", nargs="+", help="要卸载的包名 (支持通配符)")
|
||||
|
||||
# 重装命令
|
||||
reinstall_parser = subparsers.add_parser("r", help="重新安装包")
|
||||
reinstall_parser.add_argument("packages", nargs="+", help="要重装的包名")
|
||||
reinstall_parser.add_argument("--offline", action="store_true", help="使用离线模式")
|
||||
|
||||
# 下载命令
|
||||
download_parser = subparsers.add_parser("d", help="下载包")
|
||||
download_parser.add_argument("packages", nargs="+", help="要下载的包名")
|
||||
download_parser.add_argument("--offline", action="store_true", help="使用离线模式")
|
||||
|
||||
# 升级 pip 命令
|
||||
subparsers.add_parser("up", help="升级 pip")
|
||||
|
||||
# 冻结依赖命令
|
||||
subparsers.add_parser("f", help="冻结依赖到 requirements.txt")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "i":
|
||||
graph = px.Graph.from_specs([px.TaskSpec("pip_install", cmd=["pip", "install", *args.packages], verbose=True)])
|
||||
elif args.command == "u":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pip_uninstall", fn=pip_uninstall, args=(args.packages,), verbose=True)]
|
||||
)
|
||||
elif args.command == "r":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pip_reinstall",
|
||||
fn=pip_reinstall,
|
||||
args=(args.packages,),
|
||||
kwargs={"offline": args.offline},
|
||||
verbose=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "d":
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"pip_download",
|
||||
fn=pip_download,
|
||||
args=(args.packages,),
|
||||
kwargs={"offline": args.offline},
|
||||
verbose=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
elif args.command == "up":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("pip_upgrade", cmd=["python", "-m", "pip", "install", "--upgrade", "pip"], verbose=True)]
|
||||
)
|
||||
elif args.command == "f":
|
||||
graph = px.Graph.from_specs([px.TaskSpec("pip_freeze", fn=pip_freeze, verbose=True)])
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
@@ -20,15 +20,13 @@ def maturin_build_cmd() -> list[str]:
|
||||
"""
|
||||
command = ["maturin", "build", "-r"].copy()
|
||||
if Constants.IS_WINDOWS:
|
||||
command.extend(
|
||||
[
|
||||
"--target",
|
||||
"x86_64-win7-windows-msvc",
|
||||
"-Zbuild-std",
|
||||
"-i",
|
||||
"python3.8",
|
||||
]
|
||||
)
|
||||
command.extend([
|
||||
"--target",
|
||||
"x86_64-win7-windows-msvc",
|
||||
"-Zbuild-std",
|
||||
"-i",
|
||||
"python3.8",
|
||||
])
|
||||
return command
|
||||
|
||||
|
||||
@@ -113,7 +111,7 @@ def main():
|
||||
# 清理命令
|
||||
"c": px.Graph.from_specs([git_clean]),
|
||||
# 开发工具
|
||||
"bump": px.Graph.from_specs([git_clean, bump]),
|
||||
"bump": px.Graph.from_specs([git_clean, typecheck, ruff_lint, ruff_format, 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]),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -124,14 +125,6 @@ $bitmap.Dispose()
|
||||
print(f"截图已保存: {output_path}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
screenshot_full: px.TaskSpec = px.TaskSpec("screenshot_full", fn=take_screenshot_full)
|
||||
screenshot_area: px.TaskSpec = px.TaskSpec("screenshot_area", fn=take_screenshot_area)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -139,14 +132,32 @@ screenshot_area: px.TaskSpec = px.TaskSpec("screenshot_area", fn=take_screenshot
|
||||
|
||||
def main() -> None:
|
||||
"""截图工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Screenshot - 截图工具",
|
||||
graphs={
|
||||
# 全屏截图
|
||||
"f": px.Graph.from_specs([screenshot_full]),
|
||||
# 区域截图
|
||||
"a": px.Graph.from_specs([screenshot_area]),
|
||||
},
|
||||
usage="screenshot <command> [options]",
|
||||
)
|
||||
runner.run_cli()
|
||||
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||
|
||||
# 全屏截图命令
|
||||
full_parser = subparsers.add_parser("full", help="全屏截图")
|
||||
full_parser.add_argument("--filename", type=str, help="文件名")
|
||||
|
||||
# 区域截图命令
|
||||
area_parser = subparsers.add_parser("area", help="区域截图")
|
||||
area_parser.add_argument("--filename", type=str, help="文件名")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "full":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("screenshot_full", fn=take_screenshot_full, kwargs={"filename": args.filename})]
|
||||
)
|
||||
elif args.command == "area":
|
||||
graph = px.Graph.from_specs(
|
||||
[px.TaskSpec("screenshot_area", fn=take_screenshot_area, kwargs={"filename": args.filename})]
|
||||
)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -89,17 +90,6 @@ grep -qF '{pub_key.split()[1]}' authorized_keys 2>/dev/null || echo '{pub_key}'
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
# SSH 密钥部署需要参数,这里提供默认示例
|
||||
ssh_deploy_default: px.TaskSpec = px.TaskSpec(
|
||||
"ssh_deploy_default",
|
||||
fn=lambda: ssh_copy_id("localhost", "user", "password"),
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
@@ -107,12 +97,26 @@ ssh_deploy_default: px.TaskSpec = px.TaskSpec(
|
||||
|
||||
def main() -> None:
|
||||
"""SSH 密钥部署工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="SSHCopyID - SSH 密钥部署工具",
|
||||
graphs={
|
||||
# 部署 SSH 密钥 (需要参数)
|
||||
"d": px.Graph.from_specs([ssh_deploy_default]),
|
||||
},
|
||||
usage="sshcopyid <hostname> <username> <password> [--port PORT] [--keypath KEYPATH]",
|
||||
)
|
||||
runner.run_cli()
|
||||
parser.add_argument("hostname", type=str, help="远程服务器主机名或 IP 地址")
|
||||
parser.add_argument("username", type=str, help="远程服务器用户名")
|
||||
parser.add_argument("password", type=str, help="远程服务器密码")
|
||||
parser.add_argument("--port", type=int, default=22, help="SSH 端口 (默认: 22)")
|
||||
parser.add_argument("--keypath", type=str, default="~/.ssh/id_rsa.pub", help="公钥文件路径")
|
||||
parser.add_argument("--timeout", type=int, default=30, help="SSH 操作超时秒数 (默认: 30)")
|
||||
args = parser.parse_args()
|
||||
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(
|
||||
"ssh_deploy",
|
||||
fn=ssh_copy_id,
|
||||
args=(args.hostname, args.username, args.password),
|
||||
kwargs={"port": args.port, "keypath": args.keypath, "timeout": args.timeout},
|
||||
)
|
||||
]
|
||||
)
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
@@ -31,7 +31,10 @@ def main() -> None:
|
||||
else:
|
||||
cmd = ["pkill", "-f"]
|
||||
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec(f"kill_{proc_name}", cmd=[*cmd, f"{proc_name}*"], verbose=True) for proc_name in args.process_names
|
||||
])
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec(f"kill_{proc_name}", cmd=[*cmd, f"{proc_name}*"], verbose=True)
|
||||
for proc_name in args.process_names
|
||||
]
|
||||
)
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
+15
-113
@@ -5,16 +5,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.conditions import Constants
|
||||
|
||||
# ============================================================================
|
||||
# 辅助函数
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def which_command(command: str) -> Path | None:
|
||||
@@ -31,119 +26,26 @@ def which_command(command: str) -> Path | None:
|
||||
命令路径, 如果未找到则返回 None
|
||||
"""
|
||||
cmd_path = shutil.which(command)
|
||||
return Path(cmd_path) if cmd_path else None
|
||||
|
||||
|
||||
def which_all_commands(commands: list[str]) -> dict[str, Path | None]:
|
||||
"""查找多个命令路径.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
commands : list[str]
|
||||
命令名称列表
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Path | None]
|
||||
命令路径字典
|
||||
"""
|
||||
results: dict[str, Path | None] = {}
|
||||
for cmd in commands:
|
||||
results[cmd] = which_command(cmd)
|
||||
return results
|
||||
|
||||
|
||||
def where_command_windows(command: str) -> list[Path]:
|
||||
"""Windows 下使用 where 命令查找所有匹配路径.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command : str
|
||||
命令名称
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Path]
|
||||
匹配的路径列表
|
||||
"""
|
||||
if not Constants.IS_WINDOWS:
|
||||
return []
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["where", command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
paths = [Path(line.strip()) for line in result.stdout.strip().split("\n") if line.strip()]
|
||||
return paths
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
|
||||
def print_command_info(command: str) -> None:
|
||||
"""打印命令信息.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command : str
|
||||
命令名称
|
||||
"""
|
||||
cmd_path = which_command(command)
|
||||
if cmd_path:
|
||||
print(f"{command}: {cmd_path}")
|
||||
if Constants.IS_WINDOWS:
|
||||
all_paths = where_command_windows(command)
|
||||
if len(all_paths) > 1:
|
||||
print("所有匹配路径:")
|
||||
for path in all_paths:
|
||||
print(f" {path}")
|
||||
print(f"匹配路径: - {cmd_path}")
|
||||
return Path(cmd_path)
|
||||
else:
|
||||
print(f"{command}: 未找到")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TaskSpec 定义
|
||||
# ============================================================================
|
||||
|
||||
which_python: px.TaskSpec = px.TaskSpec("which_python", fn=lambda: print_command_info("python"))
|
||||
which_pip: px.TaskSpec = px.TaskSpec("which_pip", fn=lambda: print_command_info("pip"))
|
||||
which_node: px.TaskSpec = px.TaskSpec("which_node", fn=lambda: print_command_info("node"))
|
||||
which_npm: px.TaskSpec = px.TaskSpec("which_npm", fn=lambda: print_command_info("npm"))
|
||||
which_git: px.TaskSpec = px.TaskSpec("which_git", fn=lambda: print_command_info("git"))
|
||||
which_uv: px.TaskSpec = px.TaskSpec("which_uv", fn=lambda: print_command_info("uv"))
|
||||
which_rustc: px.TaskSpec = px.TaskSpec("which_rustc", fn=lambda: print_command_info("rustc"))
|
||||
which_cargo: px.TaskSpec = px.TaskSpec("which_cargo", fn=lambda: print_command_info("cargo"))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Runner
|
||||
# ============================================================================
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""命令查找工具主函数."""
|
||||
runner = px.CliRunner(
|
||||
strategy="thread",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Which - 命令查找工具",
|
||||
graphs={
|
||||
# 查找 python
|
||||
"py": px.Graph.from_specs([which_python]),
|
||||
# 查找 pip
|
||||
"pip": px.Graph.from_specs([which_pip]),
|
||||
# 查找 node
|
||||
"node": px.Graph.from_specs([which_node]),
|
||||
# 查找 npm
|
||||
"npm": px.Graph.from_specs([which_npm]),
|
||||
# 查找 git
|
||||
"git": px.Graph.from_specs([which_git]),
|
||||
# 查找 uv
|
||||
"uv": px.Graph.from_specs([which_uv]),
|
||||
# 查找 rustc
|
||||
"rustc": px.Graph.from_specs([which_rustc]),
|
||||
# 查找 cargo
|
||||
"cargo": px.Graph.from_specs([which_cargo]),
|
||||
},
|
||||
usage="which <command> [command ...]",
|
||||
)
|
||||
runner.run_cli()
|
||||
parser.add_argument(
|
||||
"commands",
|
||||
type=str,
|
||||
nargs="+",
|
||||
help="要查找的命令名称 (如: python pip node npm git uv rustc cargo)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
graph = px.Graph.from_specs([px.TaskSpec(f"which_{cmd}", fn=which_command, args=(cmd,)) for cmd in args.commands])
|
||||
px.run(graph, strategy="thread")
|
||||
|
||||
@@ -443,7 +443,7 @@ def run(
|
||||
*,
|
||||
max_workers: int | None = None,
|
||||
dry_run: bool = False,
|
||||
verbose: bool = True,
|
||||
verbose: bool = False,
|
||||
on_event: EventCallback | None = None,
|
||||
state: StateBackend | None = None,
|
||||
) -> RunReport:
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
"""Tests for cli.autofmt module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import autofmt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# format_with_ruff
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestFormatWithRuff:
|
||||
"""Test format_with_ruff function."""
|
||||
|
||||
def test_format_with_ruff(self, tmp_path: Path) -> None:
|
||||
"""Should format with ruff."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.format_with_ruff(tmp_path, fix=True)
|
||||
assert mock_run.called
|
||||
|
||||
def test_format_with_ruff_no_fix(self, tmp_path: Path) -> None:
|
||||
"""Should format with ruff without fix."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.format_with_ruff(tmp_path, fix=False)
|
||||
# Should not include --fix flag
|
||||
call_args = mock_run.call_args[0][0]
|
||||
assert "--fix" not in call_args
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# lint_with_ruff
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestLintWithRuff:
|
||||
"""Test lint_with_ruff function."""
|
||||
|
||||
def test_lint_with_ruff(self, tmp_path: Path) -> None:
|
||||
"""Should lint with ruff."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.lint_with_ruff(tmp_path, fix=True)
|
||||
assert mock_run.called
|
||||
|
||||
def test_lint_with_ruff_no_fix(self, tmp_path: Path) -> None:
|
||||
"""Should lint with ruff without fix."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.lint_with_ruff(tmp_path, fix=False)
|
||||
# Should not include --fix flag
|
||||
call_args = mock_run.call_args[0][0]
|
||||
assert "--fix" not in call_args
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# add_docstring
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestAddDocstring:
|
||||
"""Test add_docstring function."""
|
||||
|
||||
def test_add_docstring_to_file(self, tmp_path: Path) -> None:
|
||||
"""Should add docstring to file."""
|
||||
py_file = tmp_path / "test.py"
|
||||
py_file.write_text("def test():\n pass\n")
|
||||
|
||||
result = autofmt.add_docstring(py_file, '"""Test module."""')
|
||||
assert result is True
|
||||
|
||||
def test_add_docstring_skips_files_with_docstring(self, tmp_path: Path) -> None:
|
||||
"""Should skip files that already have docstring."""
|
||||
py_file = tmp_path / "test.py"
|
||||
py_file.write_text('"""Existing docstring."""\ndef test():\n pass\n')
|
||||
|
||||
result = autofmt.add_docstring(py_file, '"""New docstring."""')
|
||||
assert result is False
|
||||
|
||||
def test_add_docstring_empty_file(self, tmp_path: Path) -> None:
|
||||
"""Should handle empty file."""
|
||||
py_file = tmp_path / "test.py"
|
||||
py_file.write_text("")
|
||||
|
||||
result = autofmt.add_docstring(py_file, '"""Test module."""')
|
||||
# Should handle empty file
|
||||
assert result is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# generate_module_docstring
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestGenerateModuleDocstring:
|
||||
"""Test generate_module_docstring function."""
|
||||
|
||||
def test_generate_module_docstring_basic(self, tmp_path: Path) -> None:
|
||||
"""Should generate basic docstring."""
|
||||
py_file = tmp_path / "test.py"
|
||||
py_file.write_text("def test():\n pass\n")
|
||||
|
||||
result = autofmt.generate_module_docstring(py_file)
|
||||
# Should contain "Tests for" since stem contains "test"
|
||||
assert "Tests for" in result
|
||||
|
||||
def test_generate_module_docstring_with_package(self, tmp_path: Path) -> None:
|
||||
"""Should generate docstring for package."""
|
||||
py_file = tmp_path / "mypackage" / "test.py"
|
||||
py_file.parent.mkdir(parents=True)
|
||||
py_file.write_text("def test():\n pass\n")
|
||||
|
||||
result = autofmt.generate_module_docstring(py_file)
|
||||
assert "mypackage" in result
|
||||
|
||||
def test_generate_module_docstring_cli(self, tmp_path: Path) -> None:
|
||||
"""Should generate docstring for CLI module."""
|
||||
py_file = tmp_path / "cli.py"
|
||||
py_file.write_text("def test():\n pass\n")
|
||||
|
||||
result = autofmt.generate_module_docstring(py_file)
|
||||
assert "Command-line interface" in result
|
||||
|
||||
def test_generate_module_docstring_util(self, tmp_path: Path) -> None:
|
||||
"""Should generate docstring for utility module."""
|
||||
py_file = tmp_path / "utils.py"
|
||||
py_file.write_text("def test():\n pass\n")
|
||||
|
||||
result = autofmt.generate_module_docstring(py_file)
|
||||
assert "Utility functions" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# auto_add_docstrings
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestAutoAddDocstrings:
|
||||
"""Test auto_add_docstrings function."""
|
||||
|
||||
def test_auto_add_docstrings(self, tmp_path: Path) -> None:
|
||||
"""Should auto add docstrings."""
|
||||
py_file = tmp_path / "test.py"
|
||||
py_file.write_text("def test():\n pass\n")
|
||||
|
||||
with patch.object(autofmt, "add_docstring", return_value=True):
|
||||
count = autofmt.auto_add_docstrings(tmp_path)
|
||||
assert count >= 0
|
||||
|
||||
def test_auto_add_docstrings_skips_ignored(self, tmp_path: Path) -> None:
|
||||
"""Should skip ignored directories."""
|
||||
py_file = tmp_path / "__pycache__" / "test.py"
|
||||
py_file.parent.mkdir()
|
||||
py_file.write_text("def test():\n pass\n")
|
||||
|
||||
count = autofmt.auto_add_docstrings(tmp_path)
|
||||
# Should skip __pycache__
|
||||
assert count == 0
|
||||
|
||||
def test_auto_add_docstrings_no_files(self, tmp_path: Path) -> None:
|
||||
"""Should handle no Python files."""
|
||||
txt_file = tmp_path / "test.txt"
|
||||
txt_file.write_text("test content")
|
||||
|
||||
count = autofmt.auto_add_docstrings(tmp_path)
|
||||
assert count == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# sync_pyproject_config
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestSyncPyprojectConfig:
|
||||
"""Test sync_pyproject_config function."""
|
||||
|
||||
def test_sync_pyproject_config_creates_file(self, tmp_path: Path) -> None:
|
||||
"""Should sync pyproject.toml config."""
|
||||
main_toml = tmp_path / "pyproject.toml"
|
||||
main_toml.write_text("[tool.ruff]\n")
|
||||
sub_dir = tmp_path / "subproject"
|
||||
sub_dir.mkdir()
|
||||
sub_toml = sub_dir / "pyproject.toml"
|
||||
sub_toml.write_text("[tool.ruff]\n")
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.sync_pyproject_config(tmp_path)
|
||||
assert mock_run.called
|
||||
|
||||
def test_sync_pyproject_config_updates_file(self, tmp_path: Path) -> None:
|
||||
"""Should update existing pyproject.toml."""
|
||||
main_toml = tmp_path / "pyproject.toml"
|
||||
main_toml.write_text("[tool.ruff]\n")
|
||||
sub_dir = tmp_path / "subproject"
|
||||
sub_dir.mkdir()
|
||||
sub_toml = sub_dir / "pyproject.toml"
|
||||
sub_toml.write_text("[tool.ruff]\n")
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.sync_pyproject_config(tmp_path)
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# format_all
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestFormatAll:
|
||||
"""Test format_all function."""
|
||||
|
||||
def test_format_all_runs_ruff_format(self, tmp_path: Path) -> None:
|
||||
"""Should run ruff format."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.format_all(tmp_path)
|
||||
assert mock_run.called
|
||||
|
||||
def test_format_all_runs_ruff_check(self, tmp_path: Path) -> None:
|
||||
"""Should run ruff check."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
autofmt.format_all(tmp_path)
|
||||
# Should call ruff format and ruff check
|
||||
assert mock_run.call_count == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_fmt_default_target(self) -> None:
|
||||
"""main() should handle fmt command with default target."""
|
||||
with patch("sys.argv", ["autofmt", "fmt"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_fmt_custom_target(self) -> None:
|
||||
"""main() should handle fmt command with custom target."""
|
||||
with patch("sys.argv", ["autofmt", "fmt", "--target", "src"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_lint_default_target(self) -> None:
|
||||
"""main() should handle lint command with default target."""
|
||||
with patch("sys.argv", ["autofmt", "lint"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_lint_with_fix(self) -> None:
|
||||
"""main() should handle lint command with fix."""
|
||||
with patch("sys.argv", ["autofmt", "lint", "--fix"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_lint_custom_target(self) -> None:
|
||||
"""main() should handle lint command with custom target."""
|
||||
with patch("sys.argv", ["autofmt", "lint", "--target", "src"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_doc_default_root(self) -> None:
|
||||
"""main() should handle doc command with default root."""
|
||||
with patch("sys.argv", ["autofmt", "doc"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_doc_custom_root(self) -> None:
|
||||
"""main() should handle doc command with custom root."""
|
||||
with patch("sys.argv", ["autofmt", "doc", "--root-dir", "src"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_sync_default_root(self) -> None:
|
||||
"""main() should handle sync command with default root."""
|
||||
with patch("sys.argv", ["autofmt", "sync"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_sync_custom_root(self) -> None:
|
||||
"""main() should handle sync command with custom root."""
|
||||
with patch("sys.argv", ["autofmt", "sync", "--root-dir", "."]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["autofmt"]), patch.object(autofmt, "main"):
|
||||
# Just call main, it should show help and return
|
||||
autofmt.main()
|
||||
# main() should return without calling px.run
|
||||
assert True
|
||||
|
||||
def test_main_creates_task_specs_with_verbose(self) -> None:
|
||||
"""main() should create TaskSpecs with verbose=True."""
|
||||
with patch("sys.argv", ["autofmt", "fmt"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_uses_thread_strategy(self) -> None:
|
||||
"""main() should use thread strategy."""
|
||||
with patch("sys.argv", ["autofmt", "fmt"]), patch.object(px, "run") as mock_run:
|
||||
autofmt.main()
|
||||
# Check that strategy="thread" was used
|
||||
assert mock_run.called
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Tests for cli.bumpversion module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import bumpversion
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# bump_version
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestBumpVersion:
|
||||
"""Test bump_version function."""
|
||||
|
||||
def test_bump_version_patch(self) -> None:
|
||||
"""Should bump patch version."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
bumpversion.bump_version("patch")
|
||||
assert mock_run.called
|
||||
|
||||
def test_bump_version_minor(self) -> None:
|
||||
"""Should bump minor version."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
bumpversion.bump_version("minor")
|
||||
assert mock_run.called
|
||||
|
||||
def test_bump_version_major(self) -> None:
|
||||
"""Should bump major version."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
bumpversion.bump_version("major")
|
||||
assert mock_run.called
|
||||
|
||||
def test_bump_version_with_tag(self) -> None:
|
||||
"""Should bump version with tag."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="v1.0.0")
|
||||
bumpversion.bump_version("patch", tag=True)
|
||||
assert mock_run.called
|
||||
|
||||
def test_bump_version_with_commit(self) -> None:
|
||||
"""Should bump version with commit."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
bumpversion.bump_version("patch", commit=True)
|
||||
assert mock_run.called
|
||||
|
||||
def test_bump_version_file_not_found(self) -> None:
|
||||
"""Should handle FileNotFoundError."""
|
||||
with patch("subprocess.run", side_effect=FileNotFoundError), pytest.raises(FileNotFoundError):
|
||||
bumpversion.bump_version("patch")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# bump_version_alpha
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestBumpVersionAlpha:
|
||||
"""Test bump_version_alpha function."""
|
||||
|
||||
def test_bump_version_alpha_patch(self) -> None:
|
||||
"""Should bump alpha patch version."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
bumpversion.bump_version_alpha("patch")
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# TaskSpec definitions
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTaskSpecDefinitions:
|
||||
"""Test that all TaskSpec definitions are valid."""
|
||||
|
||||
def test_bump_patch_spec(self) -> None:
|
||||
"""bump_patch spec should be properly defined."""
|
||||
assert bumpversion.bump_patch.name == "bump_patch"
|
||||
assert bumpversion.bump_patch.fn is not None
|
||||
|
||||
def test_bump_minor_spec(self) -> None:
|
||||
"""bump_minor spec should be properly defined."""
|
||||
assert bumpversion.bump_minor.name == "bump_minor"
|
||||
assert bumpversion.bump_minor.fn is not None
|
||||
|
||||
def test_bump_major_spec(self) -> None:
|
||||
"""bump_major spec should be properly defined."""
|
||||
assert bumpversion.bump_major.name == "bump_major"
|
||||
assert bumpversion.bump_major.fn is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_calls_run_cli(self) -> None:
|
||||
"""main() should create a CliRunner and call run_cli()."""
|
||||
with patch.object(px.CliRunner, "run_cli") as mock_run_cli:
|
||||
bumpversion.main()
|
||||
assert mock_run_cli.called
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Tests for cli.clearscreen module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import clearscreen
|
||||
from pyflowx.conditions import Constants
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# clear_screen
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestClearScreen:
|
||||
"""Test clear_screen function."""
|
||||
|
||||
def test_clear_screen_windows(self) -> None:
|
||||
"""Should clear screen on Windows."""
|
||||
if Constants.IS_WINDOWS:
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
clearscreen.clear_screen()
|
||||
assert mock_run.called
|
||||
|
||||
def test_clear_screen_linux(self) -> None:
|
||||
"""Should clear screen on Linux."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
clearscreen.clear_screen()
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_creates_graph_and_runs(self) -> None:
|
||||
"""main() should create a Graph and run it."""
|
||||
with patch.object(px, "run") as mock_run:
|
||||
clearscreen.main()
|
||||
assert mock_run.called
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Tests for cli.envpy module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import envpy
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# set_pip_mirror
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestSetPipMirror:
|
||||
"""Test set_pip_mirror function."""
|
||||
|
||||
def test_set_pip_mirror_tsinghua(self, tmp_path: Path) -> None:
|
||||
"""Should set tsinghua mirror."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envpy.set_pip_mirror("tsinghua")
|
||||
# Check pip config
|
||||
pip_config = tmp_path / "pip" / "pip.ini"
|
||||
if envpy.Constants.IS_WINDOWS:
|
||||
assert pip_config.exists() or (tmp_path / "pip" / "pip.conf").exists()
|
||||
|
||||
def test_set_pip_mirror_aliyun(self, tmp_path: Path) -> None:
|
||||
"""Should set aliyun mirror."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envpy.set_pip_mirror("aliyun")
|
||||
# Check pip config
|
||||
pip_dir = tmp_path / "pip"
|
||||
assert pip_dir.exists()
|
||||
|
||||
def test_set_pip_mirror_with_token(self, tmp_path: Path) -> None:
|
||||
"""Should set mirror with token."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envpy.set_pip_mirror("tsinghua", token="test_token")
|
||||
# Check that token is set
|
||||
|
||||
def test_set_pip_mirror_creates_pip_dir(self, tmp_path: Path) -> None:
|
||||
"""Should create pip directory if it doesn't exist."""
|
||||
pip_dir = tmp_path / "pip"
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envpy.set_pip_mirror("tsinghua")
|
||||
assert pip_dir.exists()
|
||||
assert pip_dir.is_dir()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_mirror_tsinghua(self) -> None:
|
||||
"""main() should handle mirror tsinghua command."""
|
||||
with patch("sys.argv", ["envpy", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envpy, "set_pip_mirror"
|
||||
):
|
||||
envpy.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_mirror_aliyun(self) -> None:
|
||||
"""main() should handle mirror aliyun command."""
|
||||
with patch("sys.argv", ["envpy", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envpy, "set_pip_mirror"
|
||||
):
|
||||
envpy.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_mirror_with_token(self) -> None:
|
||||
"""main() should handle mirror with token."""
|
||||
with patch("sys.argv", ["envpy", "mirror", "tsinghua", "--token", "test_token"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run, patch.object(envpy, "set_pip_mirror"):
|
||||
envpy.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help and return."""
|
||||
with patch("sys.argv", ["envpy"]):
|
||||
envpy.main()
|
||||
# Should print help and return
|
||||
|
||||
def test_main_invalid_mirror_shows_error(self) -> None:
|
||||
"""main() with invalid mirror should show error."""
|
||||
with patch("sys.argv", ["envpy", "mirror", "invalid"]), pytest.raises(SystemExit) as exc_info:
|
||||
envpy.main()
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
def test_main_creates_task_spec_with_correct_name(self) -> None:
|
||||
"""main() should create TaskSpec with correct name."""
|
||||
with patch("sys.argv", ["envpy", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envpy, "set_pip_mirror"
|
||||
):
|
||||
envpy.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
task_names = list(graph.all_specs().keys())
|
||||
assert "set_pip_mirror" in task_names
|
||||
|
||||
def test_main_uses_thread_strategy(self) -> None:
|
||||
"""main() should use thread strategy."""
|
||||
with patch("sys.argv", ["envpy", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envpy, "set_pip_mirror"
|
||||
):
|
||||
envpy.main()
|
||||
assert mock_run.call_args[1]["strategy"] == "thread"
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Tests for cli.envrs module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import envrs
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# set_rust_mirror
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestSetRustMirror:
|
||||
"""Test set_rust_mirror function."""
|
||||
|
||||
def test_set_rust_mirror_aliyun(self, tmp_path: Path) -> None:
|
||||
"""Should set aliyun mirror."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envrs.set_rust_mirror("aliyun")
|
||||
# Check environment variables
|
||||
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.aliyun.com/rustup"
|
||||
assert os.environ.get("RUSTUP_UPDATE_ROOT") == "https://mirrors.aliyun.com/rustup/rustup"
|
||||
# Check cargo config
|
||||
cargo_config = tmp_path / ".cargo" / "config.toml"
|
||||
assert cargo_config.exists()
|
||||
content = cargo_config.read_text()
|
||||
assert "aliyun" in content
|
||||
|
||||
def test_set_rust_mirror_ustc(self, tmp_path: Path) -> None:
|
||||
"""Should set ustc mirror."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envrs.set_rust_mirror("ustc")
|
||||
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.ustc.edu.cn/rust-static"
|
||||
assert os.environ.get("RUSTUP_UPDATE_ROOT") == "https://mirrors.ustc.edu.cn/rust-static/rustup"
|
||||
|
||||
def test_set_rust_mirror_tsinghua(self, tmp_path: Path) -> None:
|
||||
"""Should set tsinghua mirror."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envrs.set_rust_mirror("tsinghua")
|
||||
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.tuna.tsinghua.edu.cn/rustup"
|
||||
assert os.environ.get("RUSTUP_UPDATE_ROOT") == "https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup"
|
||||
|
||||
def test_set_rust_mirror_unknown_uses_default(self, tmp_path: Path) -> None:
|
||||
"""Should use default mirror for unknown mirror name."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envrs.set_rust_mirror("unknown")
|
||||
# Should use default mirror (tsinghua)
|
||||
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.tuna.tsinghua.edu.cn/rustup"
|
||||
|
||||
def test_set_rust_mirror_creates_cargo_dir(self, tmp_path: Path) -> None:
|
||||
"""Should create .cargo directory if it doesn't exist."""
|
||||
cargo_dir = tmp_path / ".cargo"
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envrs.set_rust_mirror("aliyun")
|
||||
assert cargo_dir.exists()
|
||||
assert cargo_dir.is_dir()
|
||||
|
||||
def test_set_rust_mirror_prints_message(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Should print mirror name."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
envrs.set_rust_mirror("aliyun")
|
||||
captured = capsys.readouterr()
|
||||
assert "已设置 Rust 镜像源: aliyun" in captured.out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# install_rust
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestInstallRust:
|
||||
"""Test install_rust function."""
|
||||
|
||||
def test_install_rust_stable(self) -> None:
|
||||
"""Should install stable Rust."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
envrs.install_rust("stable")
|
||||
mock_run.assert_called_once_with(["rustup", "toolchain", "install", "stable"], check=True)
|
||||
|
||||
def test_install_rust_nightly(self) -> None:
|
||||
"""Should install nightly Rust."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
envrs.install_rust("nightly")
|
||||
mock_run.assert_called_once_with(["rustup", "toolchain", "install", "nightly"], check=True)
|
||||
|
||||
def test_install_rust_beta(self) -> None:
|
||||
"""Should install beta Rust."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
envrs.install_rust("beta")
|
||||
mock_run.assert_called_once_with(["rustup", "toolchain", "install", "beta"], check=True)
|
||||
|
||||
def test_install_rust_file_not_found(self) -> None:
|
||||
"""Should raise FileNotFoundError when rustup not found."""
|
||||
with patch("subprocess.run", side_effect=FileNotFoundError), pytest.raises(FileNotFoundError):
|
||||
envrs.install_rust("stable")
|
||||
|
||||
def test_install_rust_prints_message(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Should print installation message."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
envrs.install_rust("stable")
|
||||
captured = capsys.readouterr()
|
||||
assert "已安装 Rust stable" in captured.out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_mirror_aliyun(self) -> None:
|
||||
"""main() should handle mirror aliyun command."""
|
||||
with patch("sys.argv", ["envrs", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envrs, "set_rust_mirror"
|
||||
):
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_mirror_ustc(self) -> None:
|
||||
"""main() should handle mirror ustc command."""
|
||||
with patch("sys.argv", ["envrs", "mirror", "ustc"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envrs, "set_rust_mirror"
|
||||
):
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_mirror_tsinghua(self) -> None:
|
||||
"""main() should handle mirror tsinghua command."""
|
||||
with patch("sys.argv", ["envrs", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envrs, "set_rust_mirror"
|
||||
):
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_mirror_default(self) -> None:
|
||||
"""main() should use default mirror when not specified."""
|
||||
with patch("sys.argv", ["envrs", "mirror"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envrs, "set_rust_mirror"
|
||||
):
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_install_stable(self) -> None:
|
||||
"""main() should handle install stable command."""
|
||||
with patch("sys.argv", ["envrs", "install", "stable"]), patch.object(px, "run") as mock_run:
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_install_nightly(self) -> None:
|
||||
"""main() should handle install nightly command."""
|
||||
with patch("sys.argv", ["envrs", "install", "nightly"]), patch.object(px, "run") as mock_run:
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_install_beta(self) -> None:
|
||||
"""main() should handle install beta command."""
|
||||
with patch("sys.argv", ["envrs", "install", "beta"]), patch.object(px, "run") as mock_run:
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_install_default(self) -> None:
|
||||
"""main() should use default version when not specified."""
|
||||
with patch("sys.argv", ["envrs", "install"]), patch.object(px, "run") as mock_run:
|
||||
envrs.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help and return."""
|
||||
with patch("sys.argv", ["envrs"]):
|
||||
envrs.main()
|
||||
# Should print help and return
|
||||
|
||||
def test_main_invalid_version_shows_error(self) -> None:
|
||||
"""main() with invalid version should show error."""
|
||||
with patch("sys.argv", ["envrs", "install", "invalid"]), pytest.raises(SystemExit) as exc_info:
|
||||
envrs.main()
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
def test_main_invalid_mirror_shows_error(self) -> None:
|
||||
"""main() with invalid mirror should show error."""
|
||||
with patch("sys.argv", ["envrs", "mirror", "invalid"]), pytest.raises(SystemExit) as exc_info:
|
||||
envrs.main()
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
def test_main_creates_task_spec_with_verbose(self) -> None:
|
||||
"""main() should create TaskSpec with verbose=True."""
|
||||
with patch("sys.argv", ["envrs", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envrs, "set_rust_mirror"
|
||||
):
|
||||
envrs.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
specs = graph.all_specs()
|
||||
for spec in specs.values():
|
||||
assert spec.verbose is True
|
||||
|
||||
def test_main_uses_thread_strategy(self) -> None:
|
||||
"""main() should use thread strategy."""
|
||||
with patch("sys.argv", ["envrs", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
|
||||
envrs, "set_rust_mirror"
|
||||
):
|
||||
envrs.main()
|
||||
assert mock_run.call_args[1]["strategy"] == "thread"
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Tests for cli.filedate module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import filedate
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# get_file_timestamp
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestGetFileTimestamp:
|
||||
"""Test get_file_timestamp function."""
|
||||
|
||||
def test_get_file_timestamp(self, tmp_path: Path) -> None:
|
||||
"""Should get file timestamp."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
timestamp = filedate.get_file_timestamp(test_file)
|
||||
assert len(timestamp) == 8 # YYYYMMDD format
|
||||
assert timestamp.isdigit()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# remove_date_prefix
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestRemoveDatePrefix:
|
||||
"""Test remove_date_prefix function."""
|
||||
|
||||
def test_remove_date_prefix_with_date(self, tmp_path: Path) -> None:
|
||||
"""Should remove date prefix from filename."""
|
||||
test_file = tmp_path / "20240101_test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
new_path = filedate.remove_date_prefix(test_file)
|
||||
assert new_path.name == "test.txt"
|
||||
|
||||
def test_remove_date_prefix_without_date(self, tmp_path: Path) -> None:
|
||||
"""Should not change filename without date prefix."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
new_path = filedate.remove_date_prefix(test_file)
|
||||
assert new_path == test_file
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# add_date_prefix
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestAddDatePrefix:
|
||||
"""Test add_date_prefix function."""
|
||||
|
||||
def test_add_date_prefix(self, tmp_path: Path) -> None:
|
||||
"""Should add date prefix to filename."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
new_path = filedate.add_date_prefix(test_file)
|
||||
assert new_path.name.startswith("20") # Starts with year
|
||||
assert "_test.txt" in new_path.name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# process_file_date
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestProcessFileDate:
|
||||
"""Test process_file_date function."""
|
||||
|
||||
def test_process_file_date_add(self, tmp_path: Path) -> None:
|
||||
"""Should add date prefix."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
filedate.process_file_date(test_file, clear=False)
|
||||
# File should be renamed with date prefix
|
||||
|
||||
def test_process_file_date_clear(self, tmp_path: Path) -> None:
|
||||
"""Should clear date prefix."""
|
||||
test_file = tmp_path / "20240101_test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
filedate.process_file_date(test_file, clear=True)
|
||||
# File should be renamed without date prefix
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# process_files_date
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestProcessFilesDate:
|
||||
"""Test process_files_date function."""
|
||||
|
||||
def test_process_files_date_batch(self, tmp_path: Path) -> None:
|
||||
"""Should process multiple files."""
|
||||
files = []
|
||||
for i in range(3):
|
||||
test_file = tmp_path / f"test{i}.txt"
|
||||
test_file.write_text(f"content{i}")
|
||||
files.append(test_file)
|
||||
|
||||
filedate.process_files_date(files, clear=False)
|
||||
# All files should be processed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_add_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle add command."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
with patch("sys.argv", ["filedate", "add", str(test_file)]), patch.object(px, "run") as mock_run:
|
||||
filedate.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_clear_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle clear command."""
|
||||
test_file = tmp_path / "20240101_test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
with patch("sys.argv", ["filedate", "clear", str(test_file)]), patch.object(px, "run") as mock_run:
|
||||
filedate.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["filedate"]):
|
||||
filedate.main()
|
||||
# Should print help and return
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Tests for cli.filelevel module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import filelevel
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# remove_marks
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestRemoveMarks:
|
||||
"""Test remove_marks function."""
|
||||
|
||||
def test_remove_marks_single_mark(self) -> None:
|
||||
"""Should remove single mark."""
|
||||
stem = "filename(PUB)"
|
||||
result = filelevel.remove_marks(stem, ["PUB"])
|
||||
assert result == "filename"
|
||||
|
||||
def test_remove_marks_multiple_marks(self) -> None:
|
||||
"""Should remove multiple marks."""
|
||||
stem = "filename(PUB)(NOR)"
|
||||
result = filelevel.remove_marks(stem, ["PUB", "NOR"])
|
||||
assert result == "filename"
|
||||
|
||||
def test_remove_marks_no_marks(self) -> None:
|
||||
"""Should not change stem without marks."""
|
||||
stem = "filename"
|
||||
result = filelevel.remove_marks(stem, ["PUB"])
|
||||
assert result == "filename"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# process_file_level
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestProcessFileLevel:
|
||||
"""Test process_file_level function."""
|
||||
|
||||
def test_process_file_level_set_pub(self, tmp_path: Path) -> None:
|
||||
"""Should set PUB level."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
filelevel.process_file_level(test_file, level=1)
|
||||
# File should be renamed with PUB level
|
||||
|
||||
def test_process_file_level_set_int(self, tmp_path: Path) -> None:
|
||||
"""Should set INT level."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
filelevel.process_file_level(test_file, level=2)
|
||||
# File should be renamed with INT level
|
||||
|
||||
def test_process_file_level_clear(self, tmp_path: Path) -> None:
|
||||
"""Should clear level."""
|
||||
test_file = tmp_path / "test(PUB).txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
filelevel.process_file_level(test_file, level=0)
|
||||
# File should be renamed without level
|
||||
|
||||
def test_process_file_level_invalid_level(self, tmp_path: Path) -> None:
|
||||
"""Should handle invalid level."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
filelevel.process_file_level(test_file, level=5)
|
||||
# Should print error message
|
||||
|
||||
def test_process_file_level_nonexistent_file(self, tmp_path: Path) -> None:
|
||||
"""Should handle nonexistent file."""
|
||||
test_file = tmp_path / "nonexistent.txt"
|
||||
|
||||
filelevel.process_file_level(test_file, level=1)
|
||||
# Should print error message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# process_files_level
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestProcessFilesLevel:
|
||||
"""Test process_files_level function."""
|
||||
|
||||
def test_process_files_level_batch(self, tmp_path: Path) -> None:
|
||||
"""Should process multiple files."""
|
||||
files = []
|
||||
for i in range(3):
|
||||
test_file = tmp_path / f"test{i}.txt"
|
||||
test_file.write_text(f"content{i}")
|
||||
files.append(test_file)
|
||||
|
||||
filelevel.process_files_level(files, level=1)
|
||||
# All files should be processed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_set_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle set command."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
with patch("sys.argv", ["filelevel", "set", str(test_file), "--level", "1"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
filelevel.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_set_command_level_2(self, tmp_path: Path) -> None:
|
||||
"""main() should handle set command with level 2."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
with patch("sys.argv", ["filelevel", "set", str(test_file), "--level", "2"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
filelevel.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["filelevel"]):
|
||||
filelevel.main()
|
||||
# Should print help and return
|
||||
@@ -0,0 +1,173 @@
|
||||
"""Tests for cli.folderback module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import folderback
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# remove_dump
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestRemoveDump:
|
||||
"""Test remove_dump function."""
|
||||
|
||||
def test_remove_dump_no_files(self, tmp_path: Path) -> None:
|
||||
"""Should handle no zip files."""
|
||||
src = tmp_path / "source"
|
||||
src.mkdir()
|
||||
dst = tmp_path / "backup"
|
||||
dst.mkdir()
|
||||
|
||||
folderback.remove_dump(src, dst, 5)
|
||||
# Should not raise error
|
||||
|
||||
def test_remove_dump_within_limit(self, tmp_path: Path) -> None:
|
||||
"""Should not remove files within limit."""
|
||||
src = tmp_path / "source"
|
||||
src.mkdir()
|
||||
dst = tmp_path / "backup"
|
||||
dst.mkdir()
|
||||
|
||||
# Create some zip files
|
||||
for i in range(3):
|
||||
zip_file = dst / f"source_20240101_12000{i}.zip"
|
||||
zip_file.write_bytes(b"ZIP content")
|
||||
|
||||
folderback.remove_dump(src, dst, 5)
|
||||
# All files should remain
|
||||
assert len(list(dst.glob("*.zip"))) == 3
|
||||
|
||||
def test_remove_dump_exceeds_limit(self, tmp_path: Path) -> None:
|
||||
"""Should remove oldest files when exceeds limit."""
|
||||
src = tmp_path / "source"
|
||||
src.mkdir()
|
||||
dst = tmp_path / "backup"
|
||||
dst.mkdir()
|
||||
|
||||
# Create more zip files than limit
|
||||
for i in range(7):
|
||||
zip_file = dst / f"source_20240101_12000{i}.zip"
|
||||
zip_file.write_bytes(b"ZIP content")
|
||||
|
||||
folderback.remove_dump(src, dst, 5)
|
||||
# Should have only 5 files
|
||||
assert len(list(dst.glob("*.zip"))) == 5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# zip_target
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestZipTarget:
|
||||
"""Test zip_target function."""
|
||||
|
||||
def test_zip_target_creates_zip(self, tmp_path: Path) -> None:
|
||||
"""Should create zip file."""
|
||||
src = tmp_path / "source"
|
||||
src.mkdir()
|
||||
(src / "test.txt").write_text("test content")
|
||||
dst = tmp_path / "backup"
|
||||
dst.mkdir()
|
||||
|
||||
with patch("time.strftime", return_value="_20240101_120000"):
|
||||
folderback.zip_target(src, dst, 5)
|
||||
|
||||
# Should create zip file
|
||||
zip_files = list(dst.glob("*.zip"))
|
||||
assert len(zip_files) == 1
|
||||
|
||||
def test_zip_target_with_subdirectories(self, tmp_path: Path) -> None:
|
||||
"""Should zip files in subdirectories."""
|
||||
src = tmp_path / "source"
|
||||
src.mkdir()
|
||||
subdir = src / "subdir"
|
||||
subdir.mkdir()
|
||||
(src / "test.txt").write_text("test content")
|
||||
(subdir / "nested.txt").write_text("nested content")
|
||||
dst = tmp_path / "backup"
|
||||
dst.mkdir()
|
||||
|
||||
with patch("time.strftime", return_value="_20240101_120000"):
|
||||
folderback.zip_target(src, dst, 5)
|
||||
|
||||
# Should create zip file
|
||||
zip_files = list(dst.glob("*.zip"))
|
||||
assert len(zip_files) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# backup_folder
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestBackupFolder:
|
||||
"""Test backup_folder function."""
|
||||
|
||||
def test_backup_folder_with_source_and_backup(self, tmp_path: Path) -> None:
|
||||
"""Should backup folder with source and backup paths."""
|
||||
source_dir = tmp_path / "source"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "test.txt").write_text("test content")
|
||||
backup_dir = tmp_path / "backup"
|
||||
|
||||
with patch.object(folderback, "zip_target") as mock_zip:
|
||||
folderback.backup_folder(str(source_dir), str(backup_dir), 5)
|
||||
assert mock_zip.called
|
||||
|
||||
def test_backup_folder_with_max_backups(self, tmp_path: Path) -> None:
|
||||
"""Should backup folder with max backups."""
|
||||
source_dir = tmp_path / "source"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "test.txt").write_text("test content")
|
||||
backup_dir = tmp_path / "backup"
|
||||
|
||||
with patch.object(folderback, "zip_target") as mock_zip:
|
||||
folderback.backup_folder(str(source_dir), str(backup_dir), 10)
|
||||
assert mock_zip.called
|
||||
|
||||
def test_backup_folder_source_not_exists(self, tmp_path: Path) -> None:
|
||||
"""Should handle non-existent source folder."""
|
||||
source_dir = tmp_path / "nonexistent"
|
||||
backup_dir = tmp_path / "backup"
|
||||
backup_dir.mkdir()
|
||||
|
||||
folderback.backup_folder(str(source_dir), str(backup_dir), 5)
|
||||
# Should print error message and return
|
||||
|
||||
def test_backup_folder_creates_dst(self, tmp_path: Path) -> None:
|
||||
"""Should create destination directory."""
|
||||
source_dir = tmp_path / "source"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "test.txt").write_text("test content")
|
||||
backup_dir = tmp_path / "backup"
|
||||
|
||||
with patch.object(folderback, "zip_target") as mock_zip:
|
||||
folderback.backup_folder(str(source_dir), str(backup_dir), 5)
|
||||
assert backup_dir.exists()
|
||||
assert mock_zip.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# TaskSpec definitions
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTaskSpecDefinitions:
|
||||
"""Test that all TaskSpec definitions are valid."""
|
||||
|
||||
def test_folderback_default_spec(self) -> None:
|
||||
"""folderback_default spec should be properly defined."""
|
||||
assert folderback.folderback_default.name == "folderback_default"
|
||||
assert folderback.folderback_default.fn is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_calls_run_cli(self) -> None:
|
||||
"""main() should create a CliRunner and call run_cli()."""
|
||||
with patch.object(px.CliRunner, "run_cli") as mock_run_cli:
|
||||
folderback.main()
|
||||
assert mock_run_cli.called
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Tests for cli.folderzip module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import folderzip
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# archive_folder
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestArchiveFolder:
|
||||
"""Test archive_folder function."""
|
||||
|
||||
def test_archive_folder(self, tmp_path: Path) -> None:
|
||||
"""Should archive a folder."""
|
||||
folder = tmp_path / "test_folder"
|
||||
folder.mkdir()
|
||||
(folder / "test.txt").write_text("test content")
|
||||
|
||||
with patch("shutil.make_archive") as mock_archive:
|
||||
folderzip.archive_folder(folder)
|
||||
assert mock_archive.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# zip_folders
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestZipFolders:
|
||||
"""Test zip_folders function."""
|
||||
|
||||
def test_zip_folders_with_cwd(self, tmp_path: Path) -> None:
|
||||
"""Should zip folders in cwd."""
|
||||
# Create some folders
|
||||
(tmp_path / "folder1").mkdir()
|
||||
(tmp_path / "folder2").mkdir()
|
||||
(tmp_path / ".git").mkdir() # Should be ignored
|
||||
|
||||
with patch.object(folderzip, "archive_folder") as mock_archive:
|
||||
folderzip.zip_folders(str(tmp_path))
|
||||
# Should archive folder1 and folder2, but not .git
|
||||
assert mock_archive.call_count == 2
|
||||
|
||||
def test_zip_folders_nonexistent_cwd(self, tmp_path: Path) -> None:
|
||||
"""Should handle nonexistent cwd."""
|
||||
folderzip.zip_folders(str(tmp_path / "nonexistent"))
|
||||
# Should print error message and return
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# TaskSpec definitions
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTaskSpecDefinitions:
|
||||
"""Test that all TaskSpec definitions are valid."""
|
||||
|
||||
def test_folderzip_default_spec(self) -> None:
|
||||
"""folderzip_default spec should be properly defined."""
|
||||
assert folderzip.folderzip_default.name == "folderzip_default"
|
||||
assert folderzip.folderzip_default.fn is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_calls_run_cli(self) -> None:
|
||||
"""main() should create a CliRunner and call run_cli()."""
|
||||
with patch.object(px.CliRunner, "run_cli") as mock_run_cli:
|
||||
folderzip.main()
|
||||
assert mock_run_cli.called
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Tests for cli.gittool module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import gittool
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# not_has_git_repo
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestNotHasGitRepo:
|
||||
"""Test not_has_git_repo function."""
|
||||
|
||||
def test_not_has_git_repo_true(self, tmp_path: Path) -> None:
|
||||
"""Should return True when no .git directory."""
|
||||
with patch.object(Path, "cwd", return_value=tmp_path):
|
||||
result = gittool.not_has_git_repo()
|
||||
assert result is True
|
||||
|
||||
def test_not_has_git_repo_false(self, tmp_path: Path) -> None:
|
||||
"""Should return False when .git directory exists."""
|
||||
git_dir = tmp_path / ".git"
|
||||
git_dir.mkdir()
|
||||
|
||||
with patch.object(Path, "cwd", return_value=tmp_path):
|
||||
result = gittool.not_has_git_repo()
|
||||
assert result is False
|
||||
|
||||
def test_not_has_git_repo_cwd_not_exists(self, tmp_path: Path) -> None:
|
||||
"""Should return True when cwd doesn't exist."""
|
||||
nonexistent = tmp_path / "nonexistent"
|
||||
|
||||
with patch.object(Path, "cwd", return_value=nonexistent):
|
||||
result = gittool.not_has_git_repo()
|
||||
assert result is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# has_files
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestHasFiles:
|
||||
"""Test has_files function."""
|
||||
|
||||
def test_has_files_true(self, tmp_path: Path) -> None:
|
||||
"""Should return True when files exist."""
|
||||
(tmp_path / "test.txt").write_text("test")
|
||||
|
||||
with patch.object(Path, "cwd", return_value=tmp_path):
|
||||
result = gittool.has_files()
|
||||
assert result is True
|
||||
|
||||
def test_has_files_false(self, tmp_path: Path) -> None:
|
||||
"""Should return False when no files."""
|
||||
with patch.object(Path, "cwd", return_value=tmp_path):
|
||||
result = gittool.has_files()
|
||||
assert result is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# init_sub_dirs
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestInitSubDirs:
|
||||
"""Test init_sub_dirs function."""
|
||||
|
||||
def test_init_sub_dirs_with_subdirectories(self, tmp_path: Path) -> None:
|
||||
"""Should initialize git in subdirectories."""
|
||||
subdir1 = tmp_path / "subdir1"
|
||||
subdir1.mkdir()
|
||||
subdir2 = tmp_path / "subdir2"
|
||||
subdir2.mkdir()
|
||||
|
||||
with patch.object(Path, "cwd", return_value=tmp_path), patch.object(px, "run") as mock_run:
|
||||
gittool.init_sub_dirs()
|
||||
# Should call px.run for each subdirectory
|
||||
assert mock_run.call_count == 2
|
||||
|
||||
def test_init_sub_dirs_no_subdirectories(self, tmp_path: Path) -> None:
|
||||
"""Should handle no subdirectories."""
|
||||
with patch.object(Path, "cwd", return_value=tmp_path), patch.object(px, "run") as mock_run:
|
||||
gittool.init_sub_dirs()
|
||||
# Should not call px.run
|
||||
assert mock_run.call_count == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# TaskSpec definitions
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTaskSpecDefinitions:
|
||||
"""Test that all TaskSpec definitions are valid."""
|
||||
|
||||
def test_push_spec(self) -> None:
|
||||
"""push spec should be properly defined."""
|
||||
assert gittool.push.name == "push"
|
||||
assert gittool.push.cmd == ["git", "push"]
|
||||
|
||||
def test_pull_spec(self) -> None:
|
||||
"""pull spec should be properly defined."""
|
||||
assert gittool.pull.name == "pull"
|
||||
assert gittool.pull.cmd == ["git", "pull"]
|
||||
|
||||
def test_kill_tgit_spec(self) -> None:
|
||||
"""kill_tgit spec should be properly defined."""
|
||||
assert gittool.kill_tgit.name == "task_kill"
|
||||
assert "taskkill" in gittool.kill_tgit.cmd
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_calls_run_cli(self) -> None:
|
||||
"""main() should create a CliRunner and call run_cli()."""
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
gittool.main()
|
||||
# run_cli() calls sys.exit(), so we should get SystemExit
|
||||
assert exc_info.value.code in (0, 1, 2)
|
||||
|
||||
def test_main_with_list_argument(self) -> None:
|
||||
"""main() should handle --list argument."""
|
||||
with patch("sys.argv", ["gittool", "--list"]), pytest.raises(SystemExit) as exc_info:
|
||||
gittool.main()
|
||||
assert exc_info.value.code == 0
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help and exit."""
|
||||
with patch("sys.argv", ["gittool"]), pytest.raises(SystemExit) as exc_info:
|
||||
gittool.main()
|
||||
assert exc_info.value.code == 1
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Tests for cli.lscalc module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import lscalc
|
||||
from pyflowx.conditions import Constants
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# get_ls_dyna_command
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestGetLsDynaCommand:
|
||||
"""Test get_ls_dyna_command function."""
|
||||
|
||||
def test_get_ls_dyna_command_windows(self) -> None:
|
||||
"""Should get LS-DYNA command for Windows."""
|
||||
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False):
|
||||
cmd = lscalc.get_ls_dyna_command("input.k", 4)
|
||||
assert "ls-dyna_mpp" in cmd
|
||||
assert "i=input.k" in cmd
|
||||
assert "ncpu=4" in cmd
|
||||
|
||||
def test_get_ls_dyna_command_linux(self) -> None:
|
||||
"""Should get LS-DYNA command for Linux."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False):
|
||||
cmd = lscalc.get_ls_dyna_command("input.k", 8)
|
||||
assert "ls-dyna_mpp" in cmd
|
||||
assert "i=input.k" in cmd
|
||||
assert "ncpu=8" in cmd
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# run_ls_dyna
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestRunLsDyna:
|
||||
"""Test run_ls_dyna function."""
|
||||
|
||||
def test_run_ls_dyna_success(self, tmp_path: Path) -> None:
|
||||
"""Should run LS-DYNA successfully."""
|
||||
input_file = tmp_path / "input.k"
|
||||
input_file.write_text("LS-DYNA input")
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
lscalc.run_ls_dyna(str(input_file), ncpu=4)
|
||||
assert mock_run.called
|
||||
|
||||
def test_run_ls_dyna_file_not_found(self, tmp_path: Path) -> None:
|
||||
"""Should handle nonexistent input file."""
|
||||
input_file = tmp_path / "nonexistent.k"
|
||||
|
||||
lscalc.run_ls_dyna(str(input_file), ncpu=4)
|
||||
# Should print error message
|
||||
|
||||
def test_run_ls_dyna_command_not_found(self, tmp_path: Path) -> None:
|
||||
"""Should handle command not found."""
|
||||
input_file = tmp_path / "input.k"
|
||||
input_file.write_text("LS-DYNA input")
|
||||
|
||||
with patch("subprocess.run", side_effect=FileNotFoundError):
|
||||
lscalc.run_ls_dyna(str(input_file), ncpu=4)
|
||||
# Should print error message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# run_ls_dyna_mpi
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestRunLsDynaMpi:
|
||||
"""Test run_ls_dyna_mpi function."""
|
||||
|
||||
def test_run_ls_dyna_mpi_success(self, tmp_path: Path) -> None:
|
||||
"""Should run LS-DYNA MPI successfully."""
|
||||
input_file = tmp_path / "input.k"
|
||||
input_file.write_text("LS-DYNA input")
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
lscalc.run_ls_dyna_mpi(str(input_file), ncpu=8)
|
||||
assert mock_run.called
|
||||
|
||||
def test_run_ls_dyna_mpi_file_not_found(self, tmp_path: Path) -> None:
|
||||
"""Should handle nonexistent input file."""
|
||||
input_file = tmp_path / "nonexistent.k"
|
||||
|
||||
lscalc.run_ls_dyna_mpi(str(input_file), ncpu=8)
|
||||
# Should print error message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# check_ls_dyna_status
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestCheckLsDynaStatus:
|
||||
"""Test check_ls_dyna_status function."""
|
||||
|
||||
def test_check_ls_dyna_status_windows(self) -> None:
|
||||
"""Should check LS-DYNA status on Windows."""
|
||||
with patch.object(Constants, "IS_WINDOWS", True), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(stdout="ls-dyna_mpp.exe", returncode=0)
|
||||
lscalc.check_ls_dyna_status()
|
||||
assert mock_run.called
|
||||
|
||||
def test_check_ls_dyna_status_linux(self) -> None:
|
||||
"""Should check LS-DYNA status on Linux."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(stdout="1234", returncode=0)
|
||||
lscalc.check_ls_dyna_status()
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_run_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle run command."""
|
||||
input_file = tmp_path / "input.k"
|
||||
input_file.write_text("LS-DYNA input")
|
||||
|
||||
with patch("sys.argv", ["lscalc", "run", str(input_file)]), patch.object(px, "run") as mock_run:
|
||||
lscalc.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_run_command_with_ncpu(self, tmp_path: Path) -> None:
|
||||
"""main() should handle run command with ncpu."""
|
||||
input_file = tmp_path / "input.k"
|
||||
input_file.write_text("LS-DYNA input")
|
||||
|
||||
with patch("sys.argv", ["lscalc", "run", str(input_file), "--ncpu", "8"]), patch.object(px, "run") as mock_run:
|
||||
lscalc.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_mpi_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle mpi command."""
|
||||
input_file = tmp_path / "input.k"
|
||||
input_file.write_text("LS-DYNA input")
|
||||
|
||||
with patch("sys.argv", ["lscalc", "mpi", str(input_file)]), patch.object(px, "run") as mock_run:
|
||||
lscalc.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_status_command(self) -> None:
|
||||
"""main() should handle status command."""
|
||||
with patch("sys.argv", ["lscalc", "status"]), patch.object(px, "run") as mock_run:
|
||||
lscalc.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["lscalc"]):
|
||||
lscalc.main()
|
||||
# Should print help and return
|
||||
@@ -0,0 +1,308 @@
|
||||
"""Tests for cli.packtool module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import packtool
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pack_source
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPackSource:
|
||||
"""Test pack_source function."""
|
||||
|
||||
def test_pack_source_basic(self, tmp_path: Path) -> None:
|
||||
"""Should pack source code."""
|
||||
project_dir = tmp_path / "project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / "main.py").write_text("print('hello')")
|
||||
output_dir = tmp_path / "output"
|
||||
|
||||
packtool.pack_source(project_dir, output_dir)
|
||||
assert output_dir.exists()
|
||||
|
||||
def test_pack_source_with_pyproject(self, tmp_path: Path) -> None:
|
||||
"""Should pack source with pyproject.toml."""
|
||||
project_dir = tmp_path / "project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / "pyproject.toml").write_text("[project]\nname = 'test'")
|
||||
(project_dir / "main.py").write_text("print('hello')")
|
||||
output_dir = tmp_path / "output"
|
||||
|
||||
packtool.pack_source(project_dir, output_dir)
|
||||
assert output_dir.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pack_dependencies
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPackDependencies:
|
||||
"""Test pack_dependencies function."""
|
||||
|
||||
def test_pack_dependencies_empty(self, tmp_path: Path) -> None:
|
||||
"""Should handle empty dependencies."""
|
||||
lib_dir = tmp_path / "libs"
|
||||
|
||||
packtool.pack_dependencies(lib_dir, [])
|
||||
# Should print message and return
|
||||
|
||||
def test_pack_dependencies_with_deps(self, tmp_path: Path) -> None:
|
||||
"""Should pack dependencies."""
|
||||
lib_dir = tmp_path / "libs"
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
packtool.pack_dependencies(lib_dir, ["numpy", "pandas"])
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pack_wheel
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPackWheel:
|
||||
"""Test pack_wheel function."""
|
||||
|
||||
def test_pack_wheel(self, tmp_path: Path) -> None:
|
||||
"""Should pack wheel."""
|
||||
project_dir = tmp_path / "project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / "pyproject.toml").write_text("[project]\nname = 'test'")
|
||||
output_dir = tmp_path / "dist"
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
packtool.pack_wheel(project_dir, output_dir)
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# install_embed_python
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestInstallEmbedPython:
|
||||
"""Test install_embed_python function."""
|
||||
|
||||
def test_install_embed_python_basic(self, tmp_path: Path) -> None:
|
||||
"""Should install embedded Python (mocked for speed)."""
|
||||
output_dir = tmp_path / "python"
|
||||
|
||||
# Create a mock cache file that doesn't exist (force download)
|
||||
with patch("urllib.request.urlretrieve") as mock_urlretrieve, \
|
||||
patch("zipfile.ZipFile") as mock_zipfile:
|
||||
|
||||
# Mock successful download
|
||||
mock_urlretrieve.return_value = None
|
||||
mock_zip_instance = MagicMock()
|
||||
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
|
||||
|
||||
# Ensure cache doesn't exist by using tmp_path as cache dir
|
||||
with patch.object(packtool, 'DEFAULT_CACHE_DIR', str(tmp_path / ".cache")):
|
||||
packtool.install_embed_python("3.10", output_dir)
|
||||
|
||||
# Verify download was called
|
||||
assert mock_urlretrieve.called
|
||||
# Verify extraction was called
|
||||
assert mock_zip_instance.extractall.called
|
||||
# Verify output directory was created
|
||||
assert output_dir.exists()
|
||||
|
||||
def test_install_embed_python_with_cache(self, tmp_path: Path) -> None:
|
||||
"""Should use cached Python if available."""
|
||||
output_dir = tmp_path / "python"
|
||||
cache_dir = tmp_path / ".cache" / "pypack"
|
||||
cache_dir.mkdir(parents=True)
|
||||
|
||||
# Create a fake cached zip file
|
||||
cache_file = cache_dir / "python-3.10.11-embed-amd64.zip"
|
||||
cache_file.write_bytes(b"PK\x03\x04" + b"\x00" * 100) # Minimal ZIP header
|
||||
|
||||
with patch("zipfile.ZipFile") as mock_zipfile:
|
||||
mock_zip_instance = MagicMock()
|
||||
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
|
||||
|
||||
packtool.install_embed_python("3.10", output_dir)
|
||||
|
||||
# Verify extraction was called (using cache)
|
||||
assert mock_zip_instance.extractall.called
|
||||
# Verify output directory was created
|
||||
assert output_dir.exists()
|
||||
|
||||
def test_install_embed_python_real_download(self, tmp_path: Path) -> None:
|
||||
"""Should actually download and extract embedded Python (requires network).
|
||||
|
||||
This test performs a real download to verify the entire workflow.
|
||||
It's marked to run only when network is available.
|
||||
"""
|
||||
import platform
|
||||
import zipfile
|
||||
|
||||
output_dir = tmp_path / "python_real"
|
||||
|
||||
# Only run on Windows (embed Python is Windows-specific)
|
||||
if platform.system() != "Windows":
|
||||
return
|
||||
|
||||
# Perform real installation
|
||||
packtool.install_embed_python("3.10", output_dir)
|
||||
|
||||
# Verify installation succeeded
|
||||
assert output_dir.exists()
|
||||
|
||||
# Verify key files are present
|
||||
expected_files = [
|
||||
"python.exe",
|
||||
"python310.dll",
|
||||
"python310.zip",
|
||||
]
|
||||
|
||||
for expected_file in expected_files:
|
||||
file_path = output_dir / expected_file
|
||||
assert file_path.exists(), f"Expected file {expected_file} not found"
|
||||
assert file_path.stat().st_size > 0, f"File {expected_file} is empty"
|
||||
|
||||
# Verify python.exe is executable
|
||||
python_exe = output_dir / "python.exe"
|
||||
assert python_exe.is_file()
|
||||
|
||||
# Verify the installation is functional
|
||||
# Check that we can at least read the zip file
|
||||
python_zip = output_dir / "python310.zip"
|
||||
assert zipfile.is_zipfile(python_zip)
|
||||
|
||||
print(f"✅ Successfully downloaded and installed embed Python to {output_dir}")
|
||||
print(f" Files: {list(output_dir.iterdir())}")
|
||||
|
||||
def test_install_embed_python_different_versions(self, tmp_path: Path) -> None:
|
||||
"""Should handle different Python versions."""
|
||||
output_dir = tmp_path / "python"
|
||||
|
||||
with patch("urllib.request.urlretrieve") as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
|
||||
mock_zip_instance = MagicMock()
|
||||
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
|
||||
|
||||
# Test different versions
|
||||
for version in ["3.8", "3.9", "3.10", "3.11", "3.12"]:
|
||||
packtool.install_embed_python(version, output_dir)
|
||||
assert mock_urlretrieve.called
|
||||
|
||||
def test_install_embed_python_creates_cache(self, tmp_path: Path) -> None:
|
||||
"""Should create cache directory and file."""
|
||||
output_dir = tmp_path / "python"
|
||||
|
||||
with patch("urllib.request.urlretrieve") as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
|
||||
mock_urlretrieve.return_value = None
|
||||
mock_zip_instance = MagicMock()
|
||||
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
|
||||
|
||||
packtool.install_embed_python("3.10", output_dir)
|
||||
|
||||
# Verify cache directory was created
|
||||
Path(packtool.DEFAULT_CACHE_DIR)
|
||||
# Note: In test environment, cache might not persist due to mocking
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# create_zip_package
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestCreateZipPackage:
|
||||
"""Test create_zip_package function."""
|
||||
|
||||
def test_create_zip_package(self, tmp_path: Path) -> None:
|
||||
"""Should create ZIP package."""
|
||||
source_dir = tmp_path / "source"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "test.txt").write_text("test content")
|
||||
output_file = tmp_path / "package.zip"
|
||||
|
||||
packtool.create_zip_package(source_dir, output_file)
|
||||
assert output_file.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# clean_build_dir
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestCleanBuildDir:
|
||||
"""Test clean_build_dir function."""
|
||||
|
||||
def test_clean_build_dir_exists(self, tmp_path: Path) -> None:
|
||||
"""Should clean existing build directory."""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
(build_dir / "test.txt").write_text("test")
|
||||
|
||||
packtool.clean_build_dir(build_dir)
|
||||
assert not build_dir.exists()
|
||||
|
||||
def test_clean_build_dir_not_exists(self, tmp_path: Path) -> None:
|
||||
"""Should handle nonexistent build directory."""
|
||||
build_dir = tmp_path / "nonexistent"
|
||||
|
||||
packtool.clean_build_dir(build_dir)
|
||||
# Should print message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_src_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle src command."""
|
||||
project_dir = tmp_path / "project"
|
||||
project_dir.mkdir()
|
||||
|
||||
with patch("sys.argv", ["packtool", "src", "--project-dir", str(project_dir)]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
packtool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_deps_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle deps command."""
|
||||
with patch("sys.argv", ["packtool", "deps", "numpy", "pandas"]), patch.object(px, "run") as mock_run:
|
||||
packtool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_wheel_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle wheel command."""
|
||||
project_dir = tmp_path / "project"
|
||||
project_dir.mkdir()
|
||||
|
||||
with patch("sys.argv", ["packtool", "wheel", "--project-dir", str(project_dir)]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
packtool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_embed_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle embed command."""
|
||||
with patch("sys.argv", ["packtool", "embed", "--version", "3.10"]), patch.object(px, "run") as mock_run:
|
||||
packtool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_zip_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle zip command."""
|
||||
source_dir = tmp_path / "source"
|
||||
source_dir.mkdir()
|
||||
|
||||
with patch("sys.argv", ["packtool", "zip", "--source-dir", str(source_dir)]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
packtool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_clean_command(self) -> None:
|
||||
"""main() should handle clean command."""
|
||||
with patch("sys.argv", ["packtool", "clean"]), patch.object(px, "run") as mock_run:
|
||||
packtool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["packtool"]):
|
||||
packtool.main()
|
||||
# Should print help and return
|
||||
@@ -0,0 +1,322 @@
|
||||
"""Tests for cli.pdftool module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import pdftool
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_merge
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfMerge:
|
||||
"""Test pdf_merge function."""
|
||||
|
||||
def test_pdf_merge_files(self, tmp_path: Path) -> None:
|
||||
"""Should merge PDF files."""
|
||||
pytest.importorskip("pypdf")
|
||||
input_files = [tmp_path / "input1.pdf", tmp_path / "input2.pdf"]
|
||||
for f in input_files:
|
||||
f.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "merged.pdf"
|
||||
|
||||
with patch("pypdf.PdfReader"), patch("pypdf.PdfWriter") as mock_writer:
|
||||
mock_writer_instance = MagicMock()
|
||||
mock_writer.return_value = mock_writer_instance
|
||||
pdftool.pdf_merge(input_files, output_file)
|
||||
assert mock_writer_instance.write.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_split
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfSplit:
|
||||
"""Test pdf_split function."""
|
||||
|
||||
def test_pdf_split_file(self, tmp_path: Path) -> None:
|
||||
"""Should split PDF file."""
|
||||
pytest.importorskip("pypdf")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_dir = tmp_path / "split"
|
||||
|
||||
with patch("pypdf.PdfReader") as mock_reader, patch("pypdf.PdfWriter"):
|
||||
mock_reader_instance = MagicMock()
|
||||
mock_reader.return_value = mock_reader_instance
|
||||
mock_reader_instance.pages = [MagicMock()]
|
||||
pdftool.pdf_split(input_file, output_dir)
|
||||
assert output_dir.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_compress
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfCompress:
|
||||
"""Test pdf_compress function."""
|
||||
|
||||
def test_pdf_compress_file(self, tmp_path: Path) -> None:
|
||||
"""Should compress PDF file."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "compressed.pdf"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open:
|
||||
mock_doc = MagicMock()
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
|
||||
# Mock save to actually create the file
|
||||
def mock_save(*args, **kwargs):
|
||||
output_file.write_bytes(b"Compressed PDF")
|
||||
|
||||
mock_doc.save = mock_save
|
||||
pdftool.pdf_compress(input_file, output_file)
|
||||
assert output_file.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_extract_text
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfExtractText:
|
||||
"""Test pdf_extract_text function."""
|
||||
|
||||
def test_pdf_extract_text_file(self, tmp_path: Path) -> None:
|
||||
"""Should extract text from PDF."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "output.txt"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open:
|
||||
mock_doc = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_page.get_text.return_value = "Test text"
|
||||
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
pdftool.pdf_extract_text(input_file, output_file)
|
||||
assert output_file.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_extract_images
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfExtractImages:
|
||||
"""Test pdf_extract_images function."""
|
||||
|
||||
def test_pdf_extract_images_file(self, tmp_path: Path) -> None:
|
||||
"""Should extract images from PDF."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_dir = tmp_path / "images"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open:
|
||||
mock_doc = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_page.get_images.return_value = [[0]]
|
||||
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
||||
mock_doc.extract_image.return_value = {"image": b"image data", "ext": "png"}
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
pdftool.pdf_extract_images(input_file, output_dir)
|
||||
assert output_dir.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_add_watermark
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfAddWatermark:
|
||||
"""Test pdf_add_watermark function."""
|
||||
|
||||
def test_pdf_add_watermark_file(self, tmp_path: Path) -> None:
|
||||
"""Should add watermark to PDF."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "watermarked.pdf"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open, patch("fitz.get_text_length") as mock_text_length:
|
||||
mock_doc = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_page.rect = MagicMock(width=800, height=600)
|
||||
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
mock_text_length.return_value = 100
|
||||
pdftool.pdf_add_watermark(input_file, output_file)
|
||||
assert mock_doc.save.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_rotate
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfRotate:
|
||||
"""Test pdf_rotate function."""
|
||||
|
||||
def test_pdf_rotate_file_90(self, tmp_path: Path) -> None:
|
||||
"""Should rotate PDF by 90 degrees."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "rotated.pdf"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open:
|
||||
mock_doc = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
pdftool.pdf_rotate(input_file, output_file, rotation=90)
|
||||
assert mock_doc.save.called
|
||||
|
||||
def test_pdf_rotate_file_180(self, tmp_path: Path) -> None:
|
||||
"""Should rotate PDF by 180 degrees."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "rotated.pdf"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open:
|
||||
mock_doc = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
pdftool.pdf_rotate(input_file, output_file, rotation=180)
|
||||
assert mock_doc.save.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_crop
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfCrop:
|
||||
"""Test pdf_crop function."""
|
||||
|
||||
def test_pdf_crop_file(self, tmp_path: Path) -> None:
|
||||
"""Should crop PDF."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "cropped.pdf"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open, patch("fitz.Rect"):
|
||||
mock_doc = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_page.rect = MagicMock(x0=0, y0=0, x1=800, y1=600)
|
||||
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
pdftool.pdf_crop(input_file, output_file, margins=(10, 10, 10, 10))
|
||||
assert mock_doc.save.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_info
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfInfo:
|
||||
"""Test pdf_info function."""
|
||||
|
||||
def test_pdf_info_file(self, tmp_path: Path) -> None:
|
||||
"""Should show PDF info."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open:
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.page_count = 10
|
||||
mock_doc.metadata = {"title": "Test", "author": "Author"}
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
pdftool.pdf_info(input_file)
|
||||
assert mock_fitz_open.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_ocr
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfOcr:
|
||||
"""Test pdf_ocr function."""
|
||||
|
||||
def test_pdf_ocr_file(self, tmp_path: Path) -> None:
|
||||
"""Should OCR PDF."""
|
||||
pytest.importorskip("fitz")
|
||||
pytest.importorskip("pytesseract")
|
||||
pytest.importorskip("PIL")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "ocr.pdf"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open, patch("PIL.Image.frombytes"), patch(
|
||||
"pytesseract.image_to_string"
|
||||
) as mock_ocr:
|
||||
mock_doc = MagicMock()
|
||||
mock_page = MagicMock()
|
||||
mock_page.rect = MagicMock(width=800, height=600)
|
||||
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
mock_ocr.return_value = "OCR text"
|
||||
pdftool.pdf_ocr(input_file, output_file)
|
||||
# Should complete OCR
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pdf_repair
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPdfRepair:
|
||||
"""Test pdf_repair function."""
|
||||
|
||||
def test_pdf_repair_file(self, tmp_path: Path) -> None:
|
||||
"""Should repair PDF."""
|
||||
pytest.importorskip("fitz")
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
output_file = tmp_path / "repaired.pdf"
|
||||
|
||||
with patch("fitz.open") as mock_fitz_open:
|
||||
mock_doc = MagicMock()
|
||||
mock_fitz_open.return_value = mock_doc
|
||||
pdftool.pdf_repair(input_file, output_file)
|
||||
assert mock_doc.save.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_merge_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle merge command."""
|
||||
input_files = [tmp_path / "input1.pdf", tmp_path / "input2.pdf"]
|
||||
for f in input_files:
|
||||
f.write_bytes(b"PDF content")
|
||||
|
||||
with patch("sys.argv", ["pdftool", "m", str(input_files[0]), str(input_files[1])]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
pdftool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_split_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle split command."""
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
|
||||
with patch("sys.argv", ["pdftool", "s", str(input_file)]), patch.object(px, "run") as mock_run:
|
||||
pdftool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_compress_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle compress command."""
|
||||
input_file = tmp_path / "input.pdf"
|
||||
input_file.write_bytes(b"PDF content")
|
||||
|
||||
with patch("sys.argv", ["pdftool", "c", str(input_file)]), patch.object(px, "run") as mock_run:
|
||||
pdftool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["pdftool"]):
|
||||
pdftool.main()
|
||||
# Should print help and return
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Tests for cli.piptool module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import piptool
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# _get_installed_packages
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestGetInstalledPackages:
|
||||
"""Test _get_installed_packages function."""
|
||||
|
||||
def test_get_installed_packages_success(self) -> None:
|
||||
"""Should get installed packages."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(stdout="numpy==1.0.0\npandas==2.0.0\n", returncode=0)
|
||||
result = piptool._get_installed_packages()
|
||||
assert "numpy" in result
|
||||
assert "pandas" in result
|
||||
|
||||
def test_get_installed_packages_empty(self) -> None:
|
||||
"""Should handle empty output."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(stdout="", returncode=0)
|
||||
result = piptool._get_installed_packages()
|
||||
assert result == []
|
||||
|
||||
def test_get_installed_packages_error(self) -> None:
|
||||
"""Should handle subprocess error."""
|
||||
with patch("subprocess.run", side_effect=subprocess.SubprocessError):
|
||||
result = piptool._get_installed_packages()
|
||||
assert result == []
|
||||
|
||||
def test_get_installed_packages_oserror(self) -> None:
|
||||
"""Should handle OSError."""
|
||||
with patch("subprocess.run", side_effect=OSError):
|
||||
result = piptool._get_installed_packages()
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# _expand_wildcard_packages
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestExpandWildcardPackages:
|
||||
"""Test _expand_wildcard_packages function."""
|
||||
|
||||
def test_expand_wildcard_no_pattern(self) -> None:
|
||||
"""Should return package name when no wildcard."""
|
||||
result = piptool._expand_wildcard_packages("numpy")
|
||||
assert result == ["numpy"]
|
||||
|
||||
def test_expand_wildcard_with_star(self) -> None:
|
||||
"""Should expand wildcard with star."""
|
||||
with patch.object(piptool, "_get_installed_packages", return_value=["numpy", "numpy-core", "pandas"]):
|
||||
result = piptool._expand_wildcard_packages("numpy*")
|
||||
assert "numpy" in result
|
||||
assert "numpy-core" in result
|
||||
|
||||
def test_expand_wildcard_with_question(self) -> None:
|
||||
"""Should expand wildcard with question mark."""
|
||||
with patch.object(piptool, "_get_installed_packages", return_value=["numpy", "numba"]):
|
||||
result = piptool._expand_wildcard_packages("num??")
|
||||
assert len(result) > 0
|
||||
|
||||
def test_expand_wildcard_no_match(self) -> None:
|
||||
"""Should return empty list when no match."""
|
||||
with patch.object(piptool, "_get_installed_packages", return_value=["pandas", "scipy"]):
|
||||
result = piptool._expand_wildcard_packages("numpy*")
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# _filter_protected_packages
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestFilterProtectedPackages:
|
||||
"""Test _filter_protected_packages function."""
|
||||
|
||||
def test_filter_protected_packages_normal(self) -> None:
|
||||
"""Should filter protected packages."""
|
||||
result = piptool._filter_protected_packages(["numpy", "pandas", "pyflowx"])
|
||||
assert "numpy" in result
|
||||
assert "pandas" in result
|
||||
assert "pyflowx" not in result
|
||||
|
||||
def test_filter_protected_packages_all_protected(self) -> None:
|
||||
"""Should filter all protected packages."""
|
||||
result = piptool._filter_protected_packages(["pyflowx", "bitool"])
|
||||
assert result == []
|
||||
|
||||
def test_filter_protected_packages_case_insensitive(self) -> None:
|
||||
"""Should filter case insensitive."""
|
||||
result = piptool._filter_protected_packages(["PyFlowX", "BITOOL"])
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pip_uninstall
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPipUninstall:
|
||||
"""Test pip_uninstall function."""
|
||||
|
||||
def test_pip_uninstall_single_package(self) -> None:
|
||||
"""Should uninstall single package."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
piptool.pip_uninstall(["numpy"])
|
||||
assert mock_run.called
|
||||
|
||||
def test_pip_uninstall_multiple_packages(self) -> None:
|
||||
"""Should uninstall multiple packages."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
piptool.pip_uninstall(["numpy", "pandas", "scipy"])
|
||||
# Should call pip uninstall
|
||||
assert mock_run.called
|
||||
|
||||
def test_pip_uninstall_with_wildcard(self) -> None:
|
||||
"""Should handle wildcard in package name."""
|
||||
with patch.object(piptool, "_expand_wildcard_packages", return_value=["numpy", "numpy-core"]), patch(
|
||||
"subprocess.run"
|
||||
) as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
piptool.pip_uninstall(["numpy*"])
|
||||
assert mock_run.called
|
||||
|
||||
def test_pip_uninstall_empty_packages(self) -> None:
|
||||
"""Should handle empty packages list."""
|
||||
with patch.object(piptool, "_expand_wildcard_packages", return_value=[]):
|
||||
piptool.pip_uninstall(["nonexistent*"])
|
||||
# Should not call subprocess.run
|
||||
|
||||
def test_pip_uninstall_all_protected(self) -> None:
|
||||
"""Should handle all protected packages."""
|
||||
piptool.pip_uninstall(["pyflowx"])
|
||||
# Should not call subprocess.run
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pip_reinstall
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPipReinstall:
|
||||
"""Test pip_reinstall function."""
|
||||
|
||||
def test_pip_reinstall_single_package(self) -> None:
|
||||
"""Should reinstall single package."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
piptool.pip_reinstall(["numpy"])
|
||||
# Should call pip uninstall and pip install
|
||||
assert mock_run.call_count == 2
|
||||
|
||||
def test_pip_reinstall_offline(self) -> None:
|
||||
"""Should reinstall packages offline."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
piptool.pip_reinstall(["numpy"], offline=True)
|
||||
# Should call pip install with offline flags
|
||||
assert mock_run.called
|
||||
|
||||
def test_pip_reinstall_all_protected(self) -> None:
|
||||
"""Should handle all protected packages."""
|
||||
piptool.pip_reinstall(["pyflowx"])
|
||||
# Should not call subprocess.run
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pip_download
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPipDownload:
|
||||
"""Test pip_download function."""
|
||||
|
||||
def test_pip_download_single_package(self) -> None:
|
||||
"""Should download single package."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
piptool.pip_download(["numpy"])
|
||||
assert mock_run.called
|
||||
|
||||
def test_pip_download_offline(self) -> None:
|
||||
"""Should download packages offline."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
piptool.pip_download(["numpy"], offline=True)
|
||||
# Should call pip download with offline flags
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# pip_freeze
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestPipFreeze:
|
||||
"""Test pip_freeze function."""
|
||||
|
||||
def test_pip_freeze(self, tmp_path: Path) -> None:
|
||||
"""Should freeze dependencies."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(stdout="numpy==1.0.0\npandas==2.0.0", returncode=0)
|
||||
piptool.pip_freeze()
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_install_command(self) -> None:
|
||||
"""main() should handle install command."""
|
||||
with patch("sys.argv", ["piptool", "i", "numpy", "pandas"]), patch.object(px, "run") as mock_run:
|
||||
piptool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_uninstall_command(self) -> None:
|
||||
"""main() should handle uninstall command."""
|
||||
with patch("sys.argv", ["piptool", "u", "numpy"]), patch.object(px, "run") as mock_run:
|
||||
piptool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_reinstall_command(self) -> None:
|
||||
"""main() should handle reinstall command."""
|
||||
with patch("sys.argv", ["piptool", "r", "numpy"]), patch.object(px, "run") as mock_run:
|
||||
piptool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_download_command(self) -> None:
|
||||
"""main() should handle download command."""
|
||||
with patch("sys.argv", ["piptool", "d", "numpy"]), patch.object(px, "run") as mock_run:
|
||||
piptool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_upgrade_command(self) -> None:
|
||||
"""main() should handle upgrade command."""
|
||||
with patch("sys.argv", ["piptool", "up"]), patch.object(px, "run") as mock_run:
|
||||
piptool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_freeze_command(self) -> None:
|
||||
"""main() should handle freeze command."""
|
||||
with patch("sys.argv", ["piptool", "f"]), patch.object(px, "run") as mock_run:
|
||||
piptool.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["piptool"]):
|
||||
piptool.main()
|
||||
# Should print help and return
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Tests for cli.screenshot module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import screenshot
|
||||
from pyflowx.conditions import Constants
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# get_screenshot_path
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestGetScreenshotPath:
|
||||
"""Test get_screenshot_path function."""
|
||||
|
||||
def test_get_screenshot_path_with_filename(self, tmp_path: Path) -> None:
|
||||
"""Should get screenshot path with filename."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
result = screenshot.get_screenshot_path("test.png")
|
||||
assert result.name == "test.png"
|
||||
|
||||
def test_get_screenshot_path_without_filename(self, tmp_path: Path) -> None:
|
||||
"""Should get screenshot path without filename."""
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
result = screenshot.get_screenshot_path()
|
||||
assert "screenshot_" in result.name
|
||||
assert result.suffix == ".png"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# take_screenshot_full
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTakeScreenshotFull:
|
||||
"""Test take_screenshot_full function."""
|
||||
|
||||
def test_take_screenshot_full_windows(self, tmp_path: Path) -> None:
|
||||
"""Should take full screenshot on Windows."""
|
||||
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False), patch.object(
|
||||
Path, "home", return_value=tmp_path
|
||||
), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
screenshot.take_screenshot_full()
|
||||
assert mock_run.called
|
||||
|
||||
def test_take_screenshot_full_macos(self, tmp_path: Path) -> None:
|
||||
"""Should take full screenshot on macOS."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", True), patch.object(
|
||||
Path, "home", return_value=tmp_path
|
||||
), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
screenshot.take_screenshot_full()
|
||||
assert mock_run.called
|
||||
|
||||
def test_take_screenshot_full_linux(self, tmp_path: Path) -> None:
|
||||
"""Should take full screenshot on Linux."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
|
||||
Path, "home", return_value=tmp_path
|
||||
), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
screenshot.take_screenshot_full()
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# take_screenshot_area
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestTakeScreenshotArea:
|
||||
"""Test take_screenshot_area function."""
|
||||
|
||||
def test_take_screenshot_area_windows(self, tmp_path: Path) -> None:
|
||||
"""Should take area screenshot on Windows."""
|
||||
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False), patch.object(
|
||||
Path, "home", return_value=tmp_path
|
||||
), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
screenshot.take_screenshot_area()
|
||||
assert mock_run.called
|
||||
|
||||
def test_take_screenshot_area_macos(self, tmp_path: Path) -> None:
|
||||
"""Should take area screenshot on macOS."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", True), patch.object(
|
||||
Path, "home", return_value=tmp_path
|
||||
), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
screenshot.take_screenshot_area()
|
||||
assert mock_run.called
|
||||
|
||||
def test_take_screenshot_area_linux(self, tmp_path: Path) -> None:
|
||||
"""Should take area screenshot on Linux."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
|
||||
Path, "home", return_value=tmp_path
|
||||
), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
screenshot.take_screenshot_area()
|
||||
assert mock_run.called
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_full_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle full command."""
|
||||
with patch("sys.argv", ["screenshot", "full"]), patch.object(px, "run") as mock_run:
|
||||
screenshot.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_area_command(self, tmp_path: Path) -> None:
|
||||
"""main() should handle area command."""
|
||||
with patch("sys.argv", ["screenshot", "area"]), patch.object(px, "run") as mock_run:
|
||||
screenshot.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help."""
|
||||
with patch("sys.argv", ["screenshot"]):
|
||||
screenshot.main()
|
||||
# Should print help and return
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Tests for cli.sshcopyid module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import sshcopyid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# ssh_copy_id
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestSshCopyId:
|
||||
"""Test ssh_copy_id function."""
|
||||
|
||||
def test_ssh_copy_id_pub_key_not_exists(self, tmp_path: Path) -> None:
|
||||
"""Should handle nonexistent public key."""
|
||||
with patch.object(Path, "expanduser", return_value=tmp_path / "nonexistent.pub"), pytest.raises(SystemExit):
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password")
|
||||
|
||||
def test_ssh_copy_id_sshpass_not_found(self, tmp_path: Path) -> None:
|
||||
"""Should handle sshpass not found."""
|
||||
pub_key = tmp_path / "id_rsa.pub"
|
||||
pub_key.write_text("ssh-rsa AAAAB3...")
|
||||
|
||||
with patch.object(Path, "expanduser", return_value=pub_key), patch(
|
||||
"subprocess.run", side_effect=FileNotFoundError
|
||||
), pytest.raises(SystemExit):
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password")
|
||||
|
||||
def test_ssh_copy_id_timeout(self, tmp_path: Path) -> None:
|
||||
"""Should handle SSH timeout."""
|
||||
pub_key = tmp_path / "id_rsa.pub"
|
||||
pub_key.write_text("ssh-rsa AAAAB3...")
|
||||
|
||||
with patch.object(Path, "expanduser", return_value=pub_key), patch(
|
||||
"subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 30)
|
||||
), pytest.raises(SystemExit):
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password")
|
||||
|
||||
def test_ssh_copy_id_process_error(self, tmp_path: Path) -> None:
|
||||
"""Should handle SSH process error."""
|
||||
pub_key = tmp_path / "id_rsa.pub"
|
||||
pub_key.write_text("ssh-rsa AAAAB3...")
|
||||
|
||||
with patch.object(Path, "expanduser", return_value=pub_key), patch(
|
||||
"subprocess.run", side_effect=subprocess.CalledProcessError(1, "cmd")
|
||||
), pytest.raises(SystemExit):
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password")
|
||||
|
||||
def test_ssh_copy_id_success(self, tmp_path: Path) -> None:
|
||||
"""Should deploy SSH key successfully."""
|
||||
pub_key = tmp_path / "id_rsa.pub"
|
||||
pub_key.write_text("ssh-rsa AAAAB3...")
|
||||
|
||||
with patch.object(Path, "expanduser", return_value=pub_key), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password")
|
||||
assert mock_run.called
|
||||
|
||||
def test_ssh_copy_id_with_custom_port(self, tmp_path: Path) -> None:
|
||||
"""Should handle custom port."""
|
||||
pub_key = tmp_path / "id_rsa.pub"
|
||||
pub_key.write_text("ssh-rsa AAAAB3...")
|
||||
|
||||
with patch.object(Path, "expanduser", return_value=pub_key), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password", port=2222)
|
||||
# Verify port is used
|
||||
call_args = mock_run.call_args[0][0]
|
||||
assert "2222" in call_args
|
||||
|
||||
def test_ssh_copy_id_with_custom_keypath(self, tmp_path: Path) -> None:
|
||||
"""Should handle custom keypath."""
|
||||
custom_key = tmp_path / "custom.pub"
|
||||
custom_key.write_text("ssh-rsa AAAAB3...")
|
||||
|
||||
with patch.object(Path, "expanduser", return_value=custom_key), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password", keypath=str(custom_key))
|
||||
assert mock_run.called
|
||||
|
||||
def test_ssh_copy_id_with_custom_timeout(self, tmp_path: Path) -> None:
|
||||
"""Should handle custom timeout."""
|
||||
pub_key = tmp_path / "id_rsa.pub"
|
||||
pub_key.write_text("ssh-rsa AAAAB3...")
|
||||
|
||||
with patch.object(Path, "expanduser", return_value=pub_key), patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
sshcopyid.ssh_copy_id("localhost", "user", "password", timeout=60)
|
||||
# Verify timeout is used in ConnectTimeout option
|
||||
call_args = mock_run.call_args[0][0]
|
||||
assert "ConnectTimeout=60" in call_args
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_with_required_args(self) -> None:
|
||||
"""main() should handle required arguments."""
|
||||
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
|
||||
sshcopyid.main()
|
||||
assert mock_run.called
|
||||
graph = mock_run.call_args[0][0]
|
||||
assert isinstance(graph, px.Graph)
|
||||
|
||||
def test_main_with_custom_port(self) -> None:
|
||||
"""main() should handle custom port argument."""
|
||||
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password", "--port", "2222"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
|
||||
sshcopyid.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_custom_keypath(self) -> None:
|
||||
"""main() should handle custom keypath argument."""
|
||||
with patch(
|
||||
"sys.argv", ["sshcopyid", "localhost", "user", "password", "--keypath", "/custom/key.pub"]
|
||||
), patch.object(px, "run") as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
|
||||
sshcopyid.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_custom_timeout(self) -> None:
|
||||
"""main() should handle custom timeout argument."""
|
||||
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password", "--timeout", "60"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
|
||||
sshcopyid.main()
|
||||
assert mock_run.called
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help and exit."""
|
||||
with patch("sys.argv", ["sshcopyid"]), pytest.raises(SystemExit) as exc_info:
|
||||
sshcopyid.main()
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
def test_main_creates_task_spec_with_correct_name(self) -> None:
|
||||
"""main() should create TaskSpec with correct name."""
|
||||
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
|
||||
sshcopyid.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
task_names = list(graph.all_specs().keys())
|
||||
assert "ssh_deploy" in task_names
|
||||
|
||||
def test_main_uses_thread_strategy(self) -> None:
|
||||
"""main() should use thread strategy."""
|
||||
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
|
||||
sshcopyid.main()
|
||||
assert mock_run.call_args[1]["strategy"] == "thread"
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Tests for cli.taskkill module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import taskkill
|
||||
from pyflowx.conditions import Constants
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_with_single_process(self) -> None:
|
||||
"""main() should handle single process argument."""
|
||||
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
|
||||
taskkill.main()
|
||||
assert mock_run.called
|
||||
graph = mock_run.call_args[0][0]
|
||||
assert isinstance(graph, px.Graph)
|
||||
|
||||
def test_main_with_multiple_processes(self) -> None:
|
||||
"""main() should handle multiple process arguments."""
|
||||
with patch("sys.argv", ["taskkill", "chrome.exe", "python.exe", "node.exe"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
taskkill.main()
|
||||
assert mock_run.called
|
||||
graph = mock_run.call_args[0][0]
|
||||
assert isinstance(graph, px.Graph)
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help and exit."""
|
||||
with patch("sys.argv", ["taskkill"]), pytest.raises(SystemExit) as exc_info:
|
||||
taskkill.main()
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
def test_main_creates_task_specs_with_correct_names(self) -> None:
|
||||
"""main() should create TaskSpecs with correct names."""
|
||||
with patch("sys.argv", ["taskkill", "chrome.exe", "python.exe"]), patch.object(px, "run") as mock_run:
|
||||
taskkill.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
task_names = list(graph.all_specs().keys())
|
||||
assert "kill_chrome.exe" in task_names
|
||||
assert "kill_python.exe" in task_names
|
||||
|
||||
def test_main_uses_thread_strategy(self) -> None:
|
||||
"""main() should use thread strategy."""
|
||||
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
|
||||
taskkill.main()
|
||||
assert mock_run.call_args[1]["strategy"] == "thread"
|
||||
|
||||
def test_main_windows_command_format(self) -> None:
|
||||
"""main() should use Windows command format on Windows."""
|
||||
if Constants.IS_WINDOWS:
|
||||
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
|
||||
taskkill.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
specs = graph.all_specs()
|
||||
# Check that command includes Windows taskkill format
|
||||
for spec in specs.values():
|
||||
assert spec.cmd[0] == "taskkill"
|
||||
assert spec.cmd[1] == "/f"
|
||||
assert spec.cmd[2] == "/im"
|
||||
|
||||
def test_main_linux_command_format(self) -> None:
|
||||
"""main() should use Linux command format on Linux."""
|
||||
with patch.object(Constants, "IS_WINDOWS", False), patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(
|
||||
px, "run"
|
||||
) as mock_run:
|
||||
taskkill.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
specs = graph.all_specs()
|
||||
# Check that command includes Linux pkill format
|
||||
for spec in specs.values():
|
||||
assert spec.cmd[0] == "pkill"
|
||||
assert spec.cmd[1] == "-f"
|
||||
|
||||
def test_main_tasks_have_verbose_true(self) -> None:
|
||||
"""main() should create tasks with verbose=True."""
|
||||
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
|
||||
taskkill.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
specs = graph.all_specs()
|
||||
for spec in specs.values():
|
||||
assert spec.verbose is True
|
||||
|
||||
def test_main_adds_wildcard_to_process_name(self) -> None:
|
||||
"""main() should add wildcard to process name."""
|
||||
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
|
||||
taskkill.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
specs = graph.all_specs()
|
||||
# Check that wildcard is added
|
||||
for spec in specs.values():
|
||||
assert spec.cmd[-1].endswith("*")
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Tests for cli.which module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import pyflowx as px
|
||||
from pyflowx.cli import which
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# which_command
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestWhichCommand:
|
||||
"""Test which_command function."""
|
||||
|
||||
def test_returns_path_when_command_found(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Should return Path when command is found."""
|
||||
with patch.object(shutil, "which", return_value="/usr/bin/python"):
|
||||
result = which.which_command("python")
|
||||
assert result == Path("/usr/bin/python")
|
||||
captured = capsys.readouterr()
|
||||
assert "匹配路径" in captured.out
|
||||
assert "/usr/bin/python" in captured.out
|
||||
|
||||
def test_returns_none_when_command_not_found(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Should return None when command is not found."""
|
||||
with patch.object(shutil, "which", return_value=None):
|
||||
result = which.which_command("nonexistent_cmd")
|
||||
assert result is None
|
||||
captured = capsys.readouterr()
|
||||
assert "未找到" in captured.out
|
||||
assert "nonexistent_cmd" in captured.out
|
||||
|
||||
def test_prints_match_path_on_success(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Should print '匹配路径: - <path>' on success."""
|
||||
with patch.object(shutil, "which", return_value="C:\\Python\\python.exe"):
|
||||
_ = which.which_command("python")
|
||||
captured = capsys.readouterr()
|
||||
assert "匹配路径: - C:\\Python\\python.exe" in captured.out
|
||||
|
||||
def test_prints_not_found_on_failure(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Should print '<command>: 未找到' on failure."""
|
||||
with patch.object(shutil, "which", return_value=None):
|
||||
_ = which.which_command("missing")
|
||||
captured = capsys.readouterr()
|
||||
assert "missing: 未找到" in captured.out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# main function
|
||||
# ---------------------------------------------------------------------- #
|
||||
class TestMain:
|
||||
"""Test main function."""
|
||||
|
||||
def test_main_with_single_command(self) -> None:
|
||||
"""main() should handle single command argument."""
|
||||
with patch("sys.argv", ["which", "python"]), patch.object(
|
||||
shutil, "which", return_value="/usr/bin/python"
|
||||
), patch.object(px, "run") as mock_run:
|
||||
which.main()
|
||||
# Should create a graph with one task
|
||||
assert mock_run.called
|
||||
graph = mock_run.call_args[0][0]
|
||||
assert isinstance(graph, px.Graph)
|
||||
|
||||
def test_main_with_multiple_commands(self) -> None:
|
||||
"""main() should handle multiple command arguments."""
|
||||
with patch("sys.argv", ["which", "python", "pip", "node"]), patch.object(
|
||||
shutil, "which", return_value="/usr/bin/cmd"
|
||||
), patch.object(px, "run") as mock_run:
|
||||
which.main()
|
||||
# Should create a graph with three tasks
|
||||
assert mock_run.called
|
||||
graph = mock_run.call_args[0][0]
|
||||
assert isinstance(graph, px.Graph)
|
||||
|
||||
def test_main_with_no_args_shows_help(self) -> None:
|
||||
"""main() with no args should show help and exit."""
|
||||
with patch("sys.argv", ["which"]), pytest.raises(SystemExit) as exc_info:
|
||||
which.main()
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
def test_main_creates_task_specs_with_correct_names(self) -> None:
|
||||
"""main() should create TaskSpecs with correct names."""
|
||||
with patch("sys.argv", ["which", "git", "npm"]), patch.object(
|
||||
shutil, "which", return_value="/usr/bin/cmd"
|
||||
), patch.object(px, "run") as mock_run:
|
||||
which.main()
|
||||
graph = mock_run.call_args[0][0]
|
||||
# Check that task names are correct
|
||||
task_names = list(graph.all_specs().keys())
|
||||
assert "which_git" in task_names
|
||||
assert "which_npm" in task_names
|
||||
|
||||
def test_main_uses_thread_strategy(self) -> None:
|
||||
"""main() should use thread strategy."""
|
||||
with patch("sys.argv", ["which", "python"]), patch.object(
|
||||
shutil, "which", return_value="/usr/bin/python"
|
||||
), patch.object(px, "run") as mock_run:
|
||||
which.main()
|
||||
assert mock_run.call_args[1]["strategy"] == "thread"
|
||||
@@ -136,7 +136,7 @@ class TestDescribeInjection:
|
||||
def test_describe_injection(self) -> None:
|
||||
"""应正确描述依赖注入、Context 标注和默认值."""
|
||||
|
||||
def fn(a: int, ctx: px.Context, flag: bool = False) -> None: # noqa: ARG001
|
||||
def fn(a: int, ctx: px.Context, flag: bool = False) -> None:
|
||||
return None
|
||||
|
||||
spec = px.TaskSpec("t", fn, depends_on=("a",))
|
||||
@@ -148,7 +148,7 @@ class TestDescribeInjection:
|
||||
def test_var_positional(self) -> None:
|
||||
"""*args 参数应显示为 *args."""
|
||||
|
||||
def fn(*args: Any) -> None: # noqa: ARG001
|
||||
def fn(*args: Any) -> None:
|
||||
return None
|
||||
|
||||
spec = px.TaskSpec("t", fn)
|
||||
@@ -158,7 +158,7 @@ class TestDescribeInjection:
|
||||
def test_var_keyword(self) -> None:
|
||||
"""**kwargs 参数应显示为 **kwargs=<all-deps>."""
|
||||
|
||||
def fn(**kwargs: Any) -> None: # pyright: ignore[reportExplicitAny, reportAny] # noqa: ARG001
|
||||
def fn(**kwargs: Any) -> None: # pyright: ignore[reportExplicitAny, reportAny]
|
||||
return None
|
||||
|
||||
spec = px.TaskSpec("t", fn, depends_on=("a",))
|
||||
@@ -168,7 +168,7 @@ class TestDescribeInjection:
|
||||
def test_unresolved(self) -> None:
|
||||
"""无依赖、无静态值、无默认的参数应显示为 <UNRESOLVED>."""
|
||||
|
||||
def fn(missing: int) -> None: # noqa: ARG001
|
||||
def fn(missing: int) -> None:
|
||||
return None
|
||||
|
||||
spec = px.TaskSpec("t", fn)
|
||||
@@ -178,7 +178,7 @@ class TestDescribeInjection:
|
||||
def test_static_kwargs(self) -> None:
|
||||
"""静态 kwargs 应显示具体值."""
|
||||
|
||||
def fn(flag: bool = False) -> None: # noqa: ARG001
|
||||
def fn(flag: bool = False) -> None:
|
||||
return None
|
||||
|
||||
spec = px.TaskSpec("t", fn, kwargs={"flag": True})
|
||||
@@ -188,7 +188,7 @@ class TestDescribeInjection:
|
||||
def test_positional_args_filled(self) -> None:
|
||||
"""spec.args 填充的位置参数应显示具体值(覆盖 args_filled 分支)."""
|
||||
|
||||
def fn(a: int, b: str) -> None: # noqa: ARG001
|
||||
def fn(a: int, b: str) -> None:
|
||||
return None
|
||||
|
||||
spec = px.TaskSpec("t", fn, args=(1, "x"))
|
||||
|
||||
Reference in New Issue
Block a user