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,1016 @@
|
|
|
1
|
+
"""Format path handler module."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import tomllib
|
|
10
|
+
|
|
11
|
+
from ...utils.logging import log_info_pair, log_warning_pair
|
|
12
|
+
from ...utils.pre_commit import PreCommitRun, run_pre_commit
|
|
13
|
+
from ...utils.pre_commit_config import managed_pre_commit_config
|
|
14
|
+
from ...utils.root import RootKind
|
|
15
|
+
from ...utils.root_fallback import current_root
|
|
16
|
+
from ...utils.subprocess_utils import SubprocessExecutor
|
|
17
|
+
from ...utils.uv_run import run_uv_command
|
|
18
|
+
from .format_executor import FormatExecutor
|
|
19
|
+
from .format_outcome import FormatFailure, FormatOutcome
|
|
20
|
+
from .python_formatter import format_with_python_tools
|
|
21
|
+
from .rust_formatter import format_rust_code
|
|
22
|
+
|
|
23
|
+
#: Directories discovery never descends into (#187): virtualenvs, dependency
|
|
24
|
+
#: trees, packaging output and caches. Unconditional -- unlike the ruff
|
|
25
|
+
#: patterns below, an explicit config cannot re-include them, because handing
|
|
26
|
+
#: one of their files to ruff as an *explicit* path is exactly the ``.venv``
|
|
27
|
+
#: rewrite this ticket reports (ruff does not apply ``exclude`` to explicit
|
|
28
|
+
#: files). ``dist`` and ``build`` are in ruff's own default ``exclude``; they
|
|
29
|
+
#: are repeated here because a project config replaces those defaults outright
|
|
30
|
+
#: and the user docs promise they stay ignored (#206).
|
|
31
|
+
_FIXED_IGNORED_DIRS = frozenset(
|
|
32
|
+
{
|
|
33
|
+
".git",
|
|
34
|
+
".hg",
|
|
35
|
+
".svn",
|
|
36
|
+
".venv",
|
|
37
|
+
"venv",
|
|
38
|
+
"node_modules",
|
|
39
|
+
"site-packages",
|
|
40
|
+
"dist-packages",
|
|
41
|
+
"dist",
|
|
42
|
+
"build",
|
|
43
|
+
"__pycache__",
|
|
44
|
+
".mypy_cache",
|
|
45
|
+
".pytest_cache",
|
|
46
|
+
".ruff_cache",
|
|
47
|
+
".tox",
|
|
48
|
+
".nox",
|
|
49
|
+
".hypothesis",
|
|
50
|
+
".eggs",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
#: Compiled exclude pattern, in config order.
|
|
55
|
+
_ExcludeMatcher = re.Pattern[str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ignored_dir_name(name: str) -> bool:
|
|
59
|
+
"""Whether *name* is in the unconditional ignore set."""
|
|
60
|
+
return name in _FIXED_IGNORED_DIRS or name.endswith(".egg-info")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_toml(path: Path) -> dict | None:
|
|
64
|
+
"""Read *path* as TOML; ``None`` when it is unreadable or malformed."""
|
|
65
|
+
try:
|
|
66
|
+
with path.open("rb") as handle:
|
|
67
|
+
return tomllib.load(handle)
|
|
68
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _string_list(value: object) -> tuple[str, ...]:
|
|
73
|
+
"""The string entries of a ruff list option (anything else is ignored)."""
|
|
74
|
+
if not isinstance(value, list):
|
|
75
|
+
return ()
|
|
76
|
+
return tuple(item for item in value if isinstance(item, str))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _base_exclude_patterns(ruff_config: dict) -> tuple[str, ...]:
|
|
80
|
+
"""The top-level ``exclude`` / ``extend-exclude`` ruff's resolver applies.
|
|
81
|
+
|
|
82
|
+
These are directory patterns: an excluded directory excludes everything
|
|
83
|
+
below it, which is why they are compiled with descendant matching.
|
|
84
|
+
``ruff check --show-files`` already applies them, so discovery only needs
|
|
85
|
+
them on the local-walk fallback.
|
|
86
|
+
"""
|
|
87
|
+
patterns: list[str] = []
|
|
88
|
+
for key in ("exclude", "extend-exclude"):
|
|
89
|
+
patterns.extend(_string_list(ruff_config.get(key)))
|
|
90
|
+
return tuple(patterns)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _mode_exclude_patterns(ruff_config: dict) -> tuple[str, ...]:
|
|
94
|
+
"""``lint.exclude`` / ``format.exclude``, applied per mode after discovery.
|
|
95
|
+
|
|
96
|
+
Ruff matches these against **file paths**, not against directory prefixes:
|
|
97
|
+
with ``exclude = ["gen"]`` ruff still visits ``gen/x.py``; ``gen/*.py`` or
|
|
98
|
+
``**/gen/**`` is what excludes it (ruff 0.15). ``*`` and ``?`` cross ``/``
|
|
99
|
+
there, so ``gen/*.py`` also drops ``gen/sub/y.py``. Discovery has to
|
|
100
|
+
mirror that exactly, because it hands every file to ruff as an *explicit*
|
|
101
|
+
path, where ruff never applies ``exclude`` at all: a file dropped here is
|
|
102
|
+
never linted, and a file kept here is linted whatever the mode list says.
|
|
103
|
+
|
|
104
|
+
Both mode lists are invisible to ``ruff check --show-files``, which stops
|
|
105
|
+
at the base resolver (#195), and both follow the *nearest* config -- so a
|
|
106
|
+
subpackage's own ``[tool.ruff.lint] exclude`` governs its files (#206).
|
|
107
|
+
The ``lint`` / ``format`` tables have no ``extend-exclude`` of their own
|
|
108
|
+
(ruff rejects the key as unknown), so only ``exclude`` is read.
|
|
109
|
+
|
|
110
|
+
The two lists are **unioned** here and applied to both commands (#206):
|
|
111
|
+
``wl lint`` runs ``ruff check`` *and* ``ruff format``, so the union is the
|
|
112
|
+
safe reading for it; ``wl format`` is consequently conservative -- a file
|
|
113
|
+
only ``lint.exclude`` names is not formatted even though ``ruff format``
|
|
114
|
+
would format it. Documented as a trade-off in ``docs/user/commands/lint.md``;
|
|
115
|
+
splitting the matcher per command would need the mode threaded through
|
|
116
|
+
discovery, which the union avoids.
|
|
117
|
+
"""
|
|
118
|
+
patterns: list[str] = []
|
|
119
|
+
for mode in ("lint", "format"):
|
|
120
|
+
section = ruff_config.get(mode)
|
|
121
|
+
if isinstance(section, dict):
|
|
122
|
+
patterns.extend(_string_list(section.get("exclude")))
|
|
123
|
+
return tuple(patterns)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _ruff_config_for(directory: Path) -> tuple[Path, dict | None]:
|
|
127
|
+
"""The directory and ruff config table governing files under *directory*.
|
|
128
|
+
|
|
129
|
+
Mirrors ruff's own discovery: the nearest ancestor carrying
|
|
130
|
+
``ruff.toml`` / ``.ruff.toml`` (same-directory files win over
|
|
131
|
+
``pyproject.toml``), else the nearest ``pyproject.toml`` with a
|
|
132
|
+
``[tool.ruff]`` table. Files without ruff config are transparent. The
|
|
133
|
+
lookup starts at each file's own directory, so a nested config governs its
|
|
134
|
+
own subtree exactly as ruff's hierarchical configuration does (#206).
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
start = directory.resolve()
|
|
138
|
+
except OSError:
|
|
139
|
+
start = directory
|
|
140
|
+
for candidate in (start, *start.parents):
|
|
141
|
+
for name in ("ruff.toml", ".ruff.toml"):
|
|
142
|
+
config_file = candidate / name
|
|
143
|
+
if config_file.is_file():
|
|
144
|
+
data = _load_toml(config_file)
|
|
145
|
+
if data is not None:
|
|
146
|
+
return candidate, data
|
|
147
|
+
pyproject = candidate / "pyproject.toml"
|
|
148
|
+
if pyproject.is_file():
|
|
149
|
+
data = _load_toml(pyproject)
|
|
150
|
+
if data is not None:
|
|
151
|
+
tool = data.get("tool")
|
|
152
|
+
ruff = tool.get("ruff") if isinstance(tool, dict) else None
|
|
153
|
+
if isinstance(ruff, dict):
|
|
154
|
+
return candidate, ruff
|
|
155
|
+
return start, None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class _RuffExcludes:
|
|
159
|
+
"""Per-directory cache of the exclude matchers that govern a directory.
|
|
160
|
+
|
|
161
|
+
Discovery asks once per directory rather than once per file: a scan can
|
|
162
|
+
list hundreds of files and every lookup walks ancestors and parses TOML.
|
|
163
|
+
The cache lives for one discovery call, so a config edited mid-process is
|
|
164
|
+
still read fresh next time.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self) -> None:
|
|
168
|
+
self._cache: dict[
|
|
169
|
+
Path, tuple[Path, tuple[_ExcludeMatcher, ...], tuple[_ExcludeMatcher, ...]]
|
|
170
|
+
] = {}
|
|
171
|
+
|
|
172
|
+
def for_directory(
|
|
173
|
+
self, directory: Path
|
|
174
|
+
) -> tuple[Path, tuple[_ExcludeMatcher, ...], tuple[_ExcludeMatcher, ...]]:
|
|
175
|
+
"""``(config directory, base matchers, mode matchers)`` for *directory*."""
|
|
176
|
+
cached = self._cache.get(directory)
|
|
177
|
+
if cached is not None:
|
|
178
|
+
return cached
|
|
179
|
+
config_dir, ruff_config = _ruff_config_for(directory)
|
|
180
|
+
ruff_config = ruff_config or {}
|
|
181
|
+
entry = (
|
|
182
|
+
config_dir,
|
|
183
|
+
_compile_excludes(_base_exclude_patterns(ruff_config)),
|
|
184
|
+
_compile_excludes(_mode_exclude_patterns(ruff_config), descendants=False),
|
|
185
|
+
)
|
|
186
|
+
self._cache[directory] = entry
|
|
187
|
+
return entry
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _warn_listing_fallback(directory: Path, reason: str) -> None:
|
|
191
|
+
"""Say out loud that discovery degraded to the local walk (#206).
|
|
192
|
+
|
|
193
|
+
A warning, not a hard failure: the formatting run that follows is the
|
|
194
|
+
authority on a broken ruff and reports its own error, while failing here
|
|
195
|
+
would preempt that with "could not list files". What must not happen is
|
|
196
|
+
the pre-#206 silence -- the local walk loses ruff's default excludes,
|
|
197
|
+
``respect-gitignore`` and nested configs, so a caller has to know the run
|
|
198
|
+
is only a partial mirror of what ruff would do.
|
|
199
|
+
"""
|
|
200
|
+
log_warning_pair(
|
|
201
|
+
f"ruff could not list files under {directory} ({reason}); falling back to "
|
|
202
|
+
"wl's own walk, which cannot fully mirror ruff's exclude rules",
|
|
203
|
+
f"ruff 无法枚举 {directory} 下的文件({reason}),改用 wl 自己的遍历;"
|
|
204
|
+
"该遍历无法完全对齐 ruff 的排除规则",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _ruff_listed_files(directory: Path, env: dict | None) -> tuple[Path, ...] | None:
|
|
209
|
+
"""The files ruff itself would visit under *directory*, or ``None``.
|
|
210
|
+
|
|
211
|
+
``ruff check --show-files`` *is* ruff's file resolution (#195), so
|
|
212
|
+
discovery stops re-implementing it: the built-in default ``exclude``
|
|
213
|
+
(``dist``, ``.venv`` ...), the top-level project ``exclude`` /
|
|
214
|
+
``extend-exclude``, nested configs and ``respect-gitignore`` all come from
|
|
215
|
+
ruff rather than from a partial local copy -- which is how ``dist`` used
|
|
216
|
+
to be handed over as an *explicit* file, where ruff never applies
|
|
217
|
+
``exclude``.
|
|
218
|
+
|
|
219
|
+
It stops at the *base* resolver. ``[tool.ruff.lint]`` and
|
|
220
|
+
``[tool.ruff.format]`` ``exclude`` are applied per mode, afterwards, so the
|
|
221
|
+
caller keeps filtering with those lists (see
|
|
222
|
+
:meth:`FormatPathHandler._discovered_python_files`); ``ruff format`` has no
|
|
223
|
+
``--show-files`` at all.
|
|
224
|
+
|
|
225
|
+
Returns ``None`` when ruff could not be asked at all (not installed,
|
|
226
|
+
refused the config). Discovery then falls back to the local walk rather
|
|
227
|
+
than failing the gate: the formatting run itself reports a broken ruff, and
|
|
228
|
+
failing closed here would replace that diagnosis with a less actionable
|
|
229
|
+
one. The fallback is no longer silent, though (#206): it cannot mirror
|
|
230
|
+
ruff's default excludes, ``respect-gitignore`` or nested configs, so the
|
|
231
|
+
degradation is announced by :func:`_warn_listing_fallback`.
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
result = run_uv_command(
|
|
235
|
+
SubprocessExecutor(),
|
|
236
|
+
["uv", "run", "ruff", "check", "--show-files", str(directory)],
|
|
237
|
+
# An empty mapping would strip PATH from the child and make every
|
|
238
|
+
# launch fail; ``None`` inherits, which is what "no environment
|
|
239
|
+
# given" means at both call sites.
|
|
240
|
+
env=env or None,
|
|
241
|
+
quiet=True,
|
|
242
|
+
)
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
_warn_listing_fallback(directory, f"{type(exc).__name__}: {exc}")
|
|
245
|
+
return None
|
|
246
|
+
if result.returncode != 0:
|
|
247
|
+
_warn_listing_fallback(directory, f"exit code {result.returncode}")
|
|
248
|
+
return None
|
|
249
|
+
return tuple(
|
|
250
|
+
Path(line.strip()) for line in result.stdout.splitlines() if line.strip()
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _relative_to_root(path: Path, root: Path) -> Path | None:
|
|
255
|
+
"""Where *path* sits under *root*, ``None`` when it sits outside.
|
|
256
|
+
|
|
257
|
+
Ruff echoes the prefix it was handed (``/var/...``) while *root* is
|
|
258
|
+
resolved (``/private/var/...`` on macOS), so the plain subtraction is
|
|
259
|
+
retried with the file's *parent* canonicalised. Only the parent is
|
|
260
|
+
canonicalised on purpose: a symlinked file must keep its own identity so
|
|
261
|
+
the caller can still recognise (and skip) it.
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
return path.relative_to(root)
|
|
265
|
+
except ValueError:
|
|
266
|
+
pass
|
|
267
|
+
try:
|
|
268
|
+
canonical = Path(os.path.realpath(path.parent)) / path.name
|
|
269
|
+
return canonical.relative_to(root)
|
|
270
|
+
except (OSError, ValueError):
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _glob_to_regex(pattern: str, *, descendants: bool = True) -> re.Pattern[str] | None:
|
|
275
|
+
"""Translate one ruff exclude pattern into an anchored regex.
|
|
276
|
+
|
|
277
|
+
Ruff compiles ``exclude`` entries with ``globset`` **without**
|
|
278
|
+
``literal_separator``, which is not gitignore syntax: ``*`` and ``?``
|
|
279
|
+
match ``/`` too, so ``a/*`` matches ``a/b/w.py`` and ``gen/*.py`` matches
|
|
280
|
+
``gen/sub/y.py`` (verified against ruff 0.15.21). ``**`` is equivalent for
|
|
281
|
+
path matching; the only place it differs is the zero-directory form
|
|
282
|
+
``**/`` at the start or middle of a pattern, which also matches no
|
|
283
|
+
directory at all. Character classes, a leading ``/`` anchor and the
|
|
284
|
+
``!`` literal (ruff does **not** support negation) are handled too.
|
|
285
|
+
|
|
286
|
+
``descendants`` selects which ruff matcher is being mirrored (#206):
|
|
287
|
+
|
|
288
|
+
* ``True`` -- the base ``exclude`` / ``extend-exclude`` resolved by
|
|
289
|
+
``ruff check --show-files`` (and mirrored on wl's fallback walk). It is
|
|
290
|
+
directory-aware: an unanchored pattern is matched at any depth
|
|
291
|
+
(``w.py`` excludes ``a/b/w.py``) and a matched directory excludes its
|
|
292
|
+
contents.
|
|
293
|
+
* ``False`` -- ``lint.exclude`` / ``format.exclude``. Ruff matches those
|
|
294
|
+
against **file paths**, never directory prefixes: a bare ``gen`` does
|
|
295
|
+
not match ``gen/x.py`` (a matched directory does not pull its contents
|
|
296
|
+
in). Depth still applies, though -- ruff resolves a pattern without
|
|
297
|
+
``/`` at any depth (``w.py`` drops ``a/b/w.py``), and a pattern with
|
|
298
|
+
``/`` is anchored at its config root (``gen/*.py`` leaves
|
|
299
|
+
``a/gen/x.py`` alone). Cross-directory reach also comes from ``*``.
|
|
300
|
+
"""
|
|
301
|
+
body = pattern
|
|
302
|
+
anchored = body.startswith("/")
|
|
303
|
+
if anchored:
|
|
304
|
+
body = body[1:]
|
|
305
|
+
body = body.rstrip("/")
|
|
306
|
+
if not body:
|
|
307
|
+
return None
|
|
308
|
+
anchored = anchored or "/" in body
|
|
309
|
+
|
|
310
|
+
parts: list[str] = []
|
|
311
|
+
index = 0
|
|
312
|
+
while index < len(body):
|
|
313
|
+
char = body[index]
|
|
314
|
+
if char == "*":
|
|
315
|
+
if body.startswith("**", index):
|
|
316
|
+
index += 2
|
|
317
|
+
if index < len(body) and body[index] == "/":
|
|
318
|
+
index += 1
|
|
319
|
+
parts.append("(?:.*/)?")
|
|
320
|
+
else:
|
|
321
|
+
parts.append(".*")
|
|
322
|
+
continue
|
|
323
|
+
# globset's default: ``*`` crosses path separators.
|
|
324
|
+
parts.append(".*")
|
|
325
|
+
elif char == "?":
|
|
326
|
+
parts.append(".")
|
|
327
|
+
elif char == "[" and (end := body.find("]", index + 1)) != -1:
|
|
328
|
+
stuff = body[index + 1 : end]
|
|
329
|
+
if stuff.startswith("!"):
|
|
330
|
+
stuff = "^" + stuff[1:]
|
|
331
|
+
parts.append(f"[{stuff}]")
|
|
332
|
+
index = end
|
|
333
|
+
else:
|
|
334
|
+
parts.append(re.escape(char))
|
|
335
|
+
index += 1
|
|
336
|
+
|
|
337
|
+
# Both ruff matchers resolve a pattern without "/" at any depth: the base
|
|
338
|
+
# walker and the mode lists (``lint.exclude = ["w.py"]`` drops
|
|
339
|
+
# ``a/b/w.py``). Only the directory-descendant suffix is base-only, since
|
|
340
|
+
# a mode list never matches a directory (#206).
|
|
341
|
+
prefix = "" if anchored else r"(?:.*/)?"
|
|
342
|
+
suffix = r"(?:/.*)?" if descendants else ""
|
|
343
|
+
return re.compile(prefix + "".join(parts) + suffix + r"\Z")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _compile_excludes(
|
|
347
|
+
patterns: Iterable[str], *, descendants: bool = True
|
|
348
|
+
) -> tuple[_ExcludeMatcher, ...]:
|
|
349
|
+
"""Compile exclude patterns in config order.
|
|
350
|
+
|
|
351
|
+
``!`` is not a negation: ruff compiles exclude entries as globs, so a
|
|
352
|
+
leading ``!`` is an ordinary character (#206). Applying gitignore-style
|
|
353
|
+
negation here would keep a file ruff excludes and hand it to
|
|
354
|
+
``ruff --fix``.
|
|
355
|
+
"""
|
|
356
|
+
matchers: list[_ExcludeMatcher] = []
|
|
357
|
+
for pattern in patterns:
|
|
358
|
+
regex = _glob_to_regex(pattern, descendants=descendants)
|
|
359
|
+
if regex is not None:
|
|
360
|
+
matchers.append(regex)
|
|
361
|
+
return tuple(matchers)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _is_excluded(relative: str, matchers: tuple[_ExcludeMatcher, ...]) -> bool:
|
|
365
|
+
"""Whether *relative* matches any exclude pattern."""
|
|
366
|
+
return any(regex.match(relative) for regex in matchers)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _relative_posix(path: Path, root: Path) -> str:
|
|
370
|
+
"""*path* relative to *root* as a posix string, for pattern matching.
|
|
371
|
+
|
|
372
|
+
*root* is a resolved directory (the config's), so the plain subtraction is
|
|
373
|
+
tried first; resolving is only needed when the scan root is a symlink.
|
|
374
|
+
"""
|
|
375
|
+
try:
|
|
376
|
+
return path.relative_to(root).as_posix()
|
|
377
|
+
except ValueError:
|
|
378
|
+
pass
|
|
379
|
+
try:
|
|
380
|
+
return path.resolve().relative_to(root).as_posix()
|
|
381
|
+
except (OSError, ValueError):
|
|
382
|
+
return path.as_posix()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class FormatPathHandler:
|
|
386
|
+
"""Handler for formatting paths."""
|
|
387
|
+
|
|
388
|
+
def __init__(self):
|
|
389
|
+
self.executor = FormatExecutor()
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def _as_path(path: object) -> Path:
|
|
393
|
+
"""Normalise a path argument (``str`` or ``Path``-like) to ``Path``."""
|
|
394
|
+
if (
|
|
395
|
+
hasattr(path, "exists")
|
|
396
|
+
and hasattr(path, "is_file")
|
|
397
|
+
and hasattr(path, "is_dir")
|
|
398
|
+
and hasattr(path, "name")
|
|
399
|
+
):
|
|
400
|
+
return cast("Path", path)
|
|
401
|
+
return Path(cast("str", path))
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def _is_discovered_file(path_obj: Path) -> bool:
|
|
405
|
+
"""Whether a discovered ``.py`` file may be rewritten by the gate.
|
|
406
|
+
|
|
407
|
+
A symlink can point anywhere (including a virtualenv's sources), so
|
|
408
|
+
the walk never follows one (#187).
|
|
409
|
+
"""
|
|
410
|
+
return path_obj.suffix == ".py" and not path_obj.is_symlink()
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _is_named_python_file(path_obj: Path) -> bool:
|
|
414
|
+
"""Whether an *explicitly named* path names a Python file to rewrite.
|
|
415
|
+
|
|
416
|
+
``Path.is_file`` follows a symlink, and that is the point (#195): the
|
|
417
|
+
user named this path, ruff follows it too, and ``wl format <link>``
|
|
418
|
+
rewrote through it before #187 stopped the *walk* from following
|
|
419
|
+
links. ``wl format -s link.py`` silently doing nothing is the
|
|
420
|
+
regression this restores.
|
|
421
|
+
"""
|
|
422
|
+
return path_obj.suffix == ".py"
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _in_ignored_directory(path_obj: Path) -> bool:
|
|
426
|
+
"""Whether any ancestor of an *explicit* path is in the ignore set."""
|
|
427
|
+
return any(_ignored_dir_name(part) for part in path_obj.parts[:-1])
|
|
428
|
+
|
|
429
|
+
def _get_all_python_files(self, path, env: dict | None = None):
|
|
430
|
+
"""Get the Python files under *path* that ruff would actually format.
|
|
431
|
+
|
|
432
|
+
An explicit path is taken at face value; a directory is resolved by
|
|
433
|
+
ruff's own file resolution (#195) plus the filters ruff cannot express
|
|
434
|
+
downstream (see :meth:`_discovered_python_files`).
|
|
435
|
+
"""
|
|
436
|
+
path_obj = self._as_path(path)
|
|
437
|
+
if not path_obj.exists():
|
|
438
|
+
return []
|
|
439
|
+
if path_obj.is_file():
|
|
440
|
+
if not self._is_named_python_file(path_obj):
|
|
441
|
+
return []
|
|
442
|
+
return [] if self._in_ignored_directory(path_obj) else [path_obj]
|
|
443
|
+
if not path_obj.is_dir() or _ignored_dir_name(path_obj.name):
|
|
444
|
+
return []
|
|
445
|
+
return self._discovered_python_files(path_obj, env)
|
|
446
|
+
|
|
447
|
+
def _discovered_python_files(
|
|
448
|
+
self, directory: Path, env: dict | None = None
|
|
449
|
+
) -> list[Path]:
|
|
450
|
+
"""The ``.py`` files under *directory* that ``wl format`` may rewrite.
|
|
451
|
+
|
|
452
|
+
The candidate set comes from ruff itself (:func:`_ruff_listed_files`),
|
|
453
|
+
which is the only thing that knows the *effective* base exclude:
|
|
454
|
+
built-in defaults, project config, nested configs and
|
|
455
|
+
``respect-gitignore``. Three filters stay here because ``--show-files``
|
|
456
|
+
cannot apply them:
|
|
457
|
+
|
|
458
|
+
* the unconditional ignore set -- a project that writes its own
|
|
459
|
+
``exclude`` replaces ruff's defaults outright, so ``.venv`` would
|
|
460
|
+
reappear (#187);
|
|
461
|
+
* symlinks -- ruff *does* list one inside a scanned directory, and it
|
|
462
|
+
may point into a virtualenv (#187);
|
|
463
|
+
* ``[tool.ruff.lint]`` / ``[tool.ruff.format] exclude`` -- ruff applies
|
|
464
|
+
those *after* the base file resolver ``--show-files`` reports, so
|
|
465
|
+
neither leg's own list is visible there (#195), and each file's
|
|
466
|
+
*nearest* config supplies them (#206).
|
|
467
|
+
"""
|
|
468
|
+
listed = _ruff_listed_files(directory, env)
|
|
469
|
+
if listed is None:
|
|
470
|
+
# Ruff could not be asked; the local walk is still better than
|
|
471
|
+
# handing the directory over blindly (the failure was warned).
|
|
472
|
+
return list(self._iter_python_files(directory))
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
root = directory.resolve()
|
|
476
|
+
except OSError:
|
|
477
|
+
root = directory
|
|
478
|
+
excludes = _RuffExcludes()
|
|
479
|
+
|
|
480
|
+
discovered: list[Path] = []
|
|
481
|
+
for absolute in listed:
|
|
482
|
+
relative = _relative_to_root(absolute, root)
|
|
483
|
+
if relative is None:
|
|
484
|
+
continue
|
|
485
|
+
candidate = directory / relative
|
|
486
|
+
if not self._is_discovered_file(candidate):
|
|
487
|
+
continue
|
|
488
|
+
if any(_ignored_dir_name(part) for part in relative.parts[:-1]):
|
|
489
|
+
continue
|
|
490
|
+
config_dir, _base, mode = excludes.for_directory(candidate.parent)
|
|
491
|
+
if mode and _is_excluded(_relative_posix(candidate, config_dir), mode):
|
|
492
|
+
continue
|
|
493
|
+
discovered.append(candidate)
|
|
494
|
+
return discovered
|
|
495
|
+
|
|
496
|
+
def _iter_python_files(self, directory: Path) -> Iterator[Path]:
|
|
497
|
+
"""Walk *directory*, for when ruff cannot list the files itself.
|
|
498
|
+
|
|
499
|
+
Everything ruff's resolver would have applied has to be re-applied
|
|
500
|
+
here, from each file's own config: the base lists with their
|
|
501
|
+
directory semantics, the mode lists with ruff's file-path semantics.
|
|
502
|
+
"""
|
|
503
|
+
excludes = _RuffExcludes()
|
|
504
|
+
for dirpath, dirnames, filenames in os.walk(directory, followlinks=False):
|
|
505
|
+
current = Path(dirpath)
|
|
506
|
+
config_dir, base, mode = excludes.for_directory(current)
|
|
507
|
+
# Base patterns are directory-aware, exactly as ruff's walker
|
|
508
|
+
# treats them: a matched directory is pruned with its contents.
|
|
509
|
+
# Mode lists never prune -- ruff matches them against file paths
|
|
510
|
+
# only (#206).
|
|
511
|
+
kept: list[str] = []
|
|
512
|
+
for name in sorted(dirnames):
|
|
513
|
+
if _ignored_dir_name(name):
|
|
514
|
+
continue
|
|
515
|
+
candidate = current / name
|
|
516
|
+
if base and _is_excluded(_relative_posix(candidate, config_dir), base):
|
|
517
|
+
continue
|
|
518
|
+
kept.append(name)
|
|
519
|
+
dirnames[:] = kept
|
|
520
|
+
|
|
521
|
+
for name in sorted(filenames):
|
|
522
|
+
if not name.endswith(".py"):
|
|
523
|
+
continue
|
|
524
|
+
candidate = current / name
|
|
525
|
+
if not self._is_discovered_file(candidate):
|
|
526
|
+
continue
|
|
527
|
+
relative = _relative_posix(candidate, config_dir)
|
|
528
|
+
if base and _is_excluded(relative, base):
|
|
529
|
+
continue
|
|
530
|
+
if mode and _is_excluded(relative, mode):
|
|
531
|
+
continue
|
|
532
|
+
yield candidate
|
|
533
|
+
|
|
534
|
+
def _step_by_step_processing(
|
|
535
|
+
self, paths, env, quiet, unsafe, for_lint: bool = False
|
|
536
|
+
) -> FormatOutcome:
|
|
537
|
+
"""Process files in step-by-step mode with parallel execution.
|
|
538
|
+
|
|
539
|
+
Step-by-step truncates at the first failure: a ruff/format failure
|
|
540
|
+
skips pre-commit entirely, and the *result* keeps only the first
|
|
541
|
+
failure. The executor aggregates every file (audit patch 2 / F3) --
|
|
542
|
+
truncation lives here, not there.
|
|
543
|
+
|
|
544
|
+
A directory named on the command line that is called ``rust`` also
|
|
545
|
+
goes through ``format_rust_code`` (#187): ``wl format -s rust`` used
|
|
546
|
+
to skip Rust entirely, so the step-by-step gate never judged it.
|
|
547
|
+
|
|
548
|
+
``for_lint`` skips pre-commit (#180): ``wl lint`` owns that run
|
|
549
|
+
through ``LintExecutor.run_all``, so the format prefix must not judge
|
|
550
|
+
the same hooks a second time.
|
|
551
|
+
"""
|
|
552
|
+
# Get all Python files from all specified paths
|
|
553
|
+
all_python_files: list[Path] = []
|
|
554
|
+
rust_dirs: list[Path] = []
|
|
555
|
+
for path in paths:
|
|
556
|
+
path_obj = self._as_path(path)
|
|
557
|
+
all_python_files.extend(self._get_all_python_files(path_obj, env))
|
|
558
|
+
if path_obj.is_dir() and path_obj.name == "rust":
|
|
559
|
+
rust_dirs.append(path_obj)
|
|
560
|
+
|
|
561
|
+
# Process files in parallel
|
|
562
|
+
failures: tuple[FormatFailure, ...] = ()
|
|
563
|
+
if all_python_files:
|
|
564
|
+
# Use parallel execution with caching
|
|
565
|
+
failures = self.executor.run_format_parallel(
|
|
566
|
+
all_python_files, env, quiet, unsafe
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
if failures:
|
|
570
|
+
# Step-by-step reports the first failure only (input order).
|
|
571
|
+
return FormatOutcome(failures[:1])
|
|
572
|
+
|
|
573
|
+
# An explicit ``rust`` path is formatted here too (#187).
|
|
574
|
+
for rust_dir in rust_dirs:
|
|
575
|
+
rust_failure = format_rust_code(str(rust_dir), env, quiet)
|
|
576
|
+
if rust_failure is not None:
|
|
577
|
+
return FormatOutcome((rust_failure,))
|
|
578
|
+
|
|
579
|
+
# Run pre-commit on all files after step-by-step processing is complete.
|
|
580
|
+
# A hook that *failed* (as opposed to merely auto-fixing) is a real
|
|
581
|
+
# failure and must make the step-by-step run red (#149). ``not
|
|
582
|
+
# for_lint`` alone decides: whether Python files were *discovered*
|
|
583
|
+
# must not gate the verdict, or a subdirectory run with none skips
|
|
584
|
+
# every hook and exits 0 (#187). ``wl lint`` runs pre-commit itself,
|
|
585
|
+
# so its prefix never lands here (#180).
|
|
586
|
+
if not for_lint:
|
|
587
|
+
pre_commit_failure = self._run_pre_commit_on_all_files(env, quiet)
|
|
588
|
+
if pre_commit_failure is not None:
|
|
589
|
+
return FormatOutcome((pre_commit_failure,))
|
|
590
|
+
|
|
591
|
+
return FormatOutcome()
|
|
592
|
+
|
|
593
|
+
def _non_step_by_step_processing(
|
|
594
|
+
self, paths, env, quiet, unsafe, for_lint: bool = False
|
|
595
|
+
) -> tuple[FormatFailure, ...]:
|
|
596
|
+
"""Process files in normal (non-step-by-step) mode.
|
|
597
|
+
|
|
598
|
+
Aggregates every failure -- ruff first, pre-commit last -- rather than
|
|
599
|
+
stopping at the first one (#164).
|
|
600
|
+
|
|
601
|
+
``for_lint`` keeps pre-commit out of the ``wl lint`` prefix (#180): the
|
|
602
|
+
lint run owns that verdict.
|
|
603
|
+
"""
|
|
604
|
+
failures: list[FormatFailure] = []
|
|
605
|
+
|
|
606
|
+
# Original behavior: process paths normally
|
|
607
|
+
for path in paths:
|
|
608
|
+
path_obj = self._as_path(path)
|
|
609
|
+
|
|
610
|
+
if path_obj.exists():
|
|
611
|
+
if path_obj.is_file() and path_obj.suffix == ".py":
|
|
612
|
+
failure = self.executor.format_python_file(
|
|
613
|
+
path_obj, path, env, quiet, unsafe
|
|
614
|
+
)
|
|
615
|
+
if failure is not None:
|
|
616
|
+
failures.append(failure)
|
|
617
|
+
elif path_obj.is_dir():
|
|
618
|
+
failures.extend(
|
|
619
|
+
self._format_directory(
|
|
620
|
+
path_obj, path, env, quiet, unsafe, for_lint
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
else:
|
|
624
|
+
if not quiet:
|
|
625
|
+
print(f"Warning: Path {path} does not exist")
|
|
626
|
+
|
|
627
|
+
return tuple(failures)
|
|
628
|
+
|
|
629
|
+
def format_specified_paths(
|
|
630
|
+
self, paths, env, quiet, unsafe=False, step_by_step=False, for_lint=False
|
|
631
|
+
) -> FormatOutcome:
|
|
632
|
+
"""Format specified paths and report what failed (#164).
|
|
633
|
+
|
|
634
|
+
``for_lint`` is the ``wl lint`` prefix (#180): it still formats, but
|
|
635
|
+
the pre-commit verdict belongs to the lint run, so it is never taken
|
|
636
|
+
here.
|
|
637
|
+
"""
|
|
638
|
+
if step_by_step:
|
|
639
|
+
return self._step_by_step_processing(paths, env, quiet, unsafe, for_lint)
|
|
640
|
+
return FormatOutcome(
|
|
641
|
+
self._non_step_by_step_processing(paths, env, quiet, unsafe, for_lint)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def _format_directory(
|
|
645
|
+
self, path_obj, original_path, env, quiet, unsafe, for_lint: bool = False
|
|
646
|
+
) -> tuple[FormatFailure, ...]:
|
|
647
|
+
"""Format a directory; returns ruff failures then the pre-commit one.
|
|
648
|
+
|
|
649
|
+
``for_lint`` drops the pre-commit verdict: ``wl lint`` runs the hooks
|
|
650
|
+
once itself, so its format prefix only formats (#180).
|
|
651
|
+
"""
|
|
652
|
+
failures = list(
|
|
653
|
+
format_with_python_tools(str(path_obj), env, quiet, unsafe=unsafe)
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Special handling for rust directory; the cargo verdict is a failure
|
|
657
|
+
# like any other, never a printed warning (#187).
|
|
658
|
+
if path_obj.name == "rust":
|
|
659
|
+
rust_failure = format_rust_code(str(path_obj), env, quiet)
|
|
660
|
+
if rust_failure is not None:
|
|
661
|
+
failures.append(rust_failure)
|
|
662
|
+
|
|
663
|
+
# Run pre-commit on the directory
|
|
664
|
+
if not for_lint:
|
|
665
|
+
pre_commit_failure = self._run_pre_commit_on_directory(
|
|
666
|
+
path_obj, original_path, env, quiet
|
|
667
|
+
)
|
|
668
|
+
if pre_commit_failure is not None:
|
|
669
|
+
failures.append(pre_commit_failure)
|
|
670
|
+
return tuple(failures)
|
|
671
|
+
|
|
672
|
+
def _get_pre_commit_config_path(self):
|
|
673
|
+
"""Find the pre-commit config path.
|
|
674
|
+
|
|
675
|
+
Priority: the managed ``.wl/.pre-commit-config.yaml`` of the **DEPLOY**
|
|
676
|
+
root -> ``.pre-commit-config.yaml`` (fallback).
|
|
677
|
+
|
|
678
|
+
The self-sync writes the managed file at the *deploy* root (the git
|
|
679
|
+
work-tree top), so that is where this lookup anchors too (#189).
|
|
680
|
+
Anchoring it at the PROJECT root -- as it did before -- made the file
|
|
681
|
+
invisible whenever the project root is a sub-package below the git top:
|
|
682
|
+
the fallback was a CWD-relative path that does not exist either, so
|
|
683
|
+
every hook was skipped and ``wl format`` exited 0. The root anchor
|
|
684
|
+
still comes from the process CWD (:func:`current_root`), exactly as
|
|
685
|
+
#125 established; only the *kind* moved, and the shared locator owns
|
|
686
|
+
the path itself.
|
|
687
|
+
"""
|
|
688
|
+
precommit_config_path = str(
|
|
689
|
+
managed_pre_commit_config(current_root(RootKind.PROJECT).path)
|
|
690
|
+
)
|
|
691
|
+
if not os.path.exists(precommit_config_path):
|
|
692
|
+
precommit_config_path = ".pre-commit-config.yaml"
|
|
693
|
+
return precommit_config_path
|
|
694
|
+
|
|
695
|
+
def _run_git_flow_hint(self, executor, env, quiet) -> None:
|
|
696
|
+
"""Run git-flow check and display simplified output for AI agent."""
|
|
697
|
+
result = executor.run(
|
|
698
|
+
["uv", "run", "wl", "git-flow", "check"], env=env, quiet=True
|
|
699
|
+
)
|
|
700
|
+
if result.stdout:
|
|
701
|
+
info = self._parse_git_flow_output(result.stdout)
|
|
702
|
+
self._print_git_flow_info(info)
|
|
703
|
+
if result.stderr and not quiet:
|
|
704
|
+
print(result.stderr)
|
|
705
|
+
|
|
706
|
+
def _parse_git_flow_output(self, output):
|
|
707
|
+
"""Parse git-flow output into a dictionary."""
|
|
708
|
+
info = {}
|
|
709
|
+
lines = output.strip().split("\n")
|
|
710
|
+
for line in lines:
|
|
711
|
+
if ":" in line and '"' in line:
|
|
712
|
+
key_part, value_part = line.split(":", 1)
|
|
713
|
+
key = key_part.strip()
|
|
714
|
+
value = value_part.strip().strip('",')
|
|
715
|
+
info[key] = value
|
|
716
|
+
return info
|
|
717
|
+
|
|
718
|
+
def _print_git_flow_info(self, info):
|
|
719
|
+
"""Print git-flow info in AI-agent-friendly format."""
|
|
720
|
+
status = info.get("status")
|
|
721
|
+
if not status or status == "OK":
|
|
722
|
+
return
|
|
723
|
+
|
|
724
|
+
output_lines = [f"Git Flow [{status}]"]
|
|
725
|
+
keys_to_print = [
|
|
726
|
+
("branch", "branch"),
|
|
727
|
+
("branch_type", "type"),
|
|
728
|
+
("recommended_action", "action"),
|
|
729
|
+
("source_branch", "source_branch"),
|
|
730
|
+
("allowed_push", "allowed_push"),
|
|
731
|
+
("rename_required", "rename_required"),
|
|
732
|
+
("direct_push_allowed", "direct_push_allowed"),
|
|
733
|
+
("merge_targets", "merge_targets"),
|
|
734
|
+
("suggested_branch_patterns", "suggested_patterns"),
|
|
735
|
+
("next_step", "next_step"),
|
|
736
|
+
]
|
|
737
|
+
|
|
738
|
+
for key, label in keys_to_print:
|
|
739
|
+
value = info.get(key)
|
|
740
|
+
if value:
|
|
741
|
+
output_lines.append(f" {label}: {value}")
|
|
742
|
+
|
|
743
|
+
print("\n".join(output_lines))
|
|
744
|
+
|
|
745
|
+
def _run_pre_commit_command(
|
|
746
|
+
self, env, quiet, description=None, original_path=None
|
|
747
|
+
) -> FormatFailure | None:
|
|
748
|
+
"""Run pre-commit and report whether a hook judged the check failed.
|
|
749
|
+
|
|
750
|
+
Returns a :class:`FormatFailure` only when a hook *failed* -- the case
|
|
751
|
+
``wl format`` has to propagate so the gate cannot go falsely green
|
|
752
|
+
(#149). A run whose only non-zero effect was auto-fixing files returns
|
|
753
|
+
``None``: that is the expected behaviour of running formatters and must
|
|
754
|
+
stay green.
|
|
755
|
+
|
|
756
|
+
The reading of the run itself lives in
|
|
757
|
+
:class:`wl_cli.utils.pre_commit.PreCommitRun` (#165); this method only
|
|
758
|
+
chooses what format means by "no config" and how the result is shown.
|
|
759
|
+
"""
|
|
760
|
+
from ...utils.subprocess_utils import SubprocessExecutor
|
|
761
|
+
|
|
762
|
+
try:
|
|
763
|
+
executor = SubprocessExecutor()
|
|
764
|
+
precommit_config_path = self._get_pre_commit_config_path()
|
|
765
|
+
|
|
766
|
+
# The single skip decision every format path reaches (#172): for
|
|
767
|
+
# ``wl format`` a missing managed config means "nothing to check",
|
|
768
|
+
# never a failure. ``_run_pre_commit_defaults`` used to repeat this
|
|
769
|
+
# check and own the only warning.
|
|
770
|
+
if not os.path.exists(precommit_config_path):
|
|
771
|
+
if not quiet:
|
|
772
|
+
print(
|
|
773
|
+
"Warning: .pre-commit-config.yaml not found in current directory or .wl directory, skipping pre-commit hooks"
|
|
774
|
+
)
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
self._run_git_flow_hint(executor, env, quiet)
|
|
778
|
+
if not quiet and description:
|
|
779
|
+
print(description)
|
|
780
|
+
|
|
781
|
+
# The run's exit code is deliberately *not* propagated as-is:
|
|
782
|
+
# pre-commit also exits non-zero when a hook simply rewrote files.
|
|
783
|
+
run = run_pre_commit(
|
|
784
|
+
files=None,
|
|
785
|
+
config=Path(precommit_config_path),
|
|
786
|
+
cwd=Path.cwd(),
|
|
787
|
+
env=env,
|
|
788
|
+
quiet=True,
|
|
789
|
+
)
|
|
790
|
+
hooks_failed = run.hooks_failed
|
|
791
|
+
|
|
792
|
+
# Show the same blocks the verdict was computed from (#172), and
|
|
793
|
+
# read stderr as well: a run that failed through stderr alone used
|
|
794
|
+
# to be invisible here. ``filtered_output`` stays lint's view --
|
|
795
|
+
# text filtering can include auto-fix sections the verdict ignores.
|
|
796
|
+
if run.combined_output:
|
|
797
|
+
failed_blocks = run.failed_blocks
|
|
798
|
+
if failed_blocks:
|
|
799
|
+
for block in failed_blocks:
|
|
800
|
+
print("\n".join(block))
|
|
801
|
+
elif hooks_failed:
|
|
802
|
+
# Failed without any hook ``Failed`` block: pre-commit
|
|
803
|
+
# erroring before it could judge (e.g. an invalid config).
|
|
804
|
+
# Show what it said rather than claiming the hooks passed
|
|
805
|
+
# while exiting non-zero.
|
|
806
|
+
print(run.combined_output)
|
|
807
|
+
elif not quiet:
|
|
808
|
+
print("All pre-commit hooks passed.")
|
|
809
|
+
|
|
810
|
+
if not hooks_failed:
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
return FormatFailure(
|
|
814
|
+
path=Path(original_path) if original_path else Path.cwd(),
|
|
815
|
+
kind="pre_commit",
|
|
816
|
+
message="pre-commit reported a failing hook",
|
|
817
|
+
details=self._pre_commit_failure_details(run),
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
except Exception as e:
|
|
821
|
+
# A hook that cannot be launched at all is a *warning*, not a hook
|
|
822
|
+
# verdict: keep the historical lenient behaviour here so a machine
|
|
823
|
+
# without pre-commit does not turn ``wl format`` red.
|
|
824
|
+
if not quiet:
|
|
825
|
+
if original_path:
|
|
826
|
+
print(f"Warning: Pre-commit failed for {original_path}: {e}")
|
|
827
|
+
else:
|
|
828
|
+
print(f"Warning: Pre-commit failed: {e}")
|
|
829
|
+
return None
|
|
830
|
+
|
|
831
|
+
def _pre_commit_failure_details(self, run: PreCommitRun) -> tuple[str, ...]:
|
|
832
|
+
"""One detail entry per failing hook, already joined into a block.
|
|
833
|
+
|
|
834
|
+
Taken straight from ``run.failed_blocks``, which already excludes
|
|
835
|
+
auto-fix blocks for the same reason they are not failures (#149): they
|
|
836
|
+
only rewrote files. When pre-commit errored before printing any hook
|
|
837
|
+
verdict there are no blocks, so the raw output is the detail.
|
|
838
|
+
"""
|
|
839
|
+
details = tuple("\n".join(block) for block in run.failed_blocks)
|
|
840
|
+
if details:
|
|
841
|
+
return details
|
|
842
|
+
output = run.combined_output.strip()
|
|
843
|
+
return (output,) if output else ()
|
|
844
|
+
|
|
845
|
+
def _run_pre_commit_on_all_files(self, env, quiet) -> FormatFailure | None:
|
|
846
|
+
"""Run pre-commit on all files; failure when a hook judged failure."""
|
|
847
|
+
description = "\nRunning pre-commit on all files..."
|
|
848
|
+
return self._run_pre_commit_command(env, quiet, description)
|
|
849
|
+
|
|
850
|
+
def _run_pre_commit_on_directory(
|
|
851
|
+
self, path_obj, original_path, env, quiet
|
|
852
|
+
) -> FormatFailure | None:
|
|
853
|
+
"""Run pre-commit on a directory; failure when a hook judged failure."""
|
|
854
|
+
return self._run_pre_commit_command(env, quiet, original_path=original_path)
|
|
855
|
+
|
|
856
|
+
def _step_by_step_defaults(
|
|
857
|
+
self, current_path: Path, env: dict, quiet: bool, unsafe: bool, for_lint: bool
|
|
858
|
+
) -> FormatOutcome:
|
|
859
|
+
"""Process default paths in step-by-step mode with parallel execution.
|
|
860
|
+
|
|
861
|
+
The rust branch matches the non-step-by-step default path (#180): it
|
|
862
|
+
used to be missing here, so ``wl format -s`` -- the documented
|
|
863
|
+
pre-commit quality gate -- never formatted ``rust/``.
|
|
864
|
+
"""
|
|
865
|
+
src_path = current_path / "src"
|
|
866
|
+
tools_path = current_path / "tools"
|
|
867
|
+
tests_path = current_path / "tests"
|
|
868
|
+
|
|
869
|
+
# Get all Python files from all default paths
|
|
870
|
+
all_python_files = []
|
|
871
|
+
for path in [src_path, tools_path, tests_path]:
|
|
872
|
+
if path.exists():
|
|
873
|
+
python_files = self._get_all_python_files(path, env)
|
|
874
|
+
all_python_files.extend(python_files)
|
|
875
|
+
|
|
876
|
+
# Process files in parallel
|
|
877
|
+
failures: tuple[FormatFailure, ...] = ()
|
|
878
|
+
if all_python_files:
|
|
879
|
+
# Use parallel execution with caching
|
|
880
|
+
failures = self.executor.run_format_parallel(
|
|
881
|
+
all_python_files, env, quiet, unsafe
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
if failures:
|
|
885
|
+
# Step-by-step reports the first failure only (input order).
|
|
886
|
+
return FormatOutcome(failures[:1])
|
|
887
|
+
|
|
888
|
+
# Same rust branch as the non-step-by-step default path (#180); the
|
|
889
|
+
# cargo verdict is folded into the outcome instead of being dropped
|
|
890
|
+
# (#187).
|
|
891
|
+
rust_failure = self._format_rust_if_present(current_path, env, quiet, for_lint)
|
|
892
|
+
if rust_failure is not None:
|
|
893
|
+
return FormatOutcome((rust_failure,))
|
|
894
|
+
|
|
895
|
+
# Run pre-commit on all files after step-by-step processing is complete.
|
|
896
|
+
# A hook that *failed* (not merely auto-fixed) makes the run red (#149).
|
|
897
|
+
# ``not for_lint`` alone decides (#187): discovery results no longer
|
|
898
|
+
# gate the verdict, so a run with no Python files still judges hooks.
|
|
899
|
+
# ``wl lint`` runs pre-commit itself, so its prefix skips it (#180).
|
|
900
|
+
if not for_lint:
|
|
901
|
+
pre_commit_failure = self._run_pre_commit_on_all_files(env, quiet)
|
|
902
|
+
if pre_commit_failure is not None:
|
|
903
|
+
return FormatOutcome((pre_commit_failure,))
|
|
904
|
+
|
|
905
|
+
return FormatOutcome()
|
|
906
|
+
|
|
907
|
+
def _format_rust_if_present(
|
|
908
|
+
self, current_path: Path, env: dict, quiet: bool, for_lint: bool
|
|
909
|
+
) -> FormatFailure | None:
|
|
910
|
+
"""Format ``rust/`` under *current_path* when it exists and not linting.
|
|
911
|
+
|
|
912
|
+
Both default paths -- step-by-step and not -- go through this one
|
|
913
|
+
condition, so the two cannot drift apart (#180). ``wl lint`` skips it:
|
|
914
|
+
Rust formatting is not what lint judges (the non-step path always did).
|
|
915
|
+
A failing ``cargo fmt`` is returned so the caller can fold it into
|
|
916
|
+
``FormatOutcome`` (#187).
|
|
917
|
+
"""
|
|
918
|
+
rust_path = current_path / "rust"
|
|
919
|
+
if rust_path.exists() and not for_lint:
|
|
920
|
+
if not quiet:
|
|
921
|
+
log_info_pair("Formatting Rust code...", "正在格式化 Rust 代码...")
|
|
922
|
+
return format_rust_code(str(rust_path), env, quiet)
|
|
923
|
+
return None
|
|
924
|
+
|
|
925
|
+
def _process_python_directory(
|
|
926
|
+
self,
|
|
927
|
+
path: Path,
|
|
928
|
+
env: dict,
|
|
929
|
+
quiet: bool,
|
|
930
|
+
unsafe: bool,
|
|
931
|
+
description: tuple[str, str],
|
|
932
|
+
) -> tuple[FormatFailure, ...]:
|
|
933
|
+
"""Process a single Python directory if it exists."""
|
|
934
|
+
if path.exists():
|
|
935
|
+
if not quiet:
|
|
936
|
+
# Spelled out rather than ``*description`` so the ledger can
|
|
937
|
+
# register the pair site by name (#187).
|
|
938
|
+
log_info_pair(description[0], description[1])
|
|
939
|
+
return format_with_python_tools(str(path), env, quiet, unsafe=unsafe)
|
|
940
|
+
return ()
|
|
941
|
+
|
|
942
|
+
def _non_step_by_step_defaults(
|
|
943
|
+
self, current_path: Path, env: dict, quiet: bool, unsafe: bool, for_lint: bool
|
|
944
|
+
) -> tuple[FormatFailure, ...]:
|
|
945
|
+
"""Process default paths in normal (non-step-by-step) mode.
|
|
946
|
+
|
|
947
|
+
Collects every ruff failure (src/tools/tests) then the pre-commit
|
|
948
|
+
verdict, in that order (#164).
|
|
949
|
+
"""
|
|
950
|
+
failures: list[FormatFailure] = []
|
|
951
|
+
|
|
952
|
+
# Process standard Python directories
|
|
953
|
+
directories = [
|
|
954
|
+
(
|
|
955
|
+
current_path / "src",
|
|
956
|
+
("Formatting src directory...", "正在格式化 src 目录..."),
|
|
957
|
+
),
|
|
958
|
+
(
|
|
959
|
+
current_path / "tools",
|
|
960
|
+
("Formatting tools directory...", "正在格式化 tools 目录..."),
|
|
961
|
+
),
|
|
962
|
+
(
|
|
963
|
+
current_path / "tests",
|
|
964
|
+
("Formatting tests directory...", "正在格式化 tests 目录..."),
|
|
965
|
+
),
|
|
966
|
+
]
|
|
967
|
+
|
|
968
|
+
for path, description in directories:
|
|
969
|
+
failures.extend(
|
|
970
|
+
self._process_python_directory(path, env, quiet, unsafe, description)
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
# Type stubs generation removed from format; handled in build dist
|
|
974
|
+
rust_failure = self._format_rust_if_present(current_path, env, quiet, for_lint)
|
|
975
|
+
if rust_failure is not None:
|
|
976
|
+
failures.append(rust_failure)
|
|
977
|
+
|
|
978
|
+
# Run pre-commit on all files -- except for the ``wl lint`` prefix,
|
|
979
|
+
# whose pre-commit run belongs to ``LintExecutor.run_all`` (#180).
|
|
980
|
+
if not for_lint:
|
|
981
|
+
pre_commit_failure = self._run_pre_commit_defaults(env, quiet)
|
|
982
|
+
if pre_commit_failure is not None:
|
|
983
|
+
failures.append(pre_commit_failure)
|
|
984
|
+
return tuple(failures)
|
|
985
|
+
|
|
986
|
+
def format_defaults(
|
|
987
|
+
self,
|
|
988
|
+
current_path: Path,
|
|
989
|
+
env: dict,
|
|
990
|
+
quiet: bool,
|
|
991
|
+
unsafe: bool,
|
|
992
|
+
for_lint: bool,
|
|
993
|
+
step_by_step=False,
|
|
994
|
+
) -> FormatOutcome:
|
|
995
|
+
"""Format default paths and report what failed (#164).
|
|
996
|
+
|
|
997
|
+
``for_lint`` marks the ``wl lint`` format prefix (#180): it formats,
|
|
998
|
+
but leaves pre-commit and Rust to the lint run.
|
|
999
|
+
"""
|
|
1000
|
+
if step_by_step:
|
|
1001
|
+
return self._step_by_step_defaults(
|
|
1002
|
+
current_path, env, quiet, unsafe, for_lint
|
|
1003
|
+
)
|
|
1004
|
+
return FormatOutcome(
|
|
1005
|
+
self._non_step_by_step_defaults(current_path, env, quiet, unsafe, for_lint)
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
def _run_pre_commit_defaults(self, env: dict, quiet: bool) -> FormatFailure | None:
|
|
1009
|
+
"""Run pre-commit for default formatting mode.
|
|
1010
|
+
|
|
1011
|
+
Returns a failure when a hook judged the check failed. The missing-
|
|
1012
|
+
config skip, its warning, and the one "Running pre-commit" message all
|
|
1013
|
+
live in :meth:`_run_pre_commit_command`, the single choke point every
|
|
1014
|
+
format entry path reaches (#172).
|
|
1015
|
+
"""
|
|
1016
|
+
return self._run_pre_commit_on_all_files(env, quiet)
|