py-wlcli 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- wl_cli/vendors/sop_doc/rust-readme.md +219 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Utility module for test commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _probe_pytest_cov(
|
|
8
|
+
python_executable: str, env: dict[str, str] | None = None
|
|
9
|
+
) -> bool:
|
|
10
|
+
"""pytest-cov 是否可被**这个解释器**导入。
|
|
11
|
+
|
|
12
|
+
判定必须落在真正执行 pytest 的解释器上,而不是运行 wl 的那个进程:wl 由
|
|
13
|
+
uv tool 环境提供、测试却跑在项目 .venv 里时两者是两个解释器。只看 wl 进程
|
|
14
|
+
会让 `--no-cov` 误判 —— 漏判时子进程转而吃 pyproject 的 `--cov=wl_cli`(于是
|
|
15
|
+
写 coverage 数据文件、可能撞上 fail_under);多判时子进程收到 pytest 认不出
|
|
16
|
+
的 `--no-cov`,直接以 "unrecognized arguments" 失败。
|
|
17
|
+
|
|
18
|
+
`env` 必须与 exec 该解释器时拿到的环境一致,探针与子进程才看同一份
|
|
19
|
+
PATH/site-packages。#108 起所有调用点都传执行用 env。
|
|
20
|
+
|
|
21
|
+
解释器起不来(不存在 / 无法执行)时判 False:宁可少传 `--no-cov`,也不能传
|
|
22
|
+
一个 pytest 认不出的 flag。
|
|
23
|
+
"""
|
|
24
|
+
import subprocess
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
probe = subprocess.run(
|
|
28
|
+
[python_executable, "-c", "import pytest_cov"],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
check=False,
|
|
31
|
+
timeout=30,
|
|
32
|
+
env=env,
|
|
33
|
+
)
|
|
34
|
+
return probe.returncode == 0
|
|
35
|
+
except (OSError, subprocess.SubprocessError):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_pytest_available(python_executable: str) -> str:
|
|
40
|
+
"""Ensure pytest is available, install if not."""
|
|
41
|
+
import subprocess
|
|
42
|
+
|
|
43
|
+
# 检查是否在测试环境中运行
|
|
44
|
+
is_testing = "PYTEST_CURRENT_TEST" in os.environ
|
|
45
|
+
|
|
46
|
+
# 只在非测试环境中检查和安装pytest
|
|
47
|
+
if not is_testing:
|
|
48
|
+
try:
|
|
49
|
+
subprocess.run(
|
|
50
|
+
[python_executable, "-m", "pytest", "--version"],
|
|
51
|
+
capture_output=True,
|
|
52
|
+
check=True,
|
|
53
|
+
text=True,
|
|
54
|
+
errors="replace",
|
|
55
|
+
)
|
|
56
|
+
except subprocess.CalledProcessError:
|
|
57
|
+
python_executable = _install_pytest(python_executable)
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
# 如果Python环境不存在,抛出FileNotFoundError以便测试捕获
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
return python_executable
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _install_pytest(python_executable: str) -> str:
|
|
66
|
+
"""Install pytest into the selected environment, or exit with an install hint.
|
|
67
|
+
|
|
68
|
+
uv-managed venvs (``uv venv``) ship no pip module, so ``uv pip install`` is
|
|
69
|
+
tried first (with ``--python`` pinning the exact interpreter ``wl test``
|
|
70
|
+
selected); classic pip venvs fall back to ``python -m pip install``.
|
|
71
|
+
"""
|
|
72
|
+
import shutil
|
|
73
|
+
import subprocess
|
|
74
|
+
import sys
|
|
75
|
+
|
|
76
|
+
print(
|
|
77
|
+
f"Error: pytest not found in the selected Python environment: {python_executable}"
|
|
78
|
+
)
|
|
79
|
+
print("Installing pytest...")
|
|
80
|
+
|
|
81
|
+
commands: list[list[str]] = []
|
|
82
|
+
uv_path = shutil.which("uv")
|
|
83
|
+
if uv_path:
|
|
84
|
+
commands.append(
|
|
85
|
+
[uv_path, "pip", "install", "--python", python_executable, "pytest"]
|
|
86
|
+
)
|
|
87
|
+
commands.append([python_executable, "-m", "pip", "install", "pytest"])
|
|
88
|
+
|
|
89
|
+
install_errors: list[str] = []
|
|
90
|
+
for command in commands:
|
|
91
|
+
try:
|
|
92
|
+
subprocess.run(
|
|
93
|
+
command,
|
|
94
|
+
check=True,
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
errors="replace",
|
|
98
|
+
timeout=120,
|
|
99
|
+
)
|
|
100
|
+
except (
|
|
101
|
+
subprocess.CalledProcessError,
|
|
102
|
+
FileNotFoundError,
|
|
103
|
+
PermissionError,
|
|
104
|
+
subprocess.TimeoutExpired,
|
|
105
|
+
) as exc:
|
|
106
|
+
install_errors.append(_install_failure_summary(command[0], exc))
|
|
107
|
+
continue
|
|
108
|
+
# Verify the install actually made pytest importable: uv --python can
|
|
109
|
+
# target an environment other than the one tests will invoke.
|
|
110
|
+
try:
|
|
111
|
+
subprocess.run(
|
|
112
|
+
[python_executable, "-m", "pytest", "--version"],
|
|
113
|
+
capture_output=True,
|
|
114
|
+
check=True,
|
|
115
|
+
text=True,
|
|
116
|
+
timeout=30,
|
|
117
|
+
)
|
|
118
|
+
except (
|
|
119
|
+
subprocess.CalledProcessError,
|
|
120
|
+
FileNotFoundError,
|
|
121
|
+
PermissionError,
|
|
122
|
+
subprocess.TimeoutExpired,
|
|
123
|
+
):
|
|
124
|
+
continue
|
|
125
|
+
print("pytest installed successfully.")
|
|
126
|
+
return python_executable
|
|
127
|
+
|
|
128
|
+
print("Error: could not install pytest automatically.")
|
|
129
|
+
print("pytest is a test-only dependency of wl_cli (the `test` extra).")
|
|
130
|
+
print(
|
|
131
|
+
'Install it with: uv pip install -e ".[test]" (or: pip install -e ".[test]")'
|
|
132
|
+
)
|
|
133
|
+
if install_errors:
|
|
134
|
+
print(f"Installer diagnostics: {' | '.join(install_errors)}")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _install_failure_summary(tool: str, exc: BaseException) -> str:
|
|
139
|
+
"""One-line summary of a failed installer attempt, for the exit hint."""
|
|
140
|
+
import subprocess
|
|
141
|
+
|
|
142
|
+
if isinstance(exc, subprocess.TimeoutExpired):
|
|
143
|
+
return f"{tool}: timed out after 120s"
|
|
144
|
+
if isinstance(exc, subprocess.CalledProcessError):
|
|
145
|
+
tail = (exc.stderr or exc.stdout or "").strip().splitlines()[-2:]
|
|
146
|
+
if tail:
|
|
147
|
+
return f"{tool}: {'; '.join(tail)}"
|
|
148
|
+
return f"{tool}: exit {exc.returncode}"
|
|
149
|
+
return f"{tool}: {exc}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _process_command_args(
|
|
153
|
+
self, kwargs: dict[str, Any]
|
|
154
|
+
) -> tuple[bool, bool, bool, bool, bool, bool, bool, bool, list[str]]:
|
|
155
|
+
"""Process command line arguments."""
|
|
156
|
+
report_mode = kwargs.get("report", False)
|
|
157
|
+
debug_mode = kwargs.get("debug", False)
|
|
158
|
+
step_by_step = kwargs.get("step_by_step", False)
|
|
159
|
+
use_cache_arg = kwargs.get("use_cache", False)
|
|
160
|
+
no_cache_arg = kwargs.get("no_cache", False)
|
|
161
|
+
# wl test: default no cache
|
|
162
|
+
# wl test -s: default use cache
|
|
163
|
+
if no_cache_arg:
|
|
164
|
+
no_cache = True
|
|
165
|
+
elif use_cache_arg:
|
|
166
|
+
no_cache = False
|
|
167
|
+
else:
|
|
168
|
+
no_cache = not step_by_step # wl test: no cache, wl test -s: use cache
|
|
169
|
+
clean_cache = kwargs.get("clean_cache", False)
|
|
170
|
+
cache_info = kwargs.get("cache_info", False)
|
|
171
|
+
quiet = kwargs.get("quiet", False)
|
|
172
|
+
verbose = kwargs.get("verbose", False)
|
|
173
|
+
passed_args = kwargs.get("unknown_args", kwargs.get("args", []))
|
|
174
|
+
|
|
175
|
+
# 处理特殊情况:如果args中有'debug',将其移除并设置debug_mode为True
|
|
176
|
+
if "debug" in passed_args:
|
|
177
|
+
passed_args.remove("debug")
|
|
178
|
+
debug_mode = True
|
|
179
|
+
|
|
180
|
+
# 只在非测试环境中打印和记录信息
|
|
181
|
+
is_testing = "PYTEST_CURRENT_TEST" in os.environ
|
|
182
|
+
if not is_testing:
|
|
183
|
+
print(f"Debug mode: {debug_mode}")
|
|
184
|
+
# 传递concise参数,默认为False
|
|
185
|
+
self.progress_manager._log_report_mode(report_mode, False)
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
report_mode,
|
|
189
|
+
debug_mode,
|
|
190
|
+
step_by_step,
|
|
191
|
+
no_cache,
|
|
192
|
+
clean_cache,
|
|
193
|
+
cache_info,
|
|
194
|
+
quiet,
|
|
195
|
+
verbose,
|
|
196
|
+
passed_args,
|
|
197
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .coverage_parser import coverage_totals, file_entries, read_json
|
|
4
|
+
from .env_info import get_env_info
|
|
5
|
+
from .junit_parser import parse_junit, read_xml
|
|
6
|
+
from .markdown_utils import fmt_pct, write_md
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"coverage_totals",
|
|
10
|
+
"file_entries",
|
|
11
|
+
"fmt_pct",
|
|
12
|
+
"get_env_info",
|
|
13
|
+
"parse_junit",
|
|
14
|
+
"read_json",
|
|
15
|
+
"read_xml",
|
|
16
|
+
"write_md",
|
|
17
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def read_json(path: str) -> dict[str, Any]:
|
|
9
|
+
"""读取JSON文件并返回其内容。"""
|
|
10
|
+
p = Path(path)
|
|
11
|
+
if not p.exists():
|
|
12
|
+
return {}
|
|
13
|
+
try:
|
|
14
|
+
return cast("dict[str, Any]", json.loads(p.read_text(encoding="utf-8")))
|
|
15
|
+
except Exception:
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def coverage_totals(
|
|
20
|
+
data: dict[str, Any],
|
|
21
|
+
) -> tuple[float | None, float | None, int, int, int]:
|
|
22
|
+
"""计算覆盖率统计信息。"""
|
|
23
|
+
files = data.get("files", {})
|
|
24
|
+
num_files = len(files) if isinstance(files, dict) else 0
|
|
25
|
+
covered_lines = 0
|
|
26
|
+
total_lines = 0
|
|
27
|
+
covered_branches = 0
|
|
28
|
+
total_branches = 0
|
|
29
|
+
if isinstance(files, dict):
|
|
30
|
+
for info in files.values():
|
|
31
|
+
s = info.get("summary", {})
|
|
32
|
+
try:
|
|
33
|
+
covered_lines += int(s.get("covered_lines", 0))
|
|
34
|
+
total_lines += int(s.get("num_statements", s.get("num_lines", 0)))
|
|
35
|
+
covered_branches += int(s.get("covered_branches", 0))
|
|
36
|
+
total_branches += int(s.get("num_branches", 0))
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
line_pct = (covered_lines / total_lines * 100.0) if total_lines else None
|
|
40
|
+
branch_pct = (covered_branches / total_branches * 100.0) if total_branches else None
|
|
41
|
+
return (
|
|
42
|
+
line_pct,
|
|
43
|
+
branch_pct,
|
|
44
|
+
total_lines,
|
|
45
|
+
total_branches,
|
|
46
|
+
num_files,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def file_entries(
|
|
51
|
+
data: dict[str, Any], project_root: str
|
|
52
|
+
) -> list[tuple[str, float | None, float | None]]:
|
|
53
|
+
"""获取文件覆盖率条目列表。"""
|
|
54
|
+
files = data.get("files", {})
|
|
55
|
+
res: list[tuple[str, float | None, float | None]] = []
|
|
56
|
+
if isinstance(files, dict):
|
|
57
|
+
for path, info in files.items():
|
|
58
|
+
rel = path
|
|
59
|
+
try:
|
|
60
|
+
rel = str(
|
|
61
|
+
Path(path).resolve().relative_to(Path(project_root).resolve())
|
|
62
|
+
)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
s = info.get("summary", {})
|
|
66
|
+
|
|
67
|
+
# Get line coverage percentage
|
|
68
|
+
lp = s.get("percent_covered")
|
|
69
|
+
if lp is None:
|
|
70
|
+
lp = info.get("percent_covered")
|
|
71
|
+
# If still None, calculate from covered_lines and num_statements
|
|
72
|
+
if lp is None:
|
|
73
|
+
covered_lines = s.get("covered_lines", 0)
|
|
74
|
+
num_statements = s.get("num_statements", s.get("num_lines", 0))
|
|
75
|
+
if num_statements > 0:
|
|
76
|
+
lp = (covered_lines / num_statements) * 100
|
|
77
|
+
|
|
78
|
+
# Get branch coverage percentage
|
|
79
|
+
bp = s.get("percent_branches")
|
|
80
|
+
if bp is None:
|
|
81
|
+
bp = info.get("percent_branches")
|
|
82
|
+
# If still None, calculate from covered_branches and num_branches
|
|
83
|
+
if bp is None:
|
|
84
|
+
covered_branches = s.get("covered_branches", 0)
|
|
85
|
+
num_branches = s.get("num_branches", 0)
|
|
86
|
+
if num_branches > 0:
|
|
87
|
+
bp = (covered_branches / num_branches) * 100
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
lp_f = float(lp) if lp is not None else None
|
|
91
|
+
except Exception:
|
|
92
|
+
lp_f = None
|
|
93
|
+
try:
|
|
94
|
+
bp_f = float(bp) if bp is not None else None
|
|
95
|
+
except Exception:
|
|
96
|
+
bp_f = None
|
|
97
|
+
res.append((rel, lp_f, bp_f))
|
|
98
|
+
return res
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Try to import pytest for version information
|
|
9
|
+
_pytest: Any | None
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
_pytest = pytest
|
|
15
|
+
except ImportError:
|
|
16
|
+
_pytest = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_env_info() -> list[str]:
|
|
20
|
+
"""获取测试环境信息。"""
|
|
21
|
+
now = datetime.now().isoformat(timespec="seconds")
|
|
22
|
+
py_ver = sys.version.replace("\n", " ")
|
|
23
|
+
os_info = f"{platform.system()} {platform.release()} ({platform.machine()})"
|
|
24
|
+
|
|
25
|
+
# Get pytest version if available
|
|
26
|
+
if _pytest is not None:
|
|
27
|
+
pytest_ver = getattr(_pytest, "__version__", "unknown")
|
|
28
|
+
else:
|
|
29
|
+
pytest_ver = "unknown"
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
f"时间: {now}",
|
|
33
|
+
f"Python: {py_ver}",
|
|
34
|
+
f"OS: {os_info}",
|
|
35
|
+
f"pytest: {pytest_ver}",
|
|
36
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import xml.etree.ElementTree
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_xml(path: str) -> xml.etree.ElementTree.Element | None:
|
|
8
|
+
"""读取XML文件并返回其根元素。"""
|
|
9
|
+
p = Path(path)
|
|
10
|
+
if not p.exists():
|
|
11
|
+
return None
|
|
12
|
+
try:
|
|
13
|
+
tree = xml.etree.ElementTree.parse(str(p))
|
|
14
|
+
return tree.getroot()
|
|
15
|
+
except Exception:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_junit(
|
|
20
|
+
root: xml.etree.ElementTree.Element | None,
|
|
21
|
+
) -> tuple[int, int, int, int, int, float, list[dict[str, str]]]:
|
|
22
|
+
"""解析JUnit XML数据并返回测试统计信息。"""
|
|
23
|
+
if root is None:
|
|
24
|
+
return (0, 0, 0, 0, 0, 0.0, [])
|
|
25
|
+
suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
|
|
26
|
+
tests = failures = errors = skipped = warnings = 0
|
|
27
|
+
time = 0.0
|
|
28
|
+
cases: list[dict[str, str]] = []
|
|
29
|
+
for ts in suites:
|
|
30
|
+
tests += int(ts.attrib.get("tests", 0))
|
|
31
|
+
failures += int(ts.attrib.get("failures", 0))
|
|
32
|
+
errors += int(ts.attrib.get("errors", 0))
|
|
33
|
+
skipped += int(ts.attrib.get("skipped", 0))
|
|
34
|
+
# 增加warnings计数
|
|
35
|
+
warnings += int(ts.attrib.get("warnings", 0))
|
|
36
|
+
try:
|
|
37
|
+
time += float(ts.attrib.get("time", 0.0))
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
for tc in ts.findall("testcase"):
|
|
41
|
+
name = tc.attrib.get("name", "")
|
|
42
|
+
classname = tc.attrib.get("classname", "")
|
|
43
|
+
file_attr = tc.attrib.get("file", "")
|
|
44
|
+
text = ""
|
|
45
|
+
kind = ""
|
|
46
|
+
fail = tc.find("failure")
|
|
47
|
+
err = tc.find("error")
|
|
48
|
+
# 增加warning元素的查找
|
|
49
|
+
warn = tc.find("warning")
|
|
50
|
+
if fail is not None:
|
|
51
|
+
kind = "failure"
|
|
52
|
+
text = (
|
|
53
|
+
fail.attrib.get("message", "") + "\n" + (fail.text or "")
|
|
54
|
+
).strip()
|
|
55
|
+
elif err is not None:
|
|
56
|
+
kind = "error"
|
|
57
|
+
text = (err.attrib.get("message", "") + "\n" + (err.text or "")).strip()
|
|
58
|
+
elif warn is not None:
|
|
59
|
+
kind = "warning"
|
|
60
|
+
text = (
|
|
61
|
+
warn.attrib.get("message", "") + "\n" + (warn.text or "")
|
|
62
|
+
).strip()
|
|
63
|
+
if kind:
|
|
64
|
+
if len(text) > 1200:
|
|
65
|
+
text = text[:1200] + "\n..."
|
|
66
|
+
cases.append(
|
|
67
|
+
{
|
|
68
|
+
"name": name,
|
|
69
|
+
"classname": classname,
|
|
70
|
+
"file": file_attr,
|
|
71
|
+
"kind": kind,
|
|
72
|
+
"text": text,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
return (tests, failures, errors, skipped, warnings, time, cases)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fmt_pct(x: Any) -> str:
|
|
8
|
+
"""格式化百分比值。"""
|
|
9
|
+
try:
|
|
10
|
+
v = float(x)
|
|
11
|
+
return f"{v:.2f}%"
|
|
12
|
+
except Exception:
|
|
13
|
+
return "N/A"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_md(path: str, lines: list[str]) -> None:
|
|
17
|
+
"""将文本行写入Markdown文件。"""
|
|
18
|
+
p = Path(path)
|
|
19
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""The test commands subsystem's project-root **policy** entry point.
|
|
2
|
+
|
|
3
|
+
This is not a root resolver: the root module (:mod:`wl_cli.utils.root`) owns
|
|
4
|
+
*locating* a root. What lives here is the test subsystem's own answer to "where
|
|
5
|
+
does this subsystem's ``src/`` and ``tests/`` tree live", which is pytest's
|
|
6
|
+
rootdir question rather than a definition of the project root.
|
|
7
|
+
|
|
8
|
+
Two rules make it up, and both stay in the subsystem on purpose (issue #120's
|
|
9
|
+
ruling on #116's note_13750):
|
|
10
|
+
|
|
11
|
+
- the ``tests/``/``src/`` pre-check -- a directory that already has one of those
|
|
12
|
+
*is* the root, with no marker lookup at all. The root module's ``PROJECT`` kind
|
|
13
|
+
is marker-based instead, so the two rules genuinely differ and the pre-check
|
|
14
|
+
cannot be folded into it;
|
|
15
|
+
- the caller-side fallback -- when no marker exists the subsystem answers the
|
|
16
|
+
starting directory rather than "not found" (ADR 0006: the fallback is the
|
|
17
|
+
caller's policy, never ``resolve``'s).
|
|
18
|
+
|
|
19
|
+
Issue #110 introduced this policy twice: an un-anchored spelling that read the
|
|
20
|
+
process working directory and a mirrored anchored spelling, so a detector
|
|
21
|
+
anchored outside the CWD would not leak the CWD back in. The hard cut (#123)
|
|
22
|
+
merges them into this single ``start``-parameterised form; ``start=None`` means
|
|
23
|
+
the working directory, which is exactly what the un-anchored spelling
|
|
24
|
+
hard-coded, and every other behaviour is unchanged.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from ....utils.root import RootKind
|
|
33
|
+
from ....utils.root_fallback import resolve_root_or_start
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def project_root_for(start: Path | None = None) -> str:
|
|
37
|
+
"""The test subsystem's project root for *start*, as a string.
|
|
38
|
+
|
|
39
|
+
``start=None`` asks about the process working directory. The answer is
|
|
40
|
+
*start* itself when it already looks like a project (it has ``tests/`` or
|
|
41
|
+
``src/``); otherwise the root module's ``PROJECT`` root; and when neither
|
|
42
|
+
exists, *start*. Never returns ``None``.
|
|
43
|
+
|
|
44
|
+
The last step is the shared CLI-top-level fallback policy, not a private
|
|
45
|
+
copy of it: :func:`~wl_cli.utils.root_fallback.resolve_root_or_start` (#126).
|
|
46
|
+
Its answer is taken as ``str`` (this subsystem's contract is a string) and
|
|
47
|
+
``start`` is passed verbatim (this helper never canonicalised it), keeping
|
|
48
|
+
the answer byte-for-byte what the spelling it replaces returned.
|
|
49
|
+
"""
|
|
50
|
+
if start is None:
|
|
51
|
+
try:
|
|
52
|
+
start = Path.cwd()
|
|
53
|
+
except Exception:
|
|
54
|
+
return os.getcwd()
|
|
55
|
+
try:
|
|
56
|
+
if (start / "tests").exists() or (start / "src").exists():
|
|
57
|
+
return str(start)
|
|
58
|
+
# ADR 0006: 回退是调用方策略;唯一实现在 utils/root_fallback.py(#126)
|
|
59
|
+
return str(resolve_root_or_start(start, RootKind.PROJECT).path)
|
|
60
|
+
except Exception:
|
|
61
|
+
return str(start)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Services for update command.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from wl_cli.commands.update.services.backup_manager import BackupManager
|
|
6
|
+
from wl_cli.commands.update.services.component_manager import ComponentManager
|
|
7
|
+
from wl_cli.commands.update.services.conflict_resolver import (
|
|
8
|
+
ConflictResolution,
|
|
9
|
+
ConflictResolver,
|
|
10
|
+
)
|
|
11
|
+
from wl_cli.commands.update.services.vendors_updater import VendorsUpdater
|
|
12
|
+
from wl_cli.commands.update.services.version_tracker import VersionTracker
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ComponentManager",
|
|
16
|
+
"VersionTracker",
|
|
17
|
+
"VendorsUpdater",
|
|
18
|
+
"BackupManager",
|
|
19
|
+
"ConflictResolver",
|
|
20
|
+
"ConflictResolution",
|
|
21
|
+
]
|