refactor: 删除冗余 cli 入口脚本, gittool 用数组配置 clean excludes
CI / Lint, Typecheck & Test (push) Successful in 1m11s

1. 删除 13 个已有 YAML 配置的 cli .py 入口脚本, 统一通过 pf 调用
2. gittool.yaml 用 CLEAN_EXCLUDES 数组变量配置 git clean 的 -e 参数,
   保留 .venv/.tox/node_modules/.idea 等目录避免误删
3. run_cli 执行前打印调用信息: [gittool] 执行: c
4. 更新 pyproject.toml 移除 13 个冗余 entry points, 仅保留 pf
5. 清理测试文件中的 TestMain 类 (测 _ops 模块的测试保留)
This commit is contained in:
2026-07-05 08:39:20 +08:00
parent 6a3e3a57cd
commit de368ea810
30 changed files with 20 additions and 1288 deletions
-13
View File
@@ -25,27 +25,14 @@ requires-python = ">=3.8"
version = "0.3.5"
[project.scripts]
autofmt = "pyflowx.cli.autofmt:main"
bumpversion = "pyflowx.cli.bumpversion:main"
dockercmd = "pyflowx.cli.dev.dockercmd:main"
emlman = "pyflowx.cli.emlmanager:main"
filedate = "pyflowx.cli.filedate:main"
filelvl = "pyflowx.cli.filelevel:main"
foldback = "pyflowx.cli.folderback:main"
foldzip = "pyflowx.cli.folderzip:main"
gitt = "pyflowx.cli.gittool:main"
lscalc = "pyflowx.cli.lscalc:main"
msdown = "pyflowx.cli.llm.msdownload:main"
packtool = "pyflowx.cli.packtool:main"
pdftool = "pyflowx.cli.pdftool:main"
pf = "pyflowx.cli.pf:main"
piptool = "pyflowx.cli.piptool:main"
pxp = "pyflowx.cli.profiler:main"
pymake = "pyflowx.cli.pymake:main"
reseticon = "pyflowx.cli.reseticoncache:main"
scrcap = "pyflowx.cli.screenshot:main"
sglang = "pyflowx.cli.llm.sglang:main"
sshcopy = "pyflowx.cli.sshcopyid:main"
yamlrun = "pyflowx.cli.yamlrun:main"
# dev
envdev = "pyflowx.cli.dev.envdev:main"
+1
View File
@@ -144,6 +144,7 @@ __all__ = [
"TaskTimeoutError",
"YamlLoadError",
"build_call_args",
"build_cli_parser",
"cmd",
"compose",
"describe_injection",
-52
View File
@@ -1,52 +0,0 @@
"""自动格式化工具模块.
提供 Python 代码自动格式化的常用功能封装,
支持 docstring 自动生成、pyproject.toml 配置同步等功能.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "autofmt.yaml"
def main() -> None:
"""自动格式化工具主函数."""
parser = argparse.ArgumentParser(
description="AutoFmt - 自动格式化工具",
usage="autofmt <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
format_parser = subparsers.add_parser("fmt", help="使用 ruff 格式化代码")
format_parser.add_argument("--target", type=str, default=".", help="目标路径")
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="自动修复")
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 not in ("fmt", "lint", "doc", "sync"):
parser.print_help()
return
if args.command == "fmt":
px.run_yaml(_CONFIG, jobs="fmt", variables={"TARGET": args.target})
elif args.command == "lint":
job = "lint_fix" if args.fix else "lint"
px.run_yaml(_CONFIG, jobs=job, variables={"TARGET": args.target})
elif args.command == "doc":
px.run_yaml(_CONFIG, jobs="doc", variables={"ROOT_DIR": args.root_dir})
elif args.command == "sync":
px.run_yaml(_CONFIG, jobs="sync", variables={"ROOT_DIR": args.root_dir})
-43
View File
@@ -1,43 +0,0 @@
"""版本号自动管理工具.
通过 YAML 配置调用 ``bump_project_version``, 搜索项目中所有
``__init__.py`` 和 ``pyproject.toml`` 文件, 更新版本号并提交.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Literal, get_args
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "bumpversion.yaml"
BumpVersionType = Literal["patch", "minor", "major"]
def main() -> None:
"""版本号管理工具主函数."""
parser = argparse.ArgumentParser(description="BumpVersion - 版本号自动管理工具")
parser.add_argument(
"part",
type=str,
nargs="?",
default="patch",
choices=get_args(BumpVersionType),
help=f"版本部分: {get_args(BumpVersionType)}",
)
parser.add_argument(
"--no-tag",
action="store_true",
help="提交后不创建 git tag",
)
args = parser.parse_args()
px.run_yaml(
_CONFIG,
jobs="bump",
variables={"PART": args.part, "NO_TAG": args.no_tag},
)
+7 -2
View File
@@ -7,6 +7,11 @@
# pf gittool p
# pf gittool pl
strategy: thread
variables:
# git clean -e 参数列表 (展开为 cmd 数组元素)
CLEAN_EXCLUDES: ["-e", ".venv", "-e", ".tox", "-e", "node_modules",
"-e", ".idea", "-e", "idea.config",
"-e", "idea_modules.xml", "-e", "vcs.xml"]
cli:
description: "GitTool - Git 执行工具"
usage: "pf gittool <command>"
@@ -14,7 +19,7 @@ cli:
a:
help: "添加并提交"
c:
help: "查看状态"
help: "清理并查看状态"
i:
help: "初始化并提交"
isub:
@@ -28,7 +33,7 @@ jobs:
fn: git_add_commit
args: ["chore: update"]
clean:
cmd: ["git", "clean", "-xfd", "-e", "idea.config", "-e", "idea_modules.xml", "-e", "vcs.xml"]
cmd: ["git", "clean", "-xfd", "${CLEAN_EXCLUDES}"]
c:
needs: [clean]
cmd: ["git", "status", "--porcelain"]
-41
View File
@@ -1,41 +0,0 @@
"""文件日期处理工具.
自动检测文件名的日期前缀,
并根据文件的实际创建或修改时间重命名文件.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "filedate.yaml"
def main() -> None:
"""文件日期处理工具主函数."""
parser = argparse.ArgumentParser(
description="FileDate - 文件日期处理工具",
usage="filedate <command> [options]",
)
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 not in ("add", "clear"):
parser.print_help()
return
px.run_yaml(
_CONFIG,
jobs=args.command,
variables={"FILES": [Path(f) for f in args.files]},
)
-39
View File
@@ -1,39 +0,0 @@
"""文件等级重命名工具.
根据文件等级配置自动重命名文件,
支持多种等级标识和括号格式.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "filelevel.yaml"
def main() -> None:
"""文件等级重命名工具主函数."""
parser = argparse.ArgumentParser(
description="FileLevel - 文件等级重命名工具",
usage="filelevel <command> [options]",
)
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":
parser.print_help()
return
px.run_yaml(
_CONFIG,
jobs="set",
variables={"FILES": [Path(f) for f in args.files], "LEVEL": args.level},
)
-32
View File
@@ -1,32 +0,0 @@
"""文件夹备份工具.
备份文件和文件夹为 zip 文件,
自动删除超过最大数量的旧备份文件.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "folderback.yaml"
def main() -> None:
"""文件夹备份工具主函数."""
parser = argparse.ArgumentParser(
description="FolderBack - 文件夹备份工具",
usage="folderback [--src SRC] [--dst DST] [--max-zip MAX]",
)
parser.add_argument("--src", type=str, default=".", help="源文件夹路径 (默认: 当前目录)")
parser.add_argument("--dst", type=str, default="./backup", help="目标文件夹路径 (默认: ./backup)")
parser.add_argument("--max-zip", type=int, default=5, help="最大备份数量 (默认: 5)")
args = parser.parse_args()
px.run_yaml(
_CONFIG,
jobs="backup",
variables={"SRC": args.src, "DST": args.dst, "MAX_ZIP": args.max_zip},
)
-30
View File
@@ -1,30 +0,0 @@
"""文件夹压缩工具.
压缩目录下的所有文件/文件夹为 zip 文件,
默认压缩当前目录下的所有子文件夹.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "folderzip.yaml"
def main() -> None:
"""文件夹压缩工具主函数."""
parser = argparse.ArgumentParser(
description="FolderZip - 文件夹压缩工具",
usage="folderzip [--cwd DIR]",
)
parser.add_argument("--cwd", type=str, default=".", help="工作目录 (默认: 当前目录)")
args = parser.parse_args()
px.run_yaml(
_CONFIG,
jobs="zip",
variables={"CWD": args.cwd},
)
-39
View File
@@ -1,39 +0,0 @@
"""Git 工具模块.
提供 Git 仓库管理的常用操作封装,
支持初始化、提交、清理、推送等功能.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "gittool.yaml"
def main() -> None:
"""Git工具主函数."""
parser = argparse.ArgumentParser(
description="Gittool - Git 执行工具",
usage="gitt <command>",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
subparsers.add_parser("a", help="添加并提交")
subparsers.add_parser("c", help="清理 (git clean -xfd + status)")
subparsers.add_parser("i", help="初始化、添加并提交")
subparsers.add_parser("isub", help="初始化子目录的 Git 仓库")
subparsers.add_parser("p", help="推送")
subparsers.add_parser("pl", help="拉取")
args = parser.parse_args()
valid_commands = ("a", "c", "i", "isub", "p", "pl")
if args.command not in valid_commands:
parser.print_help()
return
px.run_yaml(_CONFIG, jobs=args.command)
-51
View File
@@ -1,51 +0,0 @@
"""LS-DYNA 计算工具.
用于管理 LS-DYNA 仿真计算任务,
支持启动、监控和管理计算进程.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "lscalc.yaml"
_DEFAULT_NCPU = 4
def main() -> None:
"""LS-DYNA 计算工具主函数."""
parser = argparse.ArgumentParser(
description="LSCalc - LS-DYNA 计算工具",
usage="lscalc <command> [options]",
)
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_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 not in ("run", "mpi", "status"):
parser.print_help()
return
variables: dict[str, object] = {"NCPU": getattr(args, "ncpu", _DEFAULT_NCPU)}
if args.command in ("run", "mpi"):
variables["INPUT_FILE"] = args.input_file
px.run_yaml(
_CONFIG,
jobs=args.command,
variables=variables,
)
-76
View File
@@ -1,76 +0,0 @@
"""Python 打包工具模块.
提供 Python 项目打包的常用功能封装,
支持源码打包、依赖打包、嵌入式 Python 安装等功能.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "packtool.yaml"
_DEFAULT_BUILD_DIR = ".pypack"
_DEFAULT_DIST_DIR = "dist"
_DEFAULT_LIB_DIR = "libs"
def main() -> None:
"""Python 打包工具主函数."""
parser = argparse.ArgumentParser(
description="PackTool - Python 打包工具",
usage="packtool <command> [options]",
)
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_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="输出目录")
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_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 not in ("src", "deps", "wheel", "embed", "zip", "clean"):
parser.print_help()
return
variables: dict[str, object] = {}
if args.command == "src":
variables = {"PROJECT_DIR": Path(args.project_dir), "OUTPUT_DIR": Path(args.output_dir)}
elif args.command == "deps":
variables = {"LIB_DIR": Path(args.lib_dir), "DEPENDENCIES": args.dependencies}
elif args.command == "wheel":
variables = {"PROJECT_DIR": Path(args.project_dir), "OUTPUT_DIR": Path(args.output_dir)}
elif args.command == "embed":
variables = {"VERSION": args.version, "OUTPUT_DIR": Path(args.output_dir)}
elif args.command == "zip":
variables = {"SOURCE_DIR": Path(args.source_dir), "OUTPUT_FILE": Path(args.output_file)}
elif args.command == "clean":
variables = {"OUTPUT_DIR": Path(_DEFAULT_BUILD_DIR)}
px.run_yaml(
_CONFIG,
jobs=args.command,
variables=variables,
)
-134
View File
@@ -1,134 +0,0 @@
"""PDF 工具模块.
提供 PDF 文件操作的常用功能封装,
支持合并、拆分、压缩、加密、水印、OCR等功能.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "pdftool.yaml"
def main() -> None: # noqa: PLR0912
"""PDF 工具主函数."""
parser = argparse.ArgumentParser(
description="PDFTool - PDF 文件工具集",
usage="pdftool <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
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="输出文件路径")
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="输出目录")
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="输出文件路径")
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="密码")
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="水印文本")
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)")
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_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")
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()
valid_commands = ("m", "s", "c", "e", "d", "xt", "xi", "w", "r", "crop", "i", "ocr", "img", "repair")
if args.command not in valid_commands:
parser.print_help()
return
variables: dict[str, object] = {}
if args.command == "m":
variables = {"INPUTS": [Path(p) for p in args.inputs], "OUTPUT": Path(args.output)}
elif args.command == "s":
variables = {"INPUT": Path(args.input), "OUTPUT_DIR": Path(args.output_dir)}
elif args.command == "c":
variables = {"INPUT": Path(args.input), "OUTPUT": Path(args.output)}
elif args.command in {"e", "d"}:
variables = {"INPUT": Path(args.input), "OUTPUT": Path(args.output), "PASSWORD": args.password}
elif args.command == "xt":
variables = {"INPUT": Path(args.input), "OUTPUT": Path(args.output)}
elif args.command == "xi":
variables = {"INPUT": Path(args.input), "OUTPUT_DIR": Path(args.output_dir)}
elif args.command == "w":
variables = {"INPUT": Path(args.input), "OUTPUT": Path(args.output), "TEXT": args.text}
elif args.command == "r":
variables = {"INPUT": Path(args.input), "OUTPUT": Path(args.output), "ROTATION": args.rotation}
elif args.command == "crop":
variables = {
"INPUT": Path(args.input),
"OUTPUT": Path(args.output),
"MARGINS": [args.left, args.top, args.right, args.bottom],
}
elif args.command == "i":
variables = {"INPUT": Path(args.input)}
elif args.command == "ocr":
variables = {"INPUT": Path(args.input), "OUTPUT": Path(args.output), "LANG": args.lang}
elif args.command == "img":
variables = {"INPUT": Path(args.input), "OUTPUT_DIR": Path(args.output_dir), "DPI": args.dpi}
elif args.command == "repair":
variables = {"INPUT": Path(args.input), "OUTPUT": Path(args.output)}
px.run_yaml(
_CONFIG,
jobs=args.command,
variables=variables,
)
-59
View File
@@ -1,59 +0,0 @@
"""pip 包管理工具模块.
提供 pip 包管理操作的封装,
支持安装、卸载、下载等功能.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "piptool.yaml"
def main() -> None:
"""pip 工具主函数."""
parser = argparse.ArgumentParser(
description="PipTool - pip 包管理工具",
usage="piptool <command> [options]",
)
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="使用离线模式")
subparsers.add_parser("up", help="升级 pip")
subparsers.add_parser("f", help="冻结依赖到 requirements.txt")
args = parser.parse_args()
if args.command not in ("i", "u", "r", "d", "up", "f"):
parser.print_help()
return
variables: dict[str, object] = {}
if args.command in ("i", "u", "r", "d"):
variables["PACKAGES"] = args.packages
if args.command in ("r", "d"):
variables["OFFLINE"] = args.offline
px.run_yaml(
_CONFIG,
jobs=args.command,
variables=variables,
)
-40
View File
@@ -1,40 +0,0 @@
"""截图工具.
跨平台截图工具, 支持全屏截图和区域截图.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "screenshot.yaml"
def main() -> None:
"""截图工具主函数."""
parser = argparse.ArgumentParser(
description="Screenshot - 截图工具",
usage="screenshot <command> [options]",
)
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 not in ("full", "area"):
parser.print_help()
return
px.run_yaml(
_CONFIG,
jobs=args.command,
variables={"FILENAME": args.filename},
)
-42
View File
@@ -1,42 +0,0 @@
"""SSH 密钥部署工具.
类似 ssh-copy-id, 自动将 SSH 公钥部署到远程服务器,
支持密码认证和密钥认证两种方式.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
_CONFIG = Path(__file__).parent / "configs" / "sshcopyid.yaml"
def main() -> None:
"""SSH 密钥部署工具主函数."""
parser = argparse.ArgumentParser(
description="SSHCopyID - SSH 密钥部署工具",
usage="sshcopyid <hostname> <username> <password> [--port PORT] [--keypath KEYPATH]",
)
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()
px.run_yaml(
_CONFIG,
jobs="deploy",
variables={
"HOSTNAME": args.hostname,
"USERNAME": args.username,
"PASSWORD": args.password,
"PORT": args.port,
"KEYPATH": args.keypath,
"TIMEOUT": args.timeout,
},
)
+12 -7
View File
@@ -137,13 +137,15 @@ _ENV_VAR_EQUALS_RE = re.compile(r"^env\.(\w+)\s*==\s*['\"]([^'\"]*)['\"]$")
_ENV_VAR_NOT_EQUALS_RE = re.compile(r"^env\.(\w+)\s*!=\s*['\"]([^'\"]*)['\"]$")
# 支持的 job 字段名(连字符形式)
_JOB_FIELDS_HYPHEN = frozenset({
"continue-on-error",
"skip-if-missing",
"allow-upstream-skip",
"concurrency-key",
"runs-on",
})
_JOB_FIELDS_HYPHEN = frozenset(
{
"continue-on-error",
"skip-if-missing",
"allow-upstream-skip",
"concurrency-key",
"runs-on",
}
)
class YamlLoadError(px.PyFlowXError):
@@ -1028,6 +1030,9 @@ def run_cli(path: str | Path, argv: Sequence[str] | None = None) -> int: # noqa
variables = _extract_variables(args, cli_config)
job_display = job_name if job_name else "全部任务"
print(f"[{p.stem}] 执行: {job_display}", flush=True)
try:
run_yaml(
p,
-84
View File
@@ -5,8 +5,6 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import autofmt
from pyflowx.cli._ops import dev
@@ -218,85 +216,3 @@ class TestFormatAll:
dev.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
-57
View File
@@ -4,12 +4,9 @@ from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import patch
import pytest
import pyflowx as px
from pyflowx.cli import bumpversion
from pyflowx.cli._ops import dev
@@ -273,57 +270,3 @@ class TestEdgeCases:
# 验证最终结果
assert test_file.read_text(encoding="utf-8") == '__version__ = "2.0.0"'
class TestBumpVersionCli:
"""Test bumpversion CLI."""
def test_minor(self, tmp_path: Path) -> None:
"""Should handle minor version bump."""
test_file = tmp_path / "__init__.py"
test_file.write_text('__version__ = "1.0.0"', encoding="utf-8")
# Mock px.run: 只真正执行第一次调用(版本更新),其余返回空 dict
# run_yaml 内部调用 px.run 时传递 max_workers/dry_run 等 kwargs, 故用 *args/**kwargs 兼容
with patch("sys.argv", ["bumpversion", "minor", "--no-tag"]), patch("pyflowx.run") as mock_run:
def run_side_effect(*args: object, **kwargs: object) -> dict[str, object]:
# 执行实际版本更新任务
graph = args[0] if args else kwargs.get("graph")
assert isinstance(graph, px.Graph)
results: dict[str, object] = {}
for spec in graph.specs.values():
if spec.fn is not None and spec.args:
results[spec.name] = spec.fn(*spec.args)
return results
mock_run.side_effect = run_side_effect
bumpversion.main()
# 验证版本号已更新
assert test_file.read_text(encoding="utf-8") == '__version__ = "1.1.0"'
def test_no_valid_files(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""Should handle no valid files."""
test_file = tmp_path / "test.txt"
test_file.write_text("这是一个测试文件", encoding="utf-8")
# run_yaml 内部调用 px.run 时传递 max_workers/dry_run 等 kwargs, 故用 *args/**kwargs 兼容
with patch("sys.argv", ["bumpversion", "minor", "--no-tag"]), patch("pyflowx.run") as mock_run:
def run_side_effect(*args: object, **kwargs: object) -> dict[str, object]:
# 执行实际版本更新任务
graph = args[0] if args else kwargs.get("graph")
assert isinstance(graph, px.Graph)
results: dict[str, object] = {}
for spec in graph.specs.values():
if spec.fn is not None and spec.args:
results[spec.name] = spec.fn(*spec.args)
return results
mock_run.side_effect = run_side_effect
bumpversion.main()
# 验证未更新任何文件
assert test_file.read_text(encoding="utf-8") == "这是一个测试文件"
assert "未找到包含版本号的文件" in capsys.readouterr().out
-34
View File
@@ -3,10 +3,7 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import filedate
from pyflowx.cli._ops import files
@@ -104,34 +101,3 @@ class TestProcessFilesDate:
files.process_files_date(file_list, 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
-38
View File
@@ -3,10 +3,7 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import filelevel
from pyflowx.cli._ops import files
@@ -97,38 +94,3 @@ class TestProcessFilesLevel:
files.process_files_level(file_list, 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
-19
View File
@@ -5,8 +5,6 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import folderback
from pyflowx.cli._ops import files
@@ -159,20 +157,3 @@ class TestRegisteredFunctions:
"""folderback_default should be a registered callable."""
# folderback_default 现在是通过 @px.register_fn 注册的普通函数, 不是 TaskSpec
assert callable(files.folderback_default)
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_yaml(self) -> None:
"""main() should parse args and call px.run_yaml()."""
with patch("sys.argv", ["folderback", "--src", ".", "--dst", "./backup"]), patch.object(
px, "run_yaml"
) as mock_run_yaml:
folderback.main()
assert mock_run_yaml.called
# 验证 jobs 参数为 "backup" (YAML 中的 job 名)
assert mock_run_yaml.call_args.kwargs["jobs"] == "backup"
-17
View File
@@ -5,8 +5,6 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import folderzip
from pyflowx.cli._ops import files
@@ -61,18 +59,3 @@ class TestRegisteredFunctions:
"""folderzip_default should be a registered callable."""
# folderzip_default 现在是通过 @px.register_fn 注册的普通函数, 不是 TaskSpec
assert callable(files.folderzip_default)
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_yaml(self) -> None:
"""main() should parse args and call px.run_yaml()."""
with patch("sys.argv", ["folderzip", "--cwd", "."]), patch.object(px, "run_yaml") as mock_run_yaml:
folderzip.main()
assert mock_run_yaml.called
# 验证 jobs 参数为 "zip" (YAML 中的 job 名)
assert mock_run_yaml.call_args.kwargs["jobs"] == "zip"
-34
View File
@@ -8,7 +8,6 @@ from unittest.mock import patch
import pytest
import pyflowx as px
from pyflowx.cli import gittool
from pyflowx.cli._ops import dev
@@ -93,36 +92,3 @@ class TestInitSubDirs:
dev.init_sub_dirs()
# Should not call px.run
assert mock_run.call_count == 0
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_yaml(self) -> None:
"""main() should parse args and call px.run_yaml()."""
# 使用有效命令 p (推送), mock run_yaml 避免实际执行 git 命令
with patch("sys.argv", ["gittool", "p"]), patch.object(px, "run_yaml") as mock_run_yaml:
gittool.main()
assert mock_run_yaml.called
# 验证 jobs 参数为 "p" (YAML 中的 job 名)
assert mock_run_yaml.call_args.kwargs["jobs"] == "p"
def test_main_with_valid_command(self) -> None:
"""main() should handle valid commands."""
# 测试各个有效命令均能调用 run_yaml
for cmd in ("a", "c", "i", "isub", "p", "pl"):
with patch("sys.argv", ["gittool", cmd]), patch.object(px, "run_yaml") as mock_run_yaml:
gittool.main()
assert mock_run_yaml.called
assert mock_run_yaml.call_args.kwargs["jobs"] == cmd
def test_main_with_no_args_shows_help(self, capsys: pytest.CaptureFixture[str]) -> None:
"""main() with no args should show help and return."""
# 新的 main() 在无有效命令时打印 help 并 return, 不再 sys.exit
with patch("sys.argv", ["gittool"]):
gittool.main()
captured = capsys.readouterr()
assert "Gittool" in captured.out or "usage" in captured.out
-48
View File
@@ -5,8 +5,6 @@ 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.cli._ops import system
from pyflowx.conditions import Constants
@@ -110,49 +108,3 @@ class TestCheckLsDynaStatus:
mock_run.return_value = MagicMock(stdout="1234", returncode=0)
system.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
-66
View File
@@ -7,8 +7,6 @@ from unittest.mock import MagicMock, patch
import pytest
import pyflowx as px
from pyflowx.cli import packtool
from pyflowx.cli._ops import system
@@ -259,67 +257,3 @@ class TestCleanBuildDir:
system.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
-45
View File
@@ -8,8 +8,6 @@ from unittest.mock import MagicMock, patch
import pytest
import pyflowx as px
from pyflowx.cli import pdftool
from pyflowx.cli._ops import media
@@ -280,46 +278,3 @@ class TestPdfRepair:
mock_fitz_open.return_value = mock_doc
media.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
-51
View File
@@ -6,8 +6,6 @@ import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import piptool
from pyflowx.cli._ops import dev
@@ -204,52 +202,3 @@ class TestPipFreeze:
mock_run.return_value = MagicMock(stdout="numpy==1.0.0\npandas==2.0.0", returncode=0)
dev.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
-27
View File
@@ -5,8 +5,6 @@ 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.cli._ops import media
from pyflowx.conditions import Constants
@@ -97,28 +95,3 @@ class TestTakeScreenshotArea:
mock_run.return_value = MagicMock(returncode=0)
media.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
-68
View File
@@ -8,8 +8,6 @@ from unittest.mock import MagicMock, patch
import pytest
import pyflowx as px
from pyflowx.cli import sshcopyid
from pyflowx.cli._ops import system
@@ -97,69 +95,3 @@ class TestSshCopyId:
# 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(system, "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(system, "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(system, "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(system, "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(system, "ssh_copy_id"):
sshcopyid.main()
graph = mock_run.call_args[0][0]
task_names = list(graph.all_specs().keys())
# YAML 中 job 名为 "deploy" (不再是 "ssh_deploy")
assert "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(system, "ssh_copy_id"):
sshcopyid.main()
assert mock_run.call_args[1]["strategy"] == "thread"