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,191 @@
|
|
|
1
|
+
"""Format coordinator module."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ...utils.output_context import get_output_config
|
|
7
|
+
from .format_outcome import FormatOutcome
|
|
8
|
+
from .format_path_handler import FormatPathHandler
|
|
9
|
+
from .format_reporter import FormatReporter
|
|
10
|
+
from .python_formatter import RuffCheckVerdict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FormatCoordinator:
|
|
14
|
+
"""Coordinator for format command."""
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
"""Initialize the format coordinator.
|
|
18
|
+
|
|
19
|
+
The ``-> None`` keeps mypy checking this body: without it the two
|
|
20
|
+
collaborators stay ``Any`` and the typed aliases below would silently
|
|
21
|
+
return unchecked values (the same trap ``LintCommand.__init__`` notes).
|
|
22
|
+
"""
|
|
23
|
+
self.path_handler = FormatPathHandler()
|
|
24
|
+
self.reporter = FormatReporter()
|
|
25
|
+
|
|
26
|
+
def execute(self, *args, **kwargs) -> int:
|
|
27
|
+
"""Execute the format command; returns the process exit code."""
|
|
28
|
+
return self._execute_internal(**kwargs)
|
|
29
|
+
|
|
30
|
+
def run(self, *args, **kwargs) -> int:
|
|
31
|
+
"""Run the format command (alias for execute to maintain compatibility)."""
|
|
32
|
+
return self._execute_internal(**kwargs)
|
|
33
|
+
|
|
34
|
+
def _execute_internal(self, **kwargs) -> int:
|
|
35
|
+
"""Internal method to execute the format command.
|
|
36
|
+
|
|
37
|
+
Returns ``0`` when the run is green (or ``--report``/``wl lint`` own the
|
|
38
|
+
exit code) and ``1`` when a formatting failure must propagate (#164).
|
|
39
|
+
"""
|
|
40
|
+
quiet = kwargs.get("quiet", False)
|
|
41
|
+
# Auto-detected concise mode (non-TTY / CI / Claude) must not hide a
|
|
42
|
+
# failure's diagnostics -- only an explicit ``-q/--quiet`` may (F1).
|
|
43
|
+
requested_quiet = bool(quiet)
|
|
44
|
+
verbose = kwargs.get("verbose", False)
|
|
45
|
+
unsafe = kwargs.get("unsafe", True) # Default to True as per specifications
|
|
46
|
+
report = kwargs.get("report", False)
|
|
47
|
+
step_by_step = kwargs.get("step_by_step", False)
|
|
48
|
+
paths = kwargs.get("paths", [])
|
|
49
|
+
for_lint = kwargs.get("for_lint", False)
|
|
50
|
+
# The directory the scope means (#188). ``wl lint`` passes its root
|
|
51
|
+
# down, so a prefix started from a subdirectory (or from anywhere the
|
|
52
|
+
# CLI was invoked) formats ``root/src`` instead of resolving ``src``
|
|
53
|
+
# against the process cwd and printing "Path src does not exist".
|
|
54
|
+
# ``None`` keeps the historical cwd scope for plain ``wl format``.
|
|
55
|
+
project_root = kwargs.get("project_root")
|
|
56
|
+
|
|
57
|
+
# Auto-detect quiet mode from output context if not explicitly set
|
|
58
|
+
if not quiet:
|
|
59
|
+
# If not explicitly quiet, check if we're in concise mode
|
|
60
|
+
config = get_output_config(override_quiet=quiet, override_verbose=verbose)
|
|
61
|
+
if config.concise_mode:
|
|
62
|
+
quiet = True
|
|
63
|
+
|
|
64
|
+
# Handle --no-unsafe flag if present
|
|
65
|
+
if "no_unsafe" in kwargs:
|
|
66
|
+
unsafe = not kwargs["no_unsafe"]
|
|
67
|
+
|
|
68
|
+
env = os.environ.copy()
|
|
69
|
+
current_path = Path(project_root) if project_root is not None else Path.cwd()
|
|
70
|
+
# A caller that pinned the root gets its explicit paths anchored there
|
|
71
|
+
# (#188): the path handler probes ``Path(p).exists()``, so a bare
|
|
72
|
+
# ``src`` from a different cwd would still be dropped. Absolute paths
|
|
73
|
+
# mean the same directory to the handler and to every ruff process.
|
|
74
|
+
if project_root is not None and paths:
|
|
75
|
+
paths = [
|
|
76
|
+
p if Path(p).is_absolute() else str(current_path / p) for p in paths
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# Handle report generation
|
|
80
|
+
if report:
|
|
81
|
+
self.generate_format_report(quiet, unsafe, paths, env)
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
outcome: FormatOutcome
|
|
86
|
+
if paths:
|
|
87
|
+
outcome = self.format_specified_paths(
|
|
88
|
+
paths, env, quiet, unsafe, step_by_step, for_lint
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
outcome = self.format_defaults(
|
|
92
|
+
current_path, env, quiet, unsafe, for_lint, step_by_step
|
|
93
|
+
)
|
|
94
|
+
if outcome.ok:
|
|
95
|
+
if not quiet:
|
|
96
|
+
print("Formatting completed.")
|
|
97
|
+
return outcome.exit_code
|
|
98
|
+
if for_lint:
|
|
99
|
+
return self._lint_prefix_exit_code(outcome, quiet, requested_quiet)
|
|
100
|
+
if not requested_quiet:
|
|
101
|
+
self._render_failures(outcome)
|
|
102
|
+
return outcome.exit_code
|
|
103
|
+
except Exception as e:
|
|
104
|
+
if not quiet:
|
|
105
|
+
print(f"Warning: Error occurred during formatting: {e}")
|
|
106
|
+
return 1
|
|
107
|
+
|
|
108
|
+
def _lint_prefix_exit_code(
|
|
109
|
+
self, outcome: FormatOutcome, quiet: bool, requested_quiet: bool
|
|
110
|
+
) -> int:
|
|
111
|
+
"""Map a ``for_lint`` outcome to the prefix's exit code (#166).
|
|
112
|
+
|
|
113
|
+
``wl lint`` runs format as a prefix. A ``ruff``/``tool_missing``
|
|
114
|
+
failure means the files could not be formatted, so it stops lint
|
|
115
|
+
before ruff re-runs and renders its diagnostics.
|
|
116
|
+
|
|
117
|
+
Two verdicts stay non-fatal, because lint itself owns the authoritative
|
|
118
|
+
run for each:
|
|
119
|
+
|
|
120
|
+
* a ``pre_commit``-only verdict (#149/D3). Since #180 the prefix does
|
|
121
|
+
not run pre-commit at all for lint, so this branch now only guards a
|
|
122
|
+
``pre_commit`` failure such as one forwarded by a caller that still
|
|
123
|
+
produces it -- the exit contract is unchanged.
|
|
124
|
+
* a :class:`RuffCheckVerdict`-only outcome (#188): ``ruff check --fix``
|
|
125
|
+
exiting ``1`` means *some violations were not auto-fixable*, which is
|
|
126
|
+
the very thing lint's own ruff run reports. Aborting here is what
|
|
127
|
+
used to skip ruff's structured run, mypy, pre-commit and the report
|
|
128
|
+
whenever the report was most needed.
|
|
129
|
+
"""
|
|
130
|
+
fatal = tuple(
|
|
131
|
+
failure
|
|
132
|
+
for failure in outcome.failures
|
|
133
|
+
if failure.kind != "pre_commit"
|
|
134
|
+
and not isinstance(failure, RuffCheckVerdict)
|
|
135
|
+
)
|
|
136
|
+
if fatal:
|
|
137
|
+
if not requested_quiet:
|
|
138
|
+
self._render_failures(outcome)
|
|
139
|
+
return outcome.exit_code
|
|
140
|
+
if not quiet:
|
|
141
|
+
print("Formatting completed.")
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
def _render_failures(self, outcome: FormatOutcome) -> None:
|
|
145
|
+
"""Print the failures that made the run red (F1).
|
|
146
|
+
|
|
147
|
+
``format_python_file`` captures ruff's output (its quiet flag is
|
|
148
|
+
forced), so without this the user saw ``Formatting code...`` followed
|
|
149
|
+
by exit 1 and nothing else. Directory runs may already have streamed
|
|
150
|
+
the tool output; their message then falls back to a one-line
|
|
151
|
+
``<step> failed with return code N``, so nothing is duplicated.
|
|
152
|
+
|
|
153
|
+
``pre_commit`` details are the exception (#172): the path handler
|
|
154
|
+
already printed every failing block, from the same ``failed_blocks``
|
|
155
|
+
the verdict was computed from, at the moment it read the run. Printing
|
|
156
|
+
them again here was the duplicate the ticket is about; the failure
|
|
157
|
+
line and message below still say which run went red.
|
|
158
|
+
"""
|
|
159
|
+
for failure in outcome.failures:
|
|
160
|
+
print(f"Formatting failed ({failure.kind}): {failure.path}")
|
|
161
|
+
if failure.message:
|
|
162
|
+
print(failure.message)
|
|
163
|
+
if failure.kind == "pre_commit":
|
|
164
|
+
continue
|
|
165
|
+
for detail in failure.details:
|
|
166
|
+
print(detail)
|
|
167
|
+
|
|
168
|
+
# 添加别名方法以匹配测试期望的接口
|
|
169
|
+
def format_specified_paths(
|
|
170
|
+
self, paths, env, quiet, unsafe=False, step_by_step=False, for_lint=False
|
|
171
|
+
) -> FormatOutcome:
|
|
172
|
+
"""Alias for path_handler.format_specified_paths.
|
|
173
|
+
|
|
174
|
+
``for_lint`` reaches the handler so the ``wl lint`` prefix skips
|
|
175
|
+
pre-commit for explicit paths too (#180), not only for the defaults.
|
|
176
|
+
"""
|
|
177
|
+
return self.path_handler.format_specified_paths(
|
|
178
|
+
paths, env, quiet, unsafe, step_by_step, for_lint
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def format_defaults(
|
|
182
|
+
self, current_path, env, quiet, unsafe, for_lint, step_by_step=False
|
|
183
|
+
) -> FormatOutcome:
|
|
184
|
+
"""Alias for path_handler.format_defaults."""
|
|
185
|
+
return self.path_handler.format_defaults(
|
|
186
|
+
current_path, env, quiet, unsafe, for_lint, step_by_step
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def generate_format_report(self, quiet, unsafe, paths, env):
|
|
190
|
+
"""Alias for reporter.generate_format_report."""
|
|
191
|
+
self.reporter.generate_format_report(quiet, unsafe, paths, env)
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Format executor module."""
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import os
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .format_cache_adapter import FormatCacheAdapter
|
|
9
|
+
from .format_outcome import FormatFailure
|
|
10
|
+
from .format_progress import FormatProgress
|
|
11
|
+
from .python_formatter import (
|
|
12
|
+
FormatCommandResult,
|
|
13
|
+
RuffCheckVerdict,
|
|
14
|
+
_run_format_command,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FormatExecutor:
|
|
19
|
+
"""Executor for format operations."""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.cache = FormatCacheAdapter()
|
|
23
|
+
self.progress = FormatProgress()
|
|
24
|
+
self._has_psutil = False
|
|
25
|
+
try:
|
|
26
|
+
import psutil
|
|
27
|
+
|
|
28
|
+
self._has_psutil = True
|
|
29
|
+
self._psutil = psutil
|
|
30
|
+
except ImportError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def get_optimal_workers(self) -> int:
|
|
34
|
+
"""Get optimal number of workers based on system resources."""
|
|
35
|
+
# Get CPU core count
|
|
36
|
+
cpu_count = os.cpu_count()
|
|
37
|
+
|
|
38
|
+
# Handle case where cpu_count is None
|
|
39
|
+
if cpu_count is None:
|
|
40
|
+
return 4
|
|
41
|
+
|
|
42
|
+
# Calculate base workers based on CPU cores
|
|
43
|
+
base_workers = min(32, cpu_count * 2)
|
|
44
|
+
|
|
45
|
+
# Adjust based on available memory if psutil is available
|
|
46
|
+
if self._has_psutil:
|
|
47
|
+
try:
|
|
48
|
+
# Get available memory in GB
|
|
49
|
+
available_memory = self._psutil.virtual_memory().available / (1024**3)
|
|
50
|
+
|
|
51
|
+
# Adjust based on available memory
|
|
52
|
+
if available_memory < 2:
|
|
53
|
+
# Low memory, reduce workers
|
|
54
|
+
return max(1, base_workers // 2)
|
|
55
|
+
elif available_memory < 4:
|
|
56
|
+
# Moderate memory, use base workers
|
|
57
|
+
return base_workers
|
|
58
|
+
else:
|
|
59
|
+
# Sufficient memory, can increase workers slightly
|
|
60
|
+
return min(32, base_workers * 2)
|
|
61
|
+
except Exception:
|
|
62
|
+
# If psutil call fails, fall back to base workers
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
# Fall back to CPU-based calculation
|
|
66
|
+
return base_workers
|
|
67
|
+
|
|
68
|
+
def get_optimal_batch_size(self, workers: int) -> int:
|
|
69
|
+
"""Get optimal batch size based on number of workers."""
|
|
70
|
+
# Calculate base batch size
|
|
71
|
+
base_batch_size = workers * 10
|
|
72
|
+
|
|
73
|
+
# Cap at 100 to avoid excessive memory usage
|
|
74
|
+
return min(100, base_batch_size)
|
|
75
|
+
|
|
76
|
+
def calculate_resource_based_settings(self) -> tuple[int, int]:
|
|
77
|
+
"""Calculate resource-based settings for parallel processing."""
|
|
78
|
+
workers = self.get_optimal_workers()
|
|
79
|
+
batch_size = self.get_optimal_batch_size(workers)
|
|
80
|
+
return workers, batch_size
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _ruff_failure(
|
|
84
|
+
path_obj: Path, result: FormatCommandResult, step: str
|
|
85
|
+
) -> FormatFailure:
|
|
86
|
+
"""Classify one failed ruff command into the failure vocabulary."""
|
|
87
|
+
if result.tool_missing or result.launcher_failure is not None:
|
|
88
|
+
# #200 NIT-2: name the actual startup failure. ``tool_missing`` is
|
|
89
|
+
# the closed vocabulary's only "never ran" slot, but saying "ruff
|
|
90
|
+
# could not be launched" for a uv failure reads as "install ruff";
|
|
91
|
+
# a uv launcher failure gets its own wording, and a missing
|
|
92
|
+
# executable keeps the launch error it actually raised (F6: no
|
|
93
|
+
# over-attribution).
|
|
94
|
+
if result.launcher_failure is not None:
|
|
95
|
+
message = (
|
|
96
|
+
f"the uv launcher failed before ruff ran ({step}): "
|
|
97
|
+
f"{result.launcher_failure}"
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
reason = result.launch_error or f"exit code {result.returncode}"
|
|
101
|
+
message = f"ruff could not be started ({step}): {reason}"
|
|
102
|
+
return FormatFailure(
|
|
103
|
+
path=path_obj,
|
|
104
|
+
kind="tool_missing",
|
|
105
|
+
message=message,
|
|
106
|
+
)
|
|
107
|
+
message = result.output.strip() or (
|
|
108
|
+
f"{step} failed with return code {result.returncode}"
|
|
109
|
+
)
|
|
110
|
+
return FormatFailure(path=path_obj, kind="ruff", message=message)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _ruff_check_failure(
|
|
114
|
+
path_obj: Path, result: FormatCommandResult
|
|
115
|
+
) -> FormatFailure:
|
|
116
|
+
"""Classify a failed ``ruff check`` run on one explicit file (#196).
|
|
117
|
+
|
|
118
|
+
The explicit single-file entry used to package *every* non-zero
|
|
119
|
+
``ruff check`` as a plain formatter failure, so ``wl lint foo.py``
|
|
120
|
+
stopped at its format prefix and never reached its own ruff run,
|
|
121
|
+
mypy, pre-commit or the report -- the same early exit #188 fixed for
|
|
122
|
+
directory and default scopes.
|
|
123
|
+
|
|
124
|
+
Exit ``1`` from a ruff that *did* run means "violations remain after
|
|
125
|
+
the fixes": that is a lint verdict, carried as
|
|
126
|
+
:class:`RuffCheckVerdict`, which the coordinator lets the lint run
|
|
127
|
+
judge. This deliberately includes a file ruff cannot parse -- it
|
|
128
|
+
reports ``invalid-syntax`` on stdout with rc1, so the syntax error is a
|
|
129
|
+
check verdict here and ``ruff format`` never runs for it (#200).
|
|
130
|
+
|
|
131
|
+
Anything else -- a launcher that never ran, ``uv`` itself failing with
|
|
132
|
+
rc1 (:attr:`FormatCommandResult.launcher_failure`, #200), or exit
|
|
133
|
+
``>= 2`` (ruff refused the config/arguments) -- is still a genuine
|
|
134
|
+
formatter failure and still stops the lint prefix.
|
|
135
|
+
"""
|
|
136
|
+
if (
|
|
137
|
+
not result.tool_missing
|
|
138
|
+
and result.launcher_failure is None
|
|
139
|
+
and result.returncode == 1
|
|
140
|
+
):
|
|
141
|
+
message = result.output.strip() or "ruff check left unfixed violations"
|
|
142
|
+
return RuffCheckVerdict(path=path_obj, kind="ruff", message=message)
|
|
143
|
+
return FormatExecutor._ruff_failure(path_obj, result, "ruff check")
|
|
144
|
+
|
|
145
|
+
def format_python_file(
|
|
146
|
+
self, path_obj: Path, original_path: str, env: dict, quiet: bool, unsafe: bool
|
|
147
|
+
) -> FormatFailure | None:
|
|
148
|
+
"""Format a single Python file; ``None`` means it succeeded (#164)."""
|
|
149
|
+
try:
|
|
150
|
+
# In step-by-step mode, we want to suppress all output except errors
|
|
151
|
+
# But we still need to capture error output to display it later
|
|
152
|
+
step_by_step_quiet = quiet or True # Always quiet in step-by-step mode
|
|
153
|
+
|
|
154
|
+
# Format with ruff check
|
|
155
|
+
ruff_check_cmd = ["uv", "run", "ruff", "check", "--fix"]
|
|
156
|
+
if unsafe:
|
|
157
|
+
ruff_check_cmd.append("--unsafe-fixes")
|
|
158
|
+
ruff_check_cmd.append(str(path_obj))
|
|
159
|
+
|
|
160
|
+
check_result = _run_format_command(
|
|
161
|
+
ruff_check_cmd, env, step_by_step_quiet, passthrough=False
|
|
162
|
+
)
|
|
163
|
+
if not check_result.ok:
|
|
164
|
+
# In step-by-step mode, we return the error output
|
|
165
|
+
# It will be displayed by the caller. A check run that left
|
|
166
|
+
# unfixable violations is a lint verdict, not a format
|
|
167
|
+
# failure (#196): ``wl lint``'s own ruff run owns it.
|
|
168
|
+
return self._ruff_check_failure(path_obj, check_result)
|
|
169
|
+
|
|
170
|
+
# Format with ruff format
|
|
171
|
+
format_result = _run_format_command(
|
|
172
|
+
[
|
|
173
|
+
"uv",
|
|
174
|
+
"run",
|
|
175
|
+
"ruff",
|
|
176
|
+
"format",
|
|
177
|
+
str(path_obj),
|
|
178
|
+
],
|
|
179
|
+
env,
|
|
180
|
+
step_by_step_quiet,
|
|
181
|
+
passthrough=False,
|
|
182
|
+
)
|
|
183
|
+
if not format_result.ok:
|
|
184
|
+
# In step-by-step mode, we return the error output
|
|
185
|
+
# It will be displayed by the caller
|
|
186
|
+
return self._ruff_failure(path_obj, format_result, "ruff format")
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
except Exception as e:
|
|
190
|
+
# In step-by-step mode, we return the error message
|
|
191
|
+
# It will be displayed by the caller
|
|
192
|
+
return FormatFailure(
|
|
193
|
+
path=path_obj,
|
|
194
|
+
kind="ruff",
|
|
195
|
+
message=f"Warning: Failed to format {original_path}: {e}",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _as_failure(result: object, file_path: Path) -> FormatFailure | None:
|
|
200
|
+
"""Normalise ``format_python_file``'s answer to ``failure | None``.
|
|
201
|
+
|
|
202
|
+
``None`` is success under the #164 contract. The tuple/bool branches
|
|
203
|
+
keep older test doubles (which still answer ``(bool, message)``) usable;
|
|
204
|
+
production only ever takes the ``FormatFailure | None`` path.
|
|
205
|
+
"""
|
|
206
|
+
if result is None or isinstance(result, FormatFailure):
|
|
207
|
+
return result
|
|
208
|
+
if isinstance(result, tuple):
|
|
209
|
+
success, message = result
|
|
210
|
+
return None if success else FormatFailure(file_path, "ruff", message)
|
|
211
|
+
if isinstance(result, bool):
|
|
212
|
+
return None if result else FormatFailure(file_path, "ruff", "")
|
|
213
|
+
# F2: fail *closed*. An unrecognised return type is a contract breach,
|
|
214
|
+
# not evidence of success -- silently treating it as green is a
|
|
215
|
+
# fail-open channel into the exit code.
|
|
216
|
+
raise TypeError(
|
|
217
|
+
"format_python_file returned "
|
|
218
|
+
f"{type(result).__name__}; expected FormatFailure | None"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def process_single_file(
|
|
222
|
+
self,
|
|
223
|
+
file_path: Path,
|
|
224
|
+
env: dict,
|
|
225
|
+
quiet: bool,
|
|
226
|
+
unsafe: bool,
|
|
227
|
+
cache: dict[str, bool],
|
|
228
|
+
cache_hits: list[int],
|
|
229
|
+
) -> tuple[FormatFailure | None, Path, str, str]:
|
|
230
|
+
"""Process a single file with caching; first item is the failure.
|
|
231
|
+
|
|
232
|
+
``cache`` is only a per-run memo of the keys recorded during this run --
|
|
233
|
+
the durable cache is the ledger, so a hit survives across processes.
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
# Check if we're in test environment
|
|
237
|
+
is_test = "PYTEST_CURRENT_TEST" in os.environ
|
|
238
|
+
|
|
239
|
+
# Skip caching in test environment to speed up tests
|
|
240
|
+
if not is_test:
|
|
241
|
+
# Read file content for cache key generation with UTF-8 encoding
|
|
242
|
+
content = file_path.read_text(encoding="utf-8")
|
|
243
|
+
cache_key, cached_hit = self.cache.lookup(file_path, content)
|
|
244
|
+
|
|
245
|
+
# Check if result is in the ledger or in this run's memo
|
|
246
|
+
if cached_hit or cache_key in cache:
|
|
247
|
+
cache_hits[0] += 1
|
|
248
|
+
return None, file_path, "cache", ""
|
|
249
|
+
else:
|
|
250
|
+
cache_key = None
|
|
251
|
+
|
|
252
|
+
# Format the file
|
|
253
|
+
failure = self._as_failure(
|
|
254
|
+
self.format_python_file(file_path, str(file_path), env, quiet, unsafe),
|
|
255
|
+
file_path,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Record the result if successful and not in test environment
|
|
259
|
+
if failure is None and cache_key and not is_test:
|
|
260
|
+
cache[cache_key] = True
|
|
261
|
+
self.cache.record(cache_key)
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
failure,
|
|
265
|
+
file_path,
|
|
266
|
+
"processed" if failure is None else "failed",
|
|
267
|
+
failure.message if failure is not None else "",
|
|
268
|
+
)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
error_message = f"Error processing {file_path}: {e}"
|
|
271
|
+
if not quiet:
|
|
272
|
+
print(error_message)
|
|
273
|
+
return (
|
|
274
|
+
FormatFailure(file_path, "ruff", error_message),
|
|
275
|
+
file_path,
|
|
276
|
+
"error",
|
|
277
|
+
error_message,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def process_files_parallel_quiet(
|
|
281
|
+
self,
|
|
282
|
+
files: list[Path],
|
|
283
|
+
env: dict,
|
|
284
|
+
quiet: bool,
|
|
285
|
+
unsafe: bool,
|
|
286
|
+
cache: dict[str, bool],
|
|
287
|
+
cache_hits: list[int],
|
|
288
|
+
) -> tuple[FormatFailure, ...]:
|
|
289
|
+
"""Process files in parallel without progress bar.
|
|
290
|
+
|
|
291
|
+
Aggregates **every** failure in input order; the step-by-step layer
|
|
292
|
+
decides where to truncate (#164, audit patch 2 / review F3).
|
|
293
|
+
"""
|
|
294
|
+
max_workers, batch_size = self.calculate_resource_based_settings()
|
|
295
|
+
total_files = len(files)
|
|
296
|
+
failures: list[tuple[int, FormatFailure]] = []
|
|
297
|
+
|
|
298
|
+
# Process files in batches
|
|
299
|
+
for i in range(0, total_files, batch_size):
|
|
300
|
+
batch_files = files[i : i + batch_size]
|
|
301
|
+
|
|
302
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
303
|
+
# Submit tasks for current batch
|
|
304
|
+
future_to_index = {
|
|
305
|
+
executor.submit(
|
|
306
|
+
self.process_single_file,
|
|
307
|
+
file_path,
|
|
308
|
+
env,
|
|
309
|
+
quiet,
|
|
310
|
+
unsafe,
|
|
311
|
+
cache,
|
|
312
|
+
cache_hits,
|
|
313
|
+
): i + offset
|
|
314
|
+
for offset, file_path in enumerate(batch_files)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Process results as they complete
|
|
318
|
+
for future in concurrent.futures.as_completed(future_to_index):
|
|
319
|
+
index = future_to_index[future]
|
|
320
|
+
try:
|
|
321
|
+
failure, _, _, _ = future.result()
|
|
322
|
+
if failure is not None:
|
|
323
|
+
failures.append((index, failure))
|
|
324
|
+
except Exception as e:
|
|
325
|
+
failures.append(
|
|
326
|
+
(
|
|
327
|
+
index,
|
|
328
|
+
FormatFailure(
|
|
329
|
+
files[index],
|
|
330
|
+
"ruff",
|
|
331
|
+
f"Error processing {files[index]}: {e}",
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
failures.sort(key=lambda item: item[0])
|
|
337
|
+
return tuple(failure for _, failure in failures)
|
|
338
|
+
|
|
339
|
+
def process_files_parallel_with_progress(
|
|
340
|
+
self,
|
|
341
|
+
files: list[Path],
|
|
342
|
+
env: dict,
|
|
343
|
+
quiet: bool,
|
|
344
|
+
unsafe: bool,
|
|
345
|
+
cache: dict[str, bool],
|
|
346
|
+
cache_hits: list[int],
|
|
347
|
+
) -> tuple[tuple[FormatFailure, ...], int, bool]:
|
|
348
|
+
"""Process files in parallel with progress bar.
|
|
349
|
+
|
|
350
|
+
First tuple item holds every failure in input order (#164).
|
|
351
|
+
"""
|
|
352
|
+
total_files = len(files)
|
|
353
|
+
processed_files = 0
|
|
354
|
+
error_occurred = False
|
|
355
|
+
failures: list[tuple[int, FormatFailure]] = []
|
|
356
|
+
max_workers, batch_size = self.calculate_resource_based_settings()
|
|
357
|
+
|
|
358
|
+
# Create progress bar with optimized settings
|
|
359
|
+
pbar = self.progress.create_progress_bar(total_files)
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
# Process files in batches
|
|
363
|
+
for i in range(0, total_files, batch_size):
|
|
364
|
+
batch_files = files[i : i + batch_size]
|
|
365
|
+
|
|
366
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
367
|
+
# Submit tasks for current batch
|
|
368
|
+
future_to_index = {
|
|
369
|
+
executor.submit(
|
|
370
|
+
self.process_single_file,
|
|
371
|
+
file_path,
|
|
372
|
+
env,
|
|
373
|
+
quiet,
|
|
374
|
+
unsafe,
|
|
375
|
+
cache,
|
|
376
|
+
cache_hits,
|
|
377
|
+
): i + offset
|
|
378
|
+
for offset, file_path in enumerate(batch_files)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# Process results as they complete
|
|
382
|
+
for future in concurrent.futures.as_completed(future_to_index):
|
|
383
|
+
index = future_to_index[future]
|
|
384
|
+
file_path = files[index]
|
|
385
|
+
try:
|
|
386
|
+
failure, _, status, error_message = future.result()
|
|
387
|
+
processed_files += 1
|
|
388
|
+
|
|
389
|
+
# Update progress bar
|
|
390
|
+
truncated_name = self.progress.truncate_filename(
|
|
391
|
+
file_path.name
|
|
392
|
+
)
|
|
393
|
+
self.progress.update_progress_bar(
|
|
394
|
+
pbar,
|
|
395
|
+
truncated_name,
|
|
396
|
+
status,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if failure is not None:
|
|
400
|
+
error_occurred = True
|
|
401
|
+
self.progress.print_format_error(
|
|
402
|
+
file_path, error_message
|
|
403
|
+
)
|
|
404
|
+
failures.append((index, failure))
|
|
405
|
+
except Exception as e:
|
|
406
|
+
error_occurred = True
|
|
407
|
+
self.progress.print_processing_error(file_path, e)
|
|
408
|
+
failures.append(
|
|
409
|
+
(
|
|
410
|
+
index,
|
|
411
|
+
FormatFailure(
|
|
412
|
+
file_path,
|
|
413
|
+
"ruff",
|
|
414
|
+
f"Error processing {file_path}: {e}",
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
finally:
|
|
419
|
+
# Ensure progress bar shows 100% before closing
|
|
420
|
+
self.progress.close_progress_bar(pbar, total_files)
|
|
421
|
+
|
|
422
|
+
failures.sort(key=lambda item: item[0])
|
|
423
|
+
return (
|
|
424
|
+
tuple(failure for _, failure in failures),
|
|
425
|
+
processed_files,
|
|
426
|
+
error_occurred,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def run_format_parallel(
|
|
430
|
+
self, files: list[Path], env: dict, quiet: bool, unsafe: bool
|
|
431
|
+
) -> tuple[FormatFailure, ...]:
|
|
432
|
+
"""Run format commands in parallel with caching.
|
|
433
|
+
|
|
434
|
+
Returns every failure in input order; an empty tuple means every file
|
|
435
|
+
succeeded. Truncation for step-by-step mode happens in the path
|
|
436
|
+
handler, not here (#164, review F3).
|
|
437
|
+
"""
|
|
438
|
+
# Check if we're in test environment
|
|
439
|
+
is_test = "PYTEST_CURRENT_TEST" in os.environ
|
|
440
|
+
|
|
441
|
+
# Skip caching in test environment to speed up tests. Nothing is loaded
|
|
442
|
+
# up front: the ledger answers each file's hit, and the entries are
|
|
443
|
+
# written per file as they succeed (the cache is per-entry files, not
|
|
444
|
+
# one document rewritten at the end).
|
|
445
|
+
cache: dict[str, bool] = {}
|
|
446
|
+
cache_hits = [0] # Use list to allow modification in nested function
|
|
447
|
+
total_files = len(files)
|
|
448
|
+
failures: tuple[FormatFailure, ...] = ()
|
|
449
|
+
error_occurred = False
|
|
450
|
+
processed_files = 0
|
|
451
|
+
|
|
452
|
+
if files:
|
|
453
|
+
if quiet:
|
|
454
|
+
# Process files in parallel without progress bar
|
|
455
|
+
failures = self.process_files_parallel_quiet(
|
|
456
|
+
files, env, quiet, unsafe, cache, cache_hits
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
# Process files in parallel with progress bar
|
|
460
|
+
failures, processed_files, error_occurred = (
|
|
461
|
+
self.process_files_parallel_with_progress(
|
|
462
|
+
files, env, quiet, unsafe, cache, cache_hits
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Print cache statistics only if not in test environment
|
|
467
|
+
if not quiet and total_files > 0 and not is_test:
|
|
468
|
+
self.cache.print_statistics(cache_hits[0], total_files, error_occurred)
|
|
469
|
+
|
|
470
|
+
return failures
|