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,409 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lint executor module.
|
|
3
|
+
Runs every tool one ``wl lint`` needs -- ruff, pre-commit, mypy -- exactly once.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ...utils.pre_commit import PreCommitRun, run_pre_commit
|
|
9
|
+
from ...utils.pre_commit_config import managed_pre_commit_config
|
|
10
|
+
from ...utils.root import Root, RootKind
|
|
11
|
+
from ...utils.subprocess_utils import SubprocessExecutor, SubprocessResult
|
|
12
|
+
from .lint_run import LintRun
|
|
13
|
+
from .mypy_config import mypy_config_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _scope_path(root: Root, token: str) -> Path:
|
|
17
|
+
"""The directory *token* names under *root*.
|
|
18
|
+
|
|
19
|
+
``src/`` is the one scope directory the root module names
|
|
20
|
+
(:meth:`Root.src_dir`), so ask it for that one; every other token --
|
|
21
|
+
``tests/`` or a caller-supplied path -- has no derived-path method and is
|
|
22
|
+
joined onto the root the plain way. Routing the ``src`` probe through the
|
|
23
|
+
module keeps the root layout with a single owner (#114) without widening
|
|
24
|
+
``Root``'s interface.
|
|
25
|
+
"""
|
|
26
|
+
if Path(token).parts == ("src",):
|
|
27
|
+
return root.src_dir()
|
|
28
|
+
return root.path / token
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _scope_token_path(token: str, root: Root | None) -> Path:
|
|
32
|
+
"""The path *token* names when the command runs from the anchored root.
|
|
33
|
+
|
|
34
|
+
Both an explicit user path and a fallback default resolve through here, so
|
|
35
|
+
the two probes cannot drift apart (#174). *root* is ``None`` when the
|
|
36
|
+
caller pinned no project root; the token is then used as-is and resolves
|
|
37
|
+
against the cwd, which is where the command will run.
|
|
38
|
+
"""
|
|
39
|
+
candidate = Path(token)
|
|
40
|
+
if root is not None and not candidate.is_absolute():
|
|
41
|
+
return _scope_path(root, token)
|
|
42
|
+
return candidate
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _existing_scope_tokens(tokens: tuple[str, ...], root: Root | None) -> list[str]:
|
|
46
|
+
"""The *tokens* that resolve to an existing path, probed like explicit paths.
|
|
47
|
+
|
|
48
|
+
A probe that raises counts as missing, matching the explicit-path loop's
|
|
49
|
+
blanket guard: an unreadable path must not crash command construction.
|
|
50
|
+
"""
|
|
51
|
+
existing = []
|
|
52
|
+
for token in tokens:
|
|
53
|
+
try:
|
|
54
|
+
if _scope_token_path(token, root).exists():
|
|
55
|
+
existing.append(token)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return existing
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _cwd_anchored_scope_tokens(tokens: list[str]) -> list[str]:
|
|
62
|
+
"""*tokens* re-anchored on the cwd, as absolute paths where they exist there.
|
|
63
|
+
|
|
64
|
+
The floor for an explicit scope that resolved nowhere under the project
|
|
65
|
+
root (#179). A token can still name a real path relative to where the user
|
|
66
|
+
typed the command -- ``cd pkg && wl lint commands`` -- while the ruff run
|
|
67
|
+
happens at the project root; handing it over unchanged only produced
|
|
68
|
+
``E902``. Resolving it here hands ruff the same directory, absolute, so
|
|
69
|
+
every process agrees on it (the symlink reasoning of #170 review F4).
|
|
70
|
+
|
|
71
|
+
A token that exists nowhere -- an absolute path included -- is returned
|
|
72
|
+
untouched, so ruff reports it instead of lint silently widening to ``"."``.
|
|
73
|
+
"""
|
|
74
|
+
anchored = []
|
|
75
|
+
for token in tokens:
|
|
76
|
+
try:
|
|
77
|
+
candidate = Path(token)
|
|
78
|
+
resolved = candidate.resolve()
|
|
79
|
+
if not candidate.is_absolute() and resolved.exists():
|
|
80
|
+
anchored.append(str(resolved))
|
|
81
|
+
else:
|
|
82
|
+
anchored.append(token)
|
|
83
|
+
except Exception:
|
|
84
|
+
# Same blanket guard as the probes: an unreadable path must not
|
|
85
|
+
# crash command construction; ruff reports it.
|
|
86
|
+
anchored.append(token)
|
|
87
|
+
return anchored
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolved_scope_tokens(tokens: tuple[str, ...], root: Root | None) -> list[str]:
|
|
91
|
+
"""Every token, in order: anchored on *root* or passed through (#188).
|
|
92
|
+
|
|
93
|
+
Filtering the missing tokens out -- the old ``existing or cwd_anchored``
|
|
94
|
+
-- silently narrowed a partially-missing scope: ``wl lint src typo`` ran
|
|
95
|
+
``ruff check src`` and never mentioned ``typo``. Each token is now kept:
|
|
96
|
+
|
|
97
|
+
* one that resolves under *root* is handed over as typed;
|
|
98
|
+
* one that does not is re-anchored on the cwd (absolute when it is real
|
|
99
|
+
there -- ``cd pkg && wl lint commands``, the #179 floor);
|
|
100
|
+
* one that exists nowhere is passed through untouched, so ruff reports it
|
|
101
|
+
as ``E902`` and mypy as ``Cannot read file ...`` instead of lint
|
|
102
|
+
checking less than asked.
|
|
103
|
+
|
|
104
|
+
Probes share :func:`_scope_token_path` with the existence filters, so the
|
|
105
|
+
two can never disagree about what "resolves under the root" means.
|
|
106
|
+
"""
|
|
107
|
+
resolved: list[str] = []
|
|
108
|
+
for token in tokens:
|
|
109
|
+
try:
|
|
110
|
+
if _scope_token_path(token, root).exists():
|
|
111
|
+
resolved.append(token)
|
|
112
|
+
continue
|
|
113
|
+
except Exception:
|
|
114
|
+
# Same blanket guard as the probes: an unreadable path must not
|
|
115
|
+
# crash command construction; the tool reports it.
|
|
116
|
+
pass
|
|
117
|
+
resolved.append(_cwd_anchored_scope_tokens([token])[0])
|
|
118
|
+
return resolved
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class LintExecutor:
|
|
122
|
+
"""Executor for lint commands."""
|
|
123
|
+
|
|
124
|
+
def prepare_ruff_command(
|
|
125
|
+
self,
|
|
126
|
+
paths: list[str] | None,
|
|
127
|
+
fix: bool,
|
|
128
|
+
quiet: bool,
|
|
129
|
+
*,
|
|
130
|
+
structured: bool = False,
|
|
131
|
+
project_root: Path | None = None,
|
|
132
|
+
) -> list[str]:
|
|
133
|
+
"""Prepare the ruff command.
|
|
134
|
+
|
|
135
|
+
*project_root* is the directory this command will be **run from**:
|
|
136
|
+
the explicit-path probes below resolve against it the same way mypy's
|
|
137
|
+
explicit paths do (#179). Without it, a bare token was probed against
|
|
138
|
+
the process cwd even though the command later ran with
|
|
139
|
+
``cwd=project_root``, so from a subdirectory ``wl lint src`` could drop
|
|
140
|
+
an existing scope and widen to the whole repo. ``None`` keeps the
|
|
141
|
+
historical cwd probe, which is correct when the caller runs the command
|
|
142
|
+
from wherever it built it.
|
|
143
|
+
|
|
144
|
+
A requested scope is never widened to ``"."`` either, and no token is
|
|
145
|
+
dropped (#188): a partially-missing scope used to run only the tokens
|
|
146
|
+
that existed. Each token that does not resolve under *project_root* is
|
|
147
|
+
re-probed against the cwd: one that is real there -- ``cd pkg && wl
|
|
148
|
+
lint commands`` -- goes to ruff as an absolute path, so the run at the
|
|
149
|
+
root reaches it, while a token that exists nowhere is passed through
|
|
150
|
+
for ruff to report (``E902``). This is this function's own floor,
|
|
151
|
+
**not** mypy's #174 one: mypy's *fallback* scope is filtered, whereas
|
|
152
|
+
ruff has no default scope to fall back to and none is invented. With
|
|
153
|
+
no explicit paths at all the default stays ``"."`` (the documented
|
|
154
|
+
default, which ``wl gate`` depends on).
|
|
155
|
+
|
|
156
|
+
``structured`` asks ruff for the report's ``--output-format full``
|
|
157
|
+
text. One such run carries every finding -- header, ``-->`` location,
|
|
158
|
+
snippet, caret, ``help:``, and ruff's own closing summary -- which is
|
|
159
|
+
what lets the report group the verbatim blocks into its per-directory
|
|
160
|
+
sections without re-running ruff once per section (#170 audit patch 4 /
|
|
161
|
+
review F1-F2). ``--fix`` is orthogonal: ruff applies the fixes, then
|
|
162
|
+
reports whatever diagnostics remain, so the same single run stays
|
|
163
|
+
enough.
|
|
164
|
+
|
|
165
|
+
``--quiet`` is *not* passed on a structured run even when the caller
|
|
166
|
+
asked for it: ruff's quiet mode suppresses the summary, and with it the
|
|
167
|
+
``Found N errors.`` / ``[*] N fixable ... (N hidden fixes ...)`` lines
|
|
168
|
+
the report must keep (#170 review F2). The caller's quiet still governs
|
|
169
|
+
wl's own logging and the capture.
|
|
170
|
+
"""
|
|
171
|
+
# Prepare ruff command (offline, no uv fetch)
|
|
172
|
+
cmd = ["ruff", "check"]
|
|
173
|
+
|
|
174
|
+
# The directory the command will run from, as a value the scope probe
|
|
175
|
+
# can resolve against. ``None`` when the caller pinned no root -- then
|
|
176
|
+
# a bare token is probed against the cwd exactly as before
|
|
177
|
+
# ``project_root`` existed.
|
|
178
|
+
root = (
|
|
179
|
+
Root(project_root, RootKind.PROJECT) if project_root is not None else None
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Every token the caller named is handed over, or the whole directory
|
|
183
|
+
# when none were named. A token missing under the root is re-anchored
|
|
184
|
+
# on the cwd (absolute when real there) rather than widened to ".";
|
|
185
|
+
# a token real in neither place is passed through for ruff to report.
|
|
186
|
+
if paths:
|
|
187
|
+
cmd.extend(_resolved_scope_tokens(tuple(paths), root))
|
|
188
|
+
else:
|
|
189
|
+
cmd.append(".")
|
|
190
|
+
|
|
191
|
+
# One structured run feeds every report section
|
|
192
|
+
if structured:
|
|
193
|
+
cmd.extend(["--output-format", "full"])
|
|
194
|
+
|
|
195
|
+
# Add fix flag if requested
|
|
196
|
+
if fix:
|
|
197
|
+
cmd.append("--fix")
|
|
198
|
+
|
|
199
|
+
# Add quiet flag if requested, but never for the structured run whose
|
|
200
|
+
# summary and fixable count the report renders.
|
|
201
|
+
if quiet and not structured:
|
|
202
|
+
cmd.append("--quiet")
|
|
203
|
+
|
|
204
|
+
return cmd
|
|
205
|
+
|
|
206
|
+
def prepare_mypy_command(
|
|
207
|
+
self,
|
|
208
|
+
paths: list[str] | None,
|
|
209
|
+
quiet: bool,
|
|
210
|
+
project_root: Path | None = None,
|
|
211
|
+
*,
|
|
212
|
+
fallback_paths: tuple[str, ...] = ("src/", "tests/"),
|
|
213
|
+
) -> list[str]:
|
|
214
|
+
"""Prepare the mypy command.
|
|
215
|
+
|
|
216
|
+
*project_root* is the directory this command will be **run from**: the
|
|
217
|
+
config lookup and the path-existence checks below resolve against it.
|
|
218
|
+
It defaults to the current working directory, which is only correct
|
|
219
|
+
when the caller runs the command from wherever it built it -- the gate
|
|
220
|
+
builds its command before running it with the project root as cwd.
|
|
221
|
+
|
|
222
|
+
*fallback_paths* is the scope used when the requested *paths* cannot be
|
|
223
|
+
used, or when none were requested; it defaults to the historical
|
|
224
|
+
``src/ tests/``. A caller whose scope is a *contract* narrows it:
|
|
225
|
+
``wl gate`` checks ``src/`` and nothing else, so a root that cannot
|
|
226
|
+
satisfy that scope must fail on its own terms rather than silently
|
|
227
|
+
widen the leg to directories the change under test has nothing to do
|
|
228
|
+
with (review F0 -- a monorepo whose git work-tree root has no
|
|
229
|
+
``src/``).
|
|
230
|
+
|
|
231
|
+
An **explicit** path is never filtered (#188, the same floor as ruff's
|
|
232
|
+
command): a token that resolves nowhere under the root is handed over
|
|
233
|
+
-- cwd-anchored when it is real there -- so mypy says ``Cannot read
|
|
234
|
+
file ...`` instead of lint quietly checking only the tokens that
|
|
235
|
+
happened to exist. Only the caller's *fallback* is filtered, below.
|
|
236
|
+
|
|
237
|
+
The fallback itself is filtered to the tokens that resolve to an
|
|
238
|
+
existing path, probed exactly like an explicit one (#174): a project
|
|
239
|
+
with only ``src/`` must not ask mypy to read ``tests/``, which would
|
|
240
|
+
exit 2 and leave ``wl lint`` permanently red. When *none* of the
|
|
241
|
+
fallback tokens exists, the original fallback is passed unchanged --
|
|
242
|
+
the scope is never silently widened or emptied, so mypy still fails
|
|
243
|
+
with ``Cannot read file ...`` and ``wl gate``'s ``src/`` contract
|
|
244
|
+
still fails on its own terms. That floor assumes a non-empty
|
|
245
|
+
*fallback_paths*, as every current caller passes one; an empty
|
|
246
|
+
fallback would necessarily yield an empty scope.
|
|
247
|
+
"""
|
|
248
|
+
# Prepare mypy command
|
|
249
|
+
root_path = Path.cwd() if project_root is None else project_root
|
|
250
|
+
# The same directory as a value, for the scope probes below. ``None``
|
|
251
|
+
# when the caller pinned no root -- then a bare token is probed against
|
|
252
|
+
# the cwd exactly as before ``project_root`` existed.
|
|
253
|
+
root = Root(root_path, RootKind.PROJECT) if project_root is not None else None
|
|
254
|
+
cmd = ["mypy"]
|
|
255
|
+
|
|
256
|
+
# Absolute, never cwd-relative (#170 review F4). The command is built
|
|
257
|
+
# from *project_root* and then run with it as cwd, but the two can
|
|
258
|
+
# disagree through a symlink (``/tmp`` vs ``/private/tmp`` on macOS):
|
|
259
|
+
# a relative token computed against one spelling and resolved against
|
|
260
|
+
# the other points nowhere, and mypy then fails to start. An absolute
|
|
261
|
+
# path means the same file to every process.
|
|
262
|
+
wl_mypy_config = mypy_config_file(root_path, relative=False)
|
|
263
|
+
if wl_mypy_config is not None:
|
|
264
|
+
cmd.extend(["--config-file", str(wl_mypy_config)])
|
|
265
|
+
|
|
266
|
+
# Add every requested path, or the caller's fallback scope when none
|
|
267
|
+
# were requested. An explicit token is never dropped (#188): mypy
|
|
268
|
+
# reports the missing ones instead of lint silently narrowing.
|
|
269
|
+
if paths:
|
|
270
|
+
cmd.extend(_resolved_scope_tokens(tuple(paths), root))
|
|
271
|
+
else:
|
|
272
|
+
# Keep only fallback directories that exist (#174). ``or`` is the
|
|
273
|
+
# fail-closed floor: an all-missing fallback passes through
|
|
274
|
+
# unchanged, so mypy reports the missing scope itself.
|
|
275
|
+
cmd.extend(_existing_scope_tokens(fallback_paths, root) or fallback_paths)
|
|
276
|
+
|
|
277
|
+
# Add quiet flag if requested
|
|
278
|
+
if quiet:
|
|
279
|
+
cmd.append("--quiet")
|
|
280
|
+
|
|
281
|
+
return cmd
|
|
282
|
+
|
|
283
|
+
def run_mypy_command(
|
|
284
|
+
self, cmd: list[str], project_root: Path, quiet: bool
|
|
285
|
+
) -> SubprocessResult:
|
|
286
|
+
"""Run the mypy command and return the result (统一子进程执行)."""
|
|
287
|
+
executor = SubprocessExecutor()
|
|
288
|
+
capture = quiet
|
|
289
|
+
return executor.run(command=cmd, cwd=project_root, quiet=capture)
|
|
290
|
+
|
|
291
|
+
def run_ruff_command(
|
|
292
|
+
self, cmd: list[str], project_root: Path, quiet: bool, noreport: bool
|
|
293
|
+
) -> SubprocessResult:
|
|
294
|
+
"""Run the ruff command and return the result (统一子进程执行)."""
|
|
295
|
+
executor = SubprocessExecutor()
|
|
296
|
+
capture = quiet or not noreport
|
|
297
|
+
return executor.run(command=cmd, cwd=project_root, quiet=capture)
|
|
298
|
+
|
|
299
|
+
def run_pre_commit_command(self, project_root: Path) -> PreCommitRun:
|
|
300
|
+
"""Run the whole pre-commit suite once, for the report.
|
|
301
|
+
|
|
302
|
+
The verdict and the display text come from :class:`PreCommitRun`
|
|
303
|
+
(#165). Lint's only own decision is what a missing config means: it
|
|
304
|
+
falls back to pre-commit's default discovery instead of skipping the
|
|
305
|
+
run (#165 audit patch 2), which is why ``config`` is ``None`` there.
|
|
306
|
+
|
|
307
|
+
*Where* the managed config lives is not decided here: the shared
|
|
308
|
+
locator answers it with the ``DEPLOY`` anchor (#189). Looking it up at
|
|
309
|
+
the ``PROJECT`` root -- as this did -- missed the file whenever the
|
|
310
|
+
project root is a sub-package below the git top, and pre-commit then
|
|
311
|
+
failed with ``InvalidConfigError`` instead of discovering anything.
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
managed = managed_pre_commit_config(project_root)
|
|
315
|
+
config = managed if managed.exists() else None
|
|
316
|
+
# Bare ``pre-commit`` first (#172): lint already runs inside the
|
|
317
|
+
# project environment, so ``uv run`` would only re-resolve it.
|
|
318
|
+
# ``run_pre_commit`` falls back to ``uv run`` if the launcher is
|
|
319
|
+
# absent. Format keeps the uv-first default for its uv-managed env.
|
|
320
|
+
return run_pre_commit(
|
|
321
|
+
files=None,
|
|
322
|
+
config=config,
|
|
323
|
+
cwd=project_root,
|
|
324
|
+
quiet=True,
|
|
325
|
+
prefer_uv=False,
|
|
326
|
+
)
|
|
327
|
+
except FileNotFoundError as e:
|
|
328
|
+
# Defensive parity with the reporter this replaced: a launcher that
|
|
329
|
+
# cannot be found reads as "not installed", not as a config error.
|
|
330
|
+
return PreCommitRun(
|
|
331
|
+
SubprocessResult(
|
|
332
|
+
returncode=-1, stdout="", stderr=str(e), launch_error=str(e)
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
# The same catch-all the reporter had: an unreadable config path is
|
|
337
|
+
# a reported pre-commit failure, never a lint crash.
|
|
338
|
+
return PreCommitRun(
|
|
339
|
+
SubprocessResult(
|
|
340
|
+
returncode=-1,
|
|
341
|
+
stdout=f"Error running pre-commit dry-run: {e}",
|
|
342
|
+
stderr="",
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def run_all(
|
|
347
|
+
self,
|
|
348
|
+
project_root: Path,
|
|
349
|
+
paths: list[str] | None = None,
|
|
350
|
+
*,
|
|
351
|
+
quiet: bool = False,
|
|
352
|
+
fix: bool = False,
|
|
353
|
+
with_mypy: bool = True,
|
|
354
|
+
with_pre_commit: bool = True,
|
|
355
|
+
structured_ruff: bool = True,
|
|
356
|
+
format_code: int = 0,
|
|
357
|
+
) -> LintRun:
|
|
358
|
+
"""Run each requested tool once and return the finished :class:`LintRun`.
|
|
359
|
+
|
|
360
|
+
The caller picks the tool set (#170 audit patch 3): ``--noreport`` asks
|
|
361
|
+
for ruff alone, ``--report`` and the default path ask for all three.
|
|
362
|
+
``structured_ruff`` is the same choice seen from the report's side --
|
|
363
|
+
only a rendered report needs ruff's ``--output-format full``
|
|
364
|
+
diagnostics.
|
|
365
|
+
|
|
366
|
+
Order matches the report: ruff, then pre-commit, then mypy. Pre-commit
|
|
367
|
+
hooks may rewrite files, so mypy must run after them to judge what is
|
|
368
|
+
actually on disk.
|
|
369
|
+
"""
|
|
370
|
+
ruff_cmd = self.prepare_ruff_command(
|
|
371
|
+
paths,
|
|
372
|
+
fix,
|
|
373
|
+
quiet,
|
|
374
|
+
structured=structured_ruff,
|
|
375
|
+
project_root=project_root,
|
|
376
|
+
)
|
|
377
|
+
ruff = SubprocessExecutor().run(
|
|
378
|
+
command=ruff_cmd,
|
|
379
|
+
cwd=project_root,
|
|
380
|
+
# A structured run is captured even outside ``--quiet``: the
|
|
381
|
+
# report renders it instead of the child streaming it.
|
|
382
|
+
quiet=quiet or structured_ruff,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
pre_commit = (
|
|
386
|
+
self.run_pre_commit_command(project_root) if with_pre_commit else None
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
mypy: SubprocessResult | None = None
|
|
390
|
+
if with_mypy:
|
|
391
|
+
# ``quiet=False`` on purpose: the report's "MyPy Results" section
|
|
392
|
+
# has always shown mypy's own summary line, even under ``--quiet``.
|
|
393
|
+
mypy_cmd = self.prepare_mypy_command(
|
|
394
|
+
None, quiet=False, project_root=project_root
|
|
395
|
+
)
|
|
396
|
+
mypy = SubprocessExecutor().run(
|
|
397
|
+
command=mypy_cmd, cwd=project_root, quiet=True
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return LintRun(
|
|
401
|
+
ruff=ruff,
|
|
402
|
+
project_root=project_root,
|
|
403
|
+
mypy=mypy,
|
|
404
|
+
pre_commit=pre_commit,
|
|
405
|
+
paths=tuple(paths or ()),
|
|
406
|
+
quiet=quiet,
|
|
407
|
+
fix=fix,
|
|
408
|
+
format_code=format_code,
|
|
409
|
+
)
|