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,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows update functionality for self update command.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
from ....utils.platform_adapter import PlatformAdapter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WindowsUpdate:
|
|
15
|
+
"""Windows-specific update functionality for the wl command."""
|
|
16
|
+
|
|
17
|
+
def handle_access_error(self, uv_path: str = "") -> None:
|
|
18
|
+
"""
|
|
19
|
+
Handle Windows access denied errors with either delayed update or manual instructions.
|
|
20
|
+
"""
|
|
21
|
+
from wl_cli.utils.logging import log_info_pair
|
|
22
|
+
|
|
23
|
+
if PlatformAdapter.is_windows():
|
|
24
|
+
log_info_pair(
|
|
25
|
+
"Detected file in use error on Windows. Trying delayed update mechanism...",
|
|
26
|
+
"在Windows上检测到文件被占用错误。尝试延迟更新机制...",
|
|
27
|
+
)
|
|
28
|
+
if not uv_path:
|
|
29
|
+
# ``or ""`` keeps the declared ``str`` type honest; ``which``
|
|
30
|
+
# returns ``None`` when uv is absent. Behaviour is unchanged --
|
|
31
|
+
# both ``None`` and ``""`` fail the truthiness test below and
|
|
32
|
+
# fall through to the "Failed to find uv executable" branch.
|
|
33
|
+
uv_path = shutil.which("uv") or ""
|
|
34
|
+
if uv_path:
|
|
35
|
+
env = os.environ.copy()
|
|
36
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
37
|
+
env["PYTHONLEGACYWINDOWSFSENCODING"] = "1"
|
|
38
|
+
self._run_delayed_update(uv_path, env)
|
|
39
|
+
return
|
|
40
|
+
else:
|
|
41
|
+
log_info_pair(
|
|
42
|
+
"Failed to find uv executable for delayed update.",
|
|
43
|
+
"无法找到uv可执行文件进行延迟更新。",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
log_info_pair(
|
|
47
|
+
"Error updating wl command: Access denied. This is a common issue on Windows when trying to update a running tool.",
|
|
48
|
+
"错误:更新wl命令失败: 权限被拒绝。这是在Windows上尝试更新正在运行的工具时的常见问题。",
|
|
49
|
+
)
|
|
50
|
+
log_info_pair(
|
|
51
|
+
"Please try one of the following solutions:", "请尝试以下解决方案之一:"
|
|
52
|
+
)
|
|
53
|
+
log_info_pair(
|
|
54
|
+
"1. Close all wl command windows and run 'wl self update' again",
|
|
55
|
+
" 关闭所有wl命令窗口,然后再次运行'wl self update'",
|
|
56
|
+
)
|
|
57
|
+
log_info_pair("2. Run the command as administrator", " 以管理员身份运行命令")
|
|
58
|
+
log_info_pair(
|
|
59
|
+
"3. Use 'uv tool install --editable .' directly",
|
|
60
|
+
" 直接使用'uv tool install --editable .'命令",
|
|
61
|
+
)
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
def _run_delayed_update(self, uv_path: str, env: dict) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Run delayed update on Windows by creating a separate script that executes after the main process exits.
|
|
67
|
+
"""
|
|
68
|
+
from wl_cli.utils.logging import log_info_pair
|
|
69
|
+
|
|
70
|
+
env_repr = repr(env)
|
|
71
|
+
uv_path_escaped = uv_path.replace("\\", "\\\\")
|
|
72
|
+
|
|
73
|
+
script_content = f"""
|
|
74
|
+
import subprocess
|
|
75
|
+
import time
|
|
76
|
+
import sys
|
|
77
|
+
import os
|
|
78
|
+
|
|
79
|
+
print("Waiting for main process to exit...")
|
|
80
|
+
print("等待主进程退出...")
|
|
81
|
+
time.sleep(3)
|
|
82
|
+
|
|
83
|
+
os.environ.update({env_repr})
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
print("Uninstalling existing wl_cli...")
|
|
87
|
+
print("正在卸载现有wl_cli...")
|
|
88
|
+
subprocess.run([\"{uv_path_escaped}\", \"tool\", \"uninstall\", \"wl_cli\"], check=False, capture_output=True, text=True, encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
print("Installing wl_cli...")
|
|
91
|
+
print("正在安装wl_cli...")
|
|
92
|
+
primary_cmd = [\"{uv_path_escaped}\", \"tool\", \"install\", \"--editable\", \".\"]
|
|
93
|
+
subprocess.run(primary_cmd, check=True, capture_output=False, text=True, encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
print("\\nWL command updated successfully!")
|
|
96
|
+
print("wl命令更新成功!")
|
|
97
|
+
print("You can close this window now.")
|
|
98
|
+
print("现在可以关闭此窗口。")
|
|
99
|
+
except subprocess.CalledProcessError as e:
|
|
100
|
+
print("\\nError updating wl command: " + str(e))
|
|
101
|
+
print("更新wl命令失败: " + str(e))
|
|
102
|
+
try:
|
|
103
|
+
print("\\nTrying alternative installation method...")
|
|
104
|
+
print("尝试备选安装方法...")
|
|
105
|
+
alternative_cmd = [\"{uv_path_escaped}\", \"tool\", \"install\", \"--editable\", \".\", \"--python-preference\", \"only-system\"]
|
|
106
|
+
subprocess.run(alternative_cmd, check=True, capture_output=False, text=True, encoding="utf-8")
|
|
107
|
+
print("\\nWL command updated successfully with alternative method!")
|
|
108
|
+
print("使用备选方法成功更新wl命令!")
|
|
109
|
+
except subprocess.CalledProcessError as alt_e:
|
|
110
|
+
print("\\nAlternative installation also failed: " + str(alt_e))
|
|
111
|
+
print("备选安装方法也失败: " + str(alt_e))
|
|
112
|
+
print("Please try manual installation:")
|
|
113
|
+
print("请尝试手动安装:")
|
|
114
|
+
print("uv tool install --editable .")
|
|
115
|
+
print("Press Enter to exit...")
|
|
116
|
+
input()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print("\\nUnexpected error: " + str(e))
|
|
119
|
+
print("意外错误: " + str(e))
|
|
120
|
+
print("Press Enter to exit...")
|
|
121
|
+
input()
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
with tempfile.NamedTemporaryFile(
|
|
125
|
+
mode="w", suffix=".py", delete=False, encoding="utf-8"
|
|
126
|
+
) as f:
|
|
127
|
+
f.write(script_content)
|
|
128
|
+
temp_script_path = f.name
|
|
129
|
+
|
|
130
|
+
log_info_pair(
|
|
131
|
+
f"Running delayed update script: {temp_script_path}",
|
|
132
|
+
f"正在运行延迟更新脚本: {temp_script_path}",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
subprocess.Popen(
|
|
137
|
+
[sys.executable, temp_script_path],
|
|
138
|
+
shell=True,
|
|
139
|
+
creationflags=getattr(subprocess, "CREATE_NEW_CONSOLE", 0),
|
|
140
|
+
)
|
|
141
|
+
log_info_pair(
|
|
142
|
+
"Delayed update started. The update will continue in a new window.",
|
|
143
|
+
"延迟更新已启动。更新将在新窗口中继续进行。",
|
|
144
|
+
)
|
|
145
|
+
sys.exit(0)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
log_info_pair(
|
|
148
|
+
f"Failed to start delayed update: {e}", f"启动延迟更新失败: {e}"
|
|
149
|
+
)
|
|
150
|
+
self._show_manual_instructions()
|
|
151
|
+
|
|
152
|
+
def _show_manual_instructions(self) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Show manual installation instructions to the user.
|
|
155
|
+
"""
|
|
156
|
+
from wl_cli.utils.logging import log_info_pair
|
|
157
|
+
|
|
158
|
+
log_info_pair(
|
|
159
|
+
"Error updating wl command: Access denied. This is a common issue on Windows when trying to update a running tool.",
|
|
160
|
+
"错误:更新wl命令失败: 权限被拒绝。这是在Windows上尝试更新正在运行的工具时的常见问题。",
|
|
161
|
+
)
|
|
162
|
+
log_info_pair(
|
|
163
|
+
"Please try one of the following solutions:", "请尝试以下解决方案之一:"
|
|
164
|
+
)
|
|
165
|
+
log_info_pair(
|
|
166
|
+
"1. Close all wl command windows and run 'wl self update' again",
|
|
167
|
+
" 关闭所有wl命令窗口,然后再次运行'wl self update'",
|
|
168
|
+
)
|
|
169
|
+
log_info_pair("2. Run the command as administrator", " 以管理员身份运行命令")
|
|
170
|
+
log_info_pair(
|
|
171
|
+
"3. Use 'uv tool install --editable .' directly",
|
|
172
|
+
" 直接使用'uv tool install --editable .'命令",
|
|
173
|
+
)
|
|
174
|
+
sys.exit(1)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Base dependency analyzer with shared AST-based dependency analysis logic."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .utils.project_root import project_root_for
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseDependencyAnalyzer:
|
|
11
|
+
"""Base class with shared dependency analysis logic using AST parsing.
|
|
12
|
+
|
|
13
|
+
Provides common dependency resolution methods that can be inherited by
|
|
14
|
+
classes that need dependency analysis functionality.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self, module_file_cache: dict[tuple[str, str], Path | None] | None = None
|
|
19
|
+
):
|
|
20
|
+
"""Initialize with optional module file cache.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
module_file_cache: Optional cache for found module files
|
|
24
|
+
Caches (module_name, src_dir_str) -> Path found result
|
|
25
|
+
"""
|
|
26
|
+
self._dependency_cache: dict[str, tuple[list[Path], float]] = {}
|
|
27
|
+
self._module_file_cache: dict[tuple[str, str], Path | None] = (
|
|
28
|
+
module_file_cache or {}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _get_test_dependencies(self, test_file: Path) -> list[Path]:
|
|
32
|
+
"""Get test file dependencies by analyzing imports, including indirect dependencies."""
|
|
33
|
+
# Check dependency cache first
|
|
34
|
+
cache_key = str(test_file)
|
|
35
|
+
if cache_key in self._dependency_cache:
|
|
36
|
+
# Cache entry stores (dependencies, mtime) tuple
|
|
37
|
+
cached_deps, cached_mtime = self._dependency_cache[cache_key]
|
|
38
|
+
# Check if file has been modified since caching
|
|
39
|
+
try:
|
|
40
|
+
current_mtime = os.path.getmtime(test_file)
|
|
41
|
+
# If mtime hasn't changed, use cached result
|
|
42
|
+
if abs(current_mtime - cached_mtime) < 1e-6:
|
|
43
|
+
return cached_deps
|
|
44
|
+
except OSError:
|
|
45
|
+
# If we can't stat the file, just use cached result
|
|
46
|
+
return cached_deps
|
|
47
|
+
|
|
48
|
+
dependencies: set[Path] = set()
|
|
49
|
+
# ADR 0006: 回退是调用方策略 —— 子系统策略helper 已显式兜底 CWD
|
|
50
|
+
project_root = project_root_for()
|
|
51
|
+
src_dir = Path(project_root) / "src"
|
|
52
|
+
test_dir = Path(project_root) / "tests"
|
|
53
|
+
|
|
54
|
+
# Analyze the test file itself
|
|
55
|
+
self._analyze_file_dependencies(test_file, dependencies, src_dir, test_dir)
|
|
56
|
+
|
|
57
|
+
# Cache the result with current mtime
|
|
58
|
+
try:
|
|
59
|
+
current_mtime = os.path.getmtime(test_file)
|
|
60
|
+
except OSError:
|
|
61
|
+
current_mtime = 0.0
|
|
62
|
+
self._dependency_cache[cache_key] = (list(dependencies), current_mtime)
|
|
63
|
+
|
|
64
|
+
# Simple LRU-style eviction: if cache exceeds 1000 entries, clear half
|
|
65
|
+
if len(self._dependency_cache) > 1000:
|
|
66
|
+
# Remove half the entries to keep cache size manageable
|
|
67
|
+
keys = list(self._dependency_cache.keys())
|
|
68
|
+
half = len(keys) // 2
|
|
69
|
+
for key in keys[:half]:
|
|
70
|
+
del self._dependency_cache[key]
|
|
71
|
+
|
|
72
|
+
return list(dependencies)
|
|
73
|
+
|
|
74
|
+
def _analyze_file_dependencies(
|
|
75
|
+
self, file_path: Path, dependencies: set[Path], src_dir: Path, test_dir: Path
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Recursively analyze dependencies for a file."""
|
|
78
|
+
try:
|
|
79
|
+
# Read and parse file
|
|
80
|
+
content = file_path.read_text(encoding="utf-8")
|
|
81
|
+
tree = ast.parse(content)
|
|
82
|
+
|
|
83
|
+
# Analyze imports
|
|
84
|
+
for node in ast.walk(tree):
|
|
85
|
+
if isinstance(node, ast.Import):
|
|
86
|
+
self._analyze_import_node(node, dependencies, src_dir, test_dir)
|
|
87
|
+
elif isinstance(node, ast.ImportFrom):
|
|
88
|
+
self._analyze_import_from_node(
|
|
89
|
+
node, file_path, dependencies, src_dir, test_dir
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
except (OSError, SyntaxError, TypeError):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def _analyze_import_node(
|
|
96
|
+
self, node: ast.Import, dependencies: set[Path], src_dir: Path, test_dir: Path
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Analyze an Import node and add dependencies."""
|
|
99
|
+
for alias in node.names:
|
|
100
|
+
module_name = alias.name
|
|
101
|
+
# First try to find in src directory
|
|
102
|
+
dep_file = self._find_module_file(module_name, src_dir)
|
|
103
|
+
# If not found, try in test directory
|
|
104
|
+
if not dep_file:
|
|
105
|
+
dep_file = self._find_module_file(module_name, test_dir)
|
|
106
|
+
if dep_file and dep_file not in dependencies:
|
|
107
|
+
dependencies.add(dep_file)
|
|
108
|
+
# Recursively analyze this dependency
|
|
109
|
+
self._analyze_file_dependencies(
|
|
110
|
+
dep_file, dependencies, src_dir, test_dir
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _analyze_import_from_node(
|
|
114
|
+
self,
|
|
115
|
+
node: ast.ImportFrom,
|
|
116
|
+
file_path: Path,
|
|
117
|
+
dependencies: set[Path],
|
|
118
|
+
src_dir: Path,
|
|
119
|
+
test_dir: Path,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Analyze an ImportFrom node and add dependencies."""
|
|
122
|
+
if node.module:
|
|
123
|
+
module_name = node.module
|
|
124
|
+
# First try to find in src directory
|
|
125
|
+
dep_file = self._find_module_file(module_name, src_dir)
|
|
126
|
+
# If not found, try in test directory
|
|
127
|
+
if not dep_file:
|
|
128
|
+
dep_file = self._find_module_file(module_name, test_dir)
|
|
129
|
+
if dep_file and dep_file not in dependencies:
|
|
130
|
+
dependencies.add(dep_file)
|
|
131
|
+
# Recursively analyze this dependency
|
|
132
|
+
self._analyze_file_dependencies(
|
|
133
|
+
dep_file, dependencies, src_dir, test_dir
|
|
134
|
+
)
|
|
135
|
+
elif node.level > 0:
|
|
136
|
+
# Handle relative imports
|
|
137
|
+
relative_path = Path(file_path.parent)
|
|
138
|
+
for _ in range(node.level - 1):
|
|
139
|
+
relative_path = relative_path.parent
|
|
140
|
+
for alias in node.names:
|
|
141
|
+
# Try to find the relative module
|
|
142
|
+
relative_module_path = relative_path / f"{alias.name}.py"
|
|
143
|
+
if relative_module_path.exists():
|
|
144
|
+
if relative_module_path not in dependencies:
|
|
145
|
+
dependencies.add(relative_module_path)
|
|
146
|
+
self._analyze_file_dependencies(
|
|
147
|
+
relative_module_path, dependencies, src_dir, test_dir
|
|
148
|
+
)
|
|
149
|
+
# Try to find __init__.py in the relative directory
|
|
150
|
+
relative_dir_path = relative_path / alias.name
|
|
151
|
+
init_file = relative_dir_path / "__init__.py"
|
|
152
|
+
if init_file.exists():
|
|
153
|
+
if init_file not in dependencies:
|
|
154
|
+
dependencies.add(init_file)
|
|
155
|
+
self._analyze_file_dependencies(
|
|
156
|
+
init_file, dependencies, src_dir, test_dir
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _find_module_file(self, module_name: str, src_dir: Path) -> Path | None:
|
|
160
|
+
"""Find module file from module name."""
|
|
161
|
+
# Create cache key
|
|
162
|
+
cache_key = (module_name, str(src_dir))
|
|
163
|
+
|
|
164
|
+
# Check cache first
|
|
165
|
+
if cache_key in self._module_file_cache:
|
|
166
|
+
return self._module_file_cache[cache_key]
|
|
167
|
+
|
|
168
|
+
# Convert module name to path
|
|
169
|
+
module_path = Path(module_name.replace(".", "/"))
|
|
170
|
+
|
|
171
|
+
# Look for __init__.py in module directory
|
|
172
|
+
init_file = src_dir / module_path / "__init__.py"
|
|
173
|
+
if init_file.exists():
|
|
174
|
+
self._module_file_cache[cache_key] = init_file
|
|
175
|
+
return init_file
|
|
176
|
+
|
|
177
|
+
# Look for .py file with module name
|
|
178
|
+
py_file = src_dir / f"{module_path}.py"
|
|
179
|
+
if py_file.exists():
|
|
180
|
+
self._module_file_cache[cache_key] = py_file
|
|
181
|
+
return py_file
|
|
182
|
+
|
|
183
|
+
# Try looking in parent directories for the module
|
|
184
|
+
current_dir = src_dir
|
|
185
|
+
while current_dir.parent != current_dir:
|
|
186
|
+
# Check if module exists in current directory
|
|
187
|
+
test_init_file = current_dir / module_path / "__init__.py"
|
|
188
|
+
if test_init_file.exists():
|
|
189
|
+
self._module_file_cache[cache_key] = test_init_file
|
|
190
|
+
return test_init_file
|
|
191
|
+
test_py_file = current_dir / f"{module_path}.py"
|
|
192
|
+
if test_py_file.exists():
|
|
193
|
+
self._module_file_cache[cache_key] = test_py_file
|
|
194
|
+
return test_py_file
|
|
195
|
+
current_dir = current_dir.parent
|
|
196
|
+
|
|
197
|
+
self._module_file_cache[cache_key] = None
|
|
198
|
+
return None
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Batch processor for step-by-step test execution."""
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
from ...utils.output_context import is_concise_mode, should_show_progress_bars
|
|
13
|
+
from .cache_manager import CacheManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BatchProcessor:
|
|
17
|
+
"""Processes test files in batches for step-by-step execution."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, cache_manager: CacheManager):
|
|
20
|
+
"""Initialize with cache manager dependency."""
|
|
21
|
+
self.cache_manager = cache_manager
|
|
22
|
+
|
|
23
|
+
def process_specified_files(
|
|
24
|
+
self,
|
|
25
|
+
quiet: bool,
|
|
26
|
+
verbose: bool,
|
|
27
|
+
test_files: list[Path],
|
|
28
|
+
python_executable: str,
|
|
29
|
+
project_root: str,
|
|
30
|
+
env: dict[str, str],
|
|
31
|
+
debug_mode: bool,
|
|
32
|
+
no_cache: bool,
|
|
33
|
+
execute_single: Callable[[Path], tuple[bool, str]],
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Process specified test files in step-by-step mode, stopping at first error."""
|
|
36
|
+
show_progress = should_show_progress_bars(
|
|
37
|
+
override_quiet=quiet, override_verbose=verbose
|
|
38
|
+
)
|
|
39
|
+
concise = is_concise_mode(override_quiet=quiet, override_verbose=verbose)
|
|
40
|
+
self.cache_manager.start_run()
|
|
41
|
+
|
|
42
|
+
if not test_files:
|
|
43
|
+
print("No test files found.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Calculate processing parameters
|
|
47
|
+
cpu_count = os.cpu_count() or 4
|
|
48
|
+
max_workers = min(16, cpu_count * 2)
|
|
49
|
+
batch_size = min(50, max_workers * 5)
|
|
50
|
+
|
|
51
|
+
error_occurred, error_message = self._process_batches(
|
|
52
|
+
test_files,
|
|
53
|
+
max_workers,
|
|
54
|
+
batch_size,
|
|
55
|
+
show_progress,
|
|
56
|
+
concise,
|
|
57
|
+
execute_single,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if error_occurred:
|
|
61
|
+
if show_progress:
|
|
62
|
+
print("\033[K", end="\r")
|
|
63
|
+
print(f"{error_message}")
|
|
64
|
+
print("Stopping at first error as requested.")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
print("All tests passed successfully!")
|
|
68
|
+
self.cache_manager.report_summary(test_files)
|
|
69
|
+
|
|
70
|
+
def process_all_files(
|
|
71
|
+
self,
|
|
72
|
+
quiet: bool,
|
|
73
|
+
verbose: bool,
|
|
74
|
+
test_files: list[Path],
|
|
75
|
+
python_executable: str,
|
|
76
|
+
project_root: str,
|
|
77
|
+
env: dict[str, str],
|
|
78
|
+
debug_mode: bool,
|
|
79
|
+
no_cache: bool,
|
|
80
|
+
execute_single: Callable[[Path], tuple[bool, str]],
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Process all test files in step-by-step mode."""
|
|
83
|
+
show_progress = should_show_progress_bars(
|
|
84
|
+
override_quiet=quiet, override_verbose=verbose
|
|
85
|
+
)
|
|
86
|
+
concise = is_concise_mode(override_quiet=quiet, override_verbose=verbose)
|
|
87
|
+
self.cache_manager.start_run()
|
|
88
|
+
|
|
89
|
+
# Calculate processing parameters
|
|
90
|
+
cpu_count = os.cpu_count() or 4
|
|
91
|
+
max_workers = min(16, cpu_count * 2)
|
|
92
|
+
batch_size = min(50, max_workers * 5)
|
|
93
|
+
|
|
94
|
+
error_occurred, error_message = self._process_batches(
|
|
95
|
+
test_files,
|
|
96
|
+
max_workers,
|
|
97
|
+
batch_size,
|
|
98
|
+
show_progress,
|
|
99
|
+
concise,
|
|
100
|
+
execute_single,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if error_occurred:
|
|
104
|
+
if show_progress:
|
|
105
|
+
print("\033[K", end="\r")
|
|
106
|
+
print(f"{error_message}")
|
|
107
|
+
print("Stopping at first error as requested.")
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
print("All tests passed successfully!")
|
|
111
|
+
self.cache_manager.report_summary(test_files)
|
|
112
|
+
|
|
113
|
+
def _process_batches(
|
|
114
|
+
self,
|
|
115
|
+
test_files: list[Path],
|
|
116
|
+
max_workers: int,
|
|
117
|
+
batch_size: int,
|
|
118
|
+
show_progress: bool,
|
|
119
|
+
concise: bool,
|
|
120
|
+
execute_single: Callable[[Path], tuple[bool, str]],
|
|
121
|
+
) -> tuple[bool, str]:
|
|
122
|
+
"""Process all test batches and return error status."""
|
|
123
|
+
error_occurred = False
|
|
124
|
+
error_message = ""
|
|
125
|
+
|
|
126
|
+
if show_progress:
|
|
127
|
+
with tqdm(
|
|
128
|
+
total=len(test_files),
|
|
129
|
+
unit="file",
|
|
130
|
+
dynamic_ncols=True,
|
|
131
|
+
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
|
|
132
|
+
) as pbar:
|
|
133
|
+
error_occurred, error_message = self._process_batches_with_progress(
|
|
134
|
+
test_files,
|
|
135
|
+
max_workers,
|
|
136
|
+
batch_size,
|
|
137
|
+
pbar,
|
|
138
|
+
concise,
|
|
139
|
+
execute_single,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
error_occurred, error_message, _ = self._process_batches_without_progress(
|
|
144
|
+
test_files,
|
|
145
|
+
max_workers,
|
|
146
|
+
batch_size,
|
|
147
|
+
concise,
|
|
148
|
+
execute_single,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return error_occurred, error_message
|
|
152
|
+
|
|
153
|
+
def _process_batches_with_progress(
|
|
154
|
+
self,
|
|
155
|
+
test_files: list[Path],
|
|
156
|
+
max_workers: int,
|
|
157
|
+
batch_size: int,
|
|
158
|
+
pbar: tqdm,
|
|
159
|
+
_concise_mode: bool,
|
|
160
|
+
execute_single: Callable[[Path], tuple[bool, str]],
|
|
161
|
+
) -> tuple[bool, str]:
|
|
162
|
+
"""Process batches with progress bar."""
|
|
163
|
+
error_occurred = False
|
|
164
|
+
error_message = ""
|
|
165
|
+
|
|
166
|
+
for i in range(0, len(test_files), batch_size):
|
|
167
|
+
batch_files = test_files[i : i + batch_size]
|
|
168
|
+
error_occurred, error_message = self._process_single_batch(
|
|
169
|
+
batch_files,
|
|
170
|
+
max_workers,
|
|
171
|
+
pbar,
|
|
172
|
+
_concise_mode,
|
|
173
|
+
execute_single,
|
|
174
|
+
)
|
|
175
|
+
if error_occurred:
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
return error_occurred, error_message
|
|
179
|
+
|
|
180
|
+
def _process_batches_without_progress(
|
|
181
|
+
self,
|
|
182
|
+
test_files: list[Path],
|
|
183
|
+
max_workers: int,
|
|
184
|
+
batch_size: int,
|
|
185
|
+
concise: bool,
|
|
186
|
+
execute_single: Callable[[Path], tuple[bool, str]],
|
|
187
|
+
) -> tuple[bool, str, int]:
|
|
188
|
+
"""Process batches without progress bar."""
|
|
189
|
+
error_occurred = False
|
|
190
|
+
error_message = ""
|
|
191
|
+
processed = 0
|
|
192
|
+
|
|
193
|
+
for i in range(0, len(test_files), batch_size):
|
|
194
|
+
batch_files = test_files[i : i + batch_size]
|
|
195
|
+
error_occurred, error_message = self._process_single_batch(
|
|
196
|
+
batch_files,
|
|
197
|
+
max_workers,
|
|
198
|
+
None,
|
|
199
|
+
concise,
|
|
200
|
+
execute_single,
|
|
201
|
+
)
|
|
202
|
+
processed += len(batch_files)
|
|
203
|
+
if not concise:
|
|
204
|
+
print(f"[{processed}/{len(test_files)}]", end=" ", flush=True)
|
|
205
|
+
if error_occurred:
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
return error_occurred, error_message, processed
|
|
209
|
+
|
|
210
|
+
def _process_single_batch(
|
|
211
|
+
self,
|
|
212
|
+
batch_files: list[Path],
|
|
213
|
+
max_workers: int,
|
|
214
|
+
pbar: tqdm | None,
|
|
215
|
+
_concise_mode: bool,
|
|
216
|
+
execute_single: Callable[[Path], tuple[bool, str]],
|
|
217
|
+
) -> tuple[bool, str]:
|
|
218
|
+
"""Process a single batch of test files."""
|
|
219
|
+
error_occurred = False
|
|
220
|
+
error_message = ""
|
|
221
|
+
|
|
222
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
223
|
+
future_to_file = {
|
|
224
|
+
executor.submit(execute_single, test_file): test_file
|
|
225
|
+
for test_file in batch_files
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for future in concurrent.futures.as_completed(future_to_file):
|
|
229
|
+
test_file = future_to_file[future]
|
|
230
|
+
test_success, error_msg = self._process_future_result(
|
|
231
|
+
future, test_file, pbar
|
|
232
|
+
)
|
|
233
|
+
if not test_success:
|
|
234
|
+
error_occurred = True
|
|
235
|
+
error_message = f"Test failed for file: {test_file}\n{error_msg}"
|
|
236
|
+
for f in future_to_file:
|
|
237
|
+
if not f.done():
|
|
238
|
+
f.cancel()
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
return error_occurred, error_message
|
|
242
|
+
|
|
243
|
+
def _process_future_result(
|
|
244
|
+
self,
|
|
245
|
+
future: concurrent.futures.Future,
|
|
246
|
+
test_file: Path,
|
|
247
|
+
pbar: tqdm | None,
|
|
248
|
+
) -> tuple[bool, str]:
|
|
249
|
+
"""Process the result of a future."""
|
|
250
|
+
try:
|
|
251
|
+
test_success, error_msg = future.result()
|
|
252
|
+
if pbar:
|
|
253
|
+
truncated_name = self._truncate_filename(test_file.name)
|
|
254
|
+
self._update_test_progress(pbar, truncated_name, test_file)
|
|
255
|
+
return test_success, error_msg
|
|
256
|
+
except Exception as e:
|
|
257
|
+
error_message = f"Error running test for file {test_file}: {e}"
|
|
258
|
+
return False, error_message
|
|
259
|
+
|
|
260
|
+
def _truncate_filename(self, filename: str) -> str:
|
|
261
|
+
"""Truncate filename to fit in progress bar."""
|
|
262
|
+
max_len = 28
|
|
263
|
+
if len(filename) > max_len:
|
|
264
|
+
return filename[: max_len - 3] + "..."
|
|
265
|
+
return filename
|
|
266
|
+
|
|
267
|
+
def _update_test_progress(
|
|
268
|
+
self,
|
|
269
|
+
pbar: tqdm,
|
|
270
|
+
truncated_name: str,
|
|
271
|
+
test_file: Path,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Update progress bar for test processing."""
|
|
274
|
+
if test_file in self.cache_manager.cache_hit_files:
|
|
275
|
+
pbar.set_description(f"Testing {truncated_name:<28} [cached]")
|
|
276
|
+
else:
|
|
277
|
+
pbar.set_description(f"Testing {truncated_name:<28}")
|
|
278
|
+
pbar.update(1)
|