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,258 @@
|
|
|
1
|
+
"""One ``wl lint`` run, and the verdict it carries (#170).
|
|
2
|
+
|
|
3
|
+
Before this module a single ``wl lint`` started ruff four times (once for the
|
|
4
|
+
verdict, once per report section) and mypy three times (once for the main
|
|
5
|
+
section, once per ``error.md`` scope), while ``issues_found`` re-read ruff's
|
|
6
|
+
stdout a fourth time. The executor now runs each requested tool exactly once
|
|
7
|
+
and hands the finished results back as a :class:`LintRun`; the reporter only
|
|
8
|
+
renders it.
|
|
9
|
+
|
|
10
|
+
The value is deliberately dumb. It stores the subprocess results and derives
|
|
11
|
+
the two decisions lint makes -- :attr:`LintRun.exit_code` and
|
|
12
|
+
:attr:`LintRun.issues_found`. Both are read off ruff's *status*, never off its
|
|
13
|
+
stdout, which is what let the old reporter call a launch failure "no issues".
|
|
14
|
+
|
|
15
|
+
ruff's report section is its own ``--output-format full`` text, captured
|
|
16
|
+
verbatim (#170 review F1/F5). The old report stored exactly that text; the
|
|
17
|
+
first cut of this module ran ruff with ``--output-format json`` and rendered a
|
|
18
|
+
lossy concise line per finding, so the report shrank from ruff's snippets,
|
|
19
|
+
carets and ``help:`` lines down to one line each. Grouping the verbatim blocks
|
|
20
|
+
by the path on their ``-->`` line keeps the single execution *and* the old
|
|
21
|
+
report's content -- see :func:`parse_ruff_output`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from ...utils.pre_commit import PreCommitRun
|
|
29
|
+
from ...utils.subprocess_utils import SubprocessResult
|
|
30
|
+
|
|
31
|
+
#: ruff's ``full`` renderer locates every diagnostic with one arrow line. The
|
|
32
|
+
#: path is captured greedily so Windows drive letters (``C:\...:1:1``) and
|
|
33
|
+
#: paths containing spaces survive; the trailing ``:row:column`` is what makes
|
|
34
|
+
#: the match unambiguous.
|
|
35
|
+
_LOCATION_LINE = re.compile(r"^\s*--> (.+):(\d+):(\d+)$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def display_path(path: str, project_root: Path) -> str:
|
|
39
|
+
"""*path* relative to *project_root* when it lies under it, else unchanged.
|
|
40
|
+
|
|
41
|
+
Both sides are resolved first: mypy reports the real path, while the root
|
|
42
|
+
can arrive through a symlink (``/var`` vs ``/private/var`` on macOS), and
|
|
43
|
+
an unresolved comparison would leave every finding looking "outside" the
|
|
44
|
+
scope it belongs to.
|
|
45
|
+
"""
|
|
46
|
+
candidate = Path(path)
|
|
47
|
+
if not candidate.is_absolute():
|
|
48
|
+
return path
|
|
49
|
+
try:
|
|
50
|
+
return str(candidate.resolve().relative_to(project_root.resolve()))
|
|
51
|
+
except (OSError, ValueError):
|
|
52
|
+
return path
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class RuffBlock:
|
|
57
|
+
"""One diagnostic block, verbatim from ``ruff check --output-format full``.
|
|
58
|
+
|
|
59
|
+
``text`` keeps ruff's own header, ``-->`` location, source snippet, caret
|
|
60
|
+
and ``help:`` lines exactly as emitted. ``path``/``row``/``column`` are
|
|
61
|
+
lifted from the ``-->`` line so the reporter can group the block into its
|
|
62
|
+
per-directory section without re-rendering anything.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
text: str
|
|
66
|
+
path: str
|
|
67
|
+
row: int
|
|
68
|
+
column: int
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class RuffOutput:
|
|
73
|
+
"""One ruff run's full text, split into diagnostic blocks plus the rest.
|
|
74
|
+
|
|
75
|
+
``tail`` holds everything that is not a diagnostic block -- ruff's own
|
|
76
|
+
summary (``Found N errors.`` and the ``[*] N fixable ...`` line, including
|
|
77
|
+
the ``--unsafe-fixes`` hint) and ``All checks passed!``. Keeping it
|
|
78
|
+
verbatim is what makes the fixable count and the ``[*]`` markers ruff's
|
|
79
|
+
own facts instead of a second, hand-rolled tally (#170 review F2).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
blocks: tuple[RuffBlock, ...]
|
|
83
|
+
tail: tuple[str, ...]
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def empty(self) -> bool:
|
|
87
|
+
"""Whether the run produced no text at all (launch failure)."""
|
|
88
|
+
return not self.blocks and not self.tail
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_ruff_output(stdout: str) -> RuffOutput:
|
|
92
|
+
"""Split ``ruff check --output-format full`` text into blocks and tail.
|
|
93
|
+
|
|
94
|
+
ruff separates its rendered diagnostics with a blank line, and every
|
|
95
|
+
snippet line carries a gutter (`` |`` or ``12 | ...``), so a truly empty
|
|
96
|
+
line is always a block boundary. A block is a *diagnostic* when it carries
|
|
97
|
+
an ``--> path:row:column`` line; anything else (the summary, or a ruff
|
|
98
|
+
version that only printed a final sentence) is kept in the tail.
|
|
99
|
+
|
|
100
|
+
The function is pure and never re-renders: block text is handed back
|
|
101
|
+
byte-for-byte, which is what lets the differential test in
|
|
102
|
+
``tests/test_lint_report_equivalence.py`` compare against ruff itself.
|
|
103
|
+
"""
|
|
104
|
+
text = (stdout or "").replace("\r\n", "\n").replace("\r", "\n")
|
|
105
|
+
groups: list[list[str]] = []
|
|
106
|
+
current: list[str] = []
|
|
107
|
+
for line in text.split("\n"):
|
|
108
|
+
if not line.strip():
|
|
109
|
+
if current:
|
|
110
|
+
groups.append(current)
|
|
111
|
+
current = []
|
|
112
|
+
continue
|
|
113
|
+
current.append(line)
|
|
114
|
+
if current:
|
|
115
|
+
groups.append(current)
|
|
116
|
+
|
|
117
|
+
blocks: list[RuffBlock] = []
|
|
118
|
+
tail: list[str] = []
|
|
119
|
+
for group in groups:
|
|
120
|
+
location: re.Match[str] | None = None
|
|
121
|
+
for line in group:
|
|
122
|
+
location = _LOCATION_LINE.match(line)
|
|
123
|
+
if location is not None:
|
|
124
|
+
break
|
|
125
|
+
if location is None:
|
|
126
|
+
tail.extend(group)
|
|
127
|
+
continue
|
|
128
|
+
blocks.append(
|
|
129
|
+
RuffBlock(
|
|
130
|
+
text="\n".join(group),
|
|
131
|
+
path=location.group(1),
|
|
132
|
+
row=int(location.group(2)),
|
|
133
|
+
column=int(location.group(3)),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return RuffOutput(blocks=tuple(blocks), tail=tuple(tail))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class LintRun:
|
|
141
|
+
"""The finished tools of one ``wl lint`` execution.
|
|
142
|
+
|
|
143
|
+
``project_root``, ``quiet`` and ``fix`` are the inputs the render needs;
|
|
144
|
+
``paths`` is the scope as the user asked for it (``()`` means the default,
|
|
145
|
+
repository-wide scope). ``format_code`` is the #166 format prefix's verdict:
|
|
146
|
+
the caller stops before ruff when it is non-zero, so in practice it is
|
|
147
|
+
``0`` here, but keeping it in the value makes :attr:`exit_code` the single
|
|
148
|
+
place the whole lint exit contract is spelled (#170 audit patch 5).
|
|
149
|
+
|
|
150
|
+
Field order note: ``project_root`` has no default, so it sits beside
|
|
151
|
+
``ruff`` ahead of the optional results rather than after ``paths`` where the
|
|
152
|
+
ticket sketched it.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
ruff: SubprocessResult
|
|
156
|
+
project_root: Path
|
|
157
|
+
mypy: SubprocessResult | None = None
|
|
158
|
+
pre_commit: PreCommitRun | None = None
|
|
159
|
+
paths: tuple[str, ...] = ()
|
|
160
|
+
quiet: bool = False
|
|
161
|
+
fix: bool = False
|
|
162
|
+
format_code: int = 0
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def ruff_launch_failed(self) -> bool:
|
|
166
|
+
"""Whether ruff itself could not be started.
|
|
167
|
+
|
|
168
|
+
Asked of ``launch_error``, never of ``returncode < 0``: a child killed
|
|
169
|
+
by a signal also reports a negative code (#100).
|
|
170
|
+
"""
|
|
171
|
+
return self.ruff.launch_error is not None
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def exit_code(self) -> int:
|
|
175
|
+
"""The lint verdict: prefix, ruff, mypy, or a failing pre-commit hook.
|
|
176
|
+
|
|
177
|
+
A run that never started exits ``1`` rather than propagating the
|
|
178
|
+
executor's ``-1`` sentinel to ``sys.exit`` (where ``-1`` becomes 255).
|
|
179
|
+
A run that *was* killed by a signal is a failure too: ``subprocess``
|
|
180
|
+
reports it as ``-signum``, and ``max(format_code, -9)`` used to hand
|
|
181
|
+
back ``0`` -- a ruff killed by OOM/SIGKILL passed the gate (#170 review
|
|
182
|
+
F3). Both non-verdict states collapse to ``1``.
|
|
183
|
+
|
|
184
|
+
mypy follows the same rule whenever :attr:`mypy_failure` names a run
|
|
185
|
+
that produced no verdict -- it could not be started, was killed by a
|
|
186
|
+
signal, exited with code ``>= 2``, or exited non-zero with no
|
|
187
|
+
diagnostics at all -- and only on the paths that ran it (``--report``
|
|
188
|
+
and the default; ``--noreport`` never has a mypy result). An ordinary
|
|
189
|
+
``rc == 1`` mypy exit *with* diagnostics is a type-error verdict and
|
|
190
|
+
deliberately leaves the exit code alone (#172 wave 9 ruling).
|
|
191
|
+
|
|
192
|
+
A hook that *judged the check failed* is part of the verdict too
|
|
193
|
+
(#188): the report already rendered it as ``Failed`` while the exit
|
|
194
|
+
code said ``0``, so the terminal announced success for a red report.
|
|
195
|
+
Auto-fix-only runs stay green -- :attr:`PreCommitRun.hooks_failed`
|
|
196
|
+
already excludes blocks that merely rewrote files (#149).
|
|
197
|
+
"""
|
|
198
|
+
ruff_code = self.ruff.returncode
|
|
199
|
+
if self.ruff_launch_failed or self.ruff.was_signalled:
|
|
200
|
+
ruff_code = 1
|
|
201
|
+
code = max(self.format_code, ruff_code)
|
|
202
|
+
# One predicate for both the report and the exit code (#172 review
|
|
203
|
+
# M1): a mypy that never produced a verdict must not let the terminal
|
|
204
|
+
# announce success while the report says "mypy failed". Ordinary
|
|
205
|
+
# type errors answer ``None`` here and stay a verdict.
|
|
206
|
+
if self.mypy_failure is not None:
|
|
207
|
+
code = max(code, 1)
|
|
208
|
+
if self.pre_commit is not None and self.pre_commit.hooks_failed:
|
|
209
|
+
code = max(code, 1)
|
|
210
|
+
return code
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def issues_found(self) -> bool:
|
|
214
|
+
"""Whether ruff judged the code to have issues.
|
|
215
|
+
|
|
216
|
+
Only ruff's status is read. Its stdout is *not* consulted -- the old
|
|
217
|
+
reporter decided this by ``stdout.strip()``, so a launch failure (empty
|
|
218
|
+
stdout) read as "no issues" (#170).
|
|
219
|
+
"""
|
|
220
|
+
return self.ruff_launch_failed or self.ruff.returncode != 0
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def ruff_output(self) -> RuffOutput:
|
|
224
|
+
"""The captured ``full`` text of the one ruff run, as blocks + tail."""
|
|
225
|
+
return parse_ruff_output(self.ruff.stdout)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def mypy_failure(self) -> str | None:
|
|
229
|
+
"""A one-line description when mypy could not produce a verdict.
|
|
230
|
+
|
|
231
|
+
The exit code, not the presence of output, is the discriminator:
|
|
232
|
+
``0`` is clean, ``1`` means "type errors found" -- a verdict the gate
|
|
233
|
+
leaves alone -- and ``>= 2`` is abnormal termination (bad config, bad
|
|
234
|
+
command, internal error). So ``returncode >= 2`` is a failure whatever
|
|
235
|
+
mypy managed to print first; a broken mypy that emitted partial
|
|
236
|
+
diagnostics before dying must not pass for a type-error verdict
|
|
237
|
+
(#172 r1). ``rc == 1`` is a failure only when stdout is empty, which
|
|
238
|
+
is what a config file that cannot be found looks like:
|
|
239
|
+
``mypy: error: Cannot find config file ...`` on stderr, empty stdout.
|
|
240
|
+
A child killed by a signal is a failure regardless of what it managed
|
|
241
|
+
to print first: ``was_signalled`` is asked separately because a
|
|
242
|
+
``-signum`` exit with partial diagnostics would otherwise pass for a
|
|
243
|
+
normal type-error verdict (#172 wave 9). Naming these keeps the report
|
|
244
|
+
from answering "No issues found." to a run that never started (#170
|
|
245
|
+
review F4).
|
|
246
|
+
"""
|
|
247
|
+
result = self.mypy
|
|
248
|
+
if result is None:
|
|
249
|
+
return None
|
|
250
|
+
if result.launch_error is not None:
|
|
251
|
+
return f"mypy could not be started: {result.launch_error}"
|
|
252
|
+
if result.was_signalled:
|
|
253
|
+
return f"mypy was killed by signal {-result.returncode}."
|
|
254
|
+
if result.returncode >= 2:
|
|
255
|
+
return f"mypy failed with exit code {result.returncode}."
|
|
256
|
+
if result.returncode != 0 and not (result.stdout or "").strip():
|
|
257
|
+
return f"mypy failed with exit code {result.returncode}."
|
|
258
|
+
return None
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Single 'which mypy config file applies?' decision (map #35 / ticket #42).
|
|
2
|
+
|
|
3
|
+
Consolidates the three duplicated ``.wl/mypy.ini`` existence checks that used
|
|
4
|
+
to live in LintExecutor and LintReporter into one module, so moving the mypy
|
|
5
|
+
baseline (industrialization IS-2 keeps it in ``.wl/mypy.ini``) is a one-place
|
|
6
|
+
change.
|
|
7
|
+
|
|
8
|
+
When ``.wl/mypy.ini`` is absent (a fresh clone that has not run ``wl init``, or
|
|
9
|
+
a CI job that has not materialised it yet), the decision falls back to the
|
|
10
|
+
vendored ``vendors/config/mypy.ini`` template shipped inside the package, so
|
|
11
|
+
callers always get a usable ``--config-file``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from ...utils.root import Root, RootKind
|
|
18
|
+
|
|
19
|
+
# Vendored mypy baseline shipped with the package and deployed to
|
|
20
|
+
# ``.wl/mypy.ini`` by ``wl init``. ``parents[2]`` of this module is the
|
|
21
|
+
# ``wl_cli`` package directory.
|
|
22
|
+
_VENDORED_MYPY_INI = (
|
|
23
|
+
Path(__file__).resolve().parents[2] / "vendors" / "config" / "mypy.ini"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def mypy_config_file(project_root: Path, *, relative: bool = False) -> Path | None:
|
|
28
|
+
"""Return the mypy config to pass to ``--config-file``, or ``None``.
|
|
29
|
+
|
|
30
|
+
Prefers the project baseline ``.wl/mypy.ini``; when that is missing, falls
|
|
31
|
+
back to the vendored template shipped with the package. ``None`` is
|
|
32
|
+
returned only when neither is available.
|
|
33
|
+
|
|
34
|
+
Both the **selection** and the returned *form* are anchored at
|
|
35
|
+
*project_root* -- the directory mypy will run from. The default yields an
|
|
36
|
+
absolute path, which is what both callers use: ``LintExecutor`` (mypy runs
|
|
37
|
+
with *project_root* as cwd, but an absolute config cannot be broken by a
|
|
38
|
+
symlinked root spelling, #170 review F4) and ``wl gate`` when it names the
|
|
39
|
+
config in its warning. ``relative=True`` still yields a token relative to
|
|
40
|
+
*project_root*; both sides are resolved first, so a symlinked root cannot
|
|
41
|
+
turn the vendored fallback into ``../private/tmp/...``.
|
|
42
|
+
|
|
43
|
+
The selection must be anchored here, not at the process cwd: callers build
|
|
44
|
+
the command *before* running it (``LintExecutor.prepare_mypy_command`` can
|
|
45
|
+
be invoked from any directory, and ``wl gate`` builds every step up front),
|
|
46
|
+
so probing a bare ``Path(".wl/mypy.ini")`` would read the *caller's* cwd --
|
|
47
|
+
picking a different baseline whenever the two disagree, and falling back to
|
|
48
|
+
the vendored template when the project baseline is merely out of view (the
|
|
49
|
+
review F3 residual).
|
|
50
|
+
"""
|
|
51
|
+
# The ``.wl/mypy.ini`` layout lives in the root module; ask it rather than
|
|
52
|
+
# spelling the path here (#114). The derived path is a pure builder from
|
|
53
|
+
# *project_root*, so the kind is only a label, not a resolution.
|
|
54
|
+
candidate = Root(project_root, RootKind.PROJECT).mypy_ini()
|
|
55
|
+
try:
|
|
56
|
+
baseline_exists = candidate.exists()
|
|
57
|
+
except OSError:
|
|
58
|
+
# Same contract as the vendored probe below: a path that cannot be
|
|
59
|
+
# stat'ed means "not this baseline", never a crash.
|
|
60
|
+
baseline_exists = False
|
|
61
|
+
if baseline_exists:
|
|
62
|
+
if relative:
|
|
63
|
+
return _relative_to(candidate, project_root)
|
|
64
|
+
return candidate
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
vendored_exists = _VENDORED_MYPY_INI.exists()
|
|
68
|
+
except OSError:
|
|
69
|
+
return None
|
|
70
|
+
if not vendored_exists:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
if relative:
|
|
74
|
+
return _relative_to(_VENDORED_MYPY_INI, project_root)
|
|
75
|
+
return _VENDORED_MYPY_INI
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _relative_to(path: Path, project_root: Path) -> Path:
|
|
79
|
+
"""*path* expressed relative to *project_root*, with both sides resolved.
|
|
80
|
+
|
|
81
|
+
The vendored template is already resolved (``Path(__file__).resolve()``),
|
|
82
|
+
while *project_root* can still be a symlink spelling -- ``/tmp`` on macOS
|
|
83
|
+
resolves to ``/private/tmp``. A lexical ``os.path.relpath`` between the two
|
|
84
|
+
spellings produced a token like ``../../private/tmp/...`` that mypy could
|
|
85
|
+
not open; resolving both first keeps the token honest (#170 review F4).
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
return Path(os.path.relpath(path.resolve(), start=project_root.resolve()))
|
|
89
|
+
except OSError:
|
|
90
|
+
return Path(os.path.relpath(path, start=project_root))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Command registration and management module.
|
|
2
|
+
|
|
3
|
+
Single source of truth for the user-visible ``wl`` command surface is the
|
|
4
|
+
:data:`COMMAND_TABLE` defined below. Each entry maps a canonical (user-visible
|
|
5
|
+
Click) command name to a :class:`CommandSpec` describing where the Click
|
|
6
|
+
command object lives and how to obtain it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CommandSpec:
|
|
15
|
+
"""Static spec of one user-visible Click command on the ``wl`` group.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
module_path: Import path of the command module, relative to
|
|
19
|
+
:attr:`package` (e.g. ``"buildcommands.build"``).
|
|
20
|
+
symbol: Name of the module-level Click command object, or of a
|
|
21
|
+
zero-argument factory returning one.
|
|
22
|
+
aliases: Extra user-visible names registered alongside the canonical
|
|
23
|
+
name.
|
|
24
|
+
instantiate: If True, call the symbol to obtain the Click object
|
|
25
|
+
(``obj = obj()``); otherwise the module attribute is used as-is.
|
|
26
|
+
package: Import root :attr:`module_path` is resolved against. Legacy
|
|
27
|
+
commands live under ``wl_cli.commands``; domain namespaces (env /
|
|
28
|
+
cc / mcp, issue #220) live directly under ``wl_cli``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
module_path: str
|
|
32
|
+
symbol: str
|
|
33
|
+
aliases: tuple[str, ...] = ()
|
|
34
|
+
instantiate: bool = False
|
|
35
|
+
package: str = "wl_cli.commands"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#: Canonical user-visible command name -> its spec.
|
|
39
|
+
COMMAND_TABLE: dict[str, CommandSpec] = {
|
|
40
|
+
"lint": CommandSpec("lint.lint_command", "lint_command"),
|
|
41
|
+
"clean": CommandSpec("clean.clean", "clean_command", aliases=("c",)),
|
|
42
|
+
"cleanup": CommandSpec("cleanup", "cleanup_command"),
|
|
43
|
+
"format": CommandSpec("format.format_command", "format_command", aliases=("f",)),
|
|
44
|
+
"config": CommandSpec("config.config", "config_command"),
|
|
45
|
+
"test": CommandSpec("testcommands.test_commands", "test_command"),
|
|
46
|
+
"build": CommandSpec("buildcommands.build", "build_command", aliases=("b",)),
|
|
47
|
+
"builddist": CommandSpec("buildcommands.dist", "builddist_command"),
|
|
48
|
+
"buildtest": CommandSpec("buildcommands.test", "buildtest_command"),
|
|
49
|
+
"init": CommandSpec("initenv.initenv", "initenv_group", aliases=("i",)),
|
|
50
|
+
"self": CommandSpec("self.self_command", "self_command", instantiate=True),
|
|
51
|
+
"update": CommandSpec("update.update_command", "update_command"),
|
|
52
|
+
"workflow": CommandSpec("workflow", "workflow_command"),
|
|
53
|
+
"version": CommandSpec(
|
|
54
|
+
"version.version_command", "version_command", instantiate=True
|
|
55
|
+
),
|
|
56
|
+
"debug": CommandSpec("debug.debug", "debug_command"),
|
|
57
|
+
"git-flow": CommandSpec("git.git_flow", "git_flow_command"),
|
|
58
|
+
"lfs": CommandSpec("lfs.cli", "lfs_command"),
|
|
59
|
+
"gate": CommandSpec("gate.gate_command", "gate_command"),
|
|
60
|
+
# Domain namespaces (issue #220). Their Click groups live at
|
|
61
|
+
# ``src/wl_cli/<domain>/cli.py``, outside ``wl_cli.commands``, hence the
|
|
62
|
+
# explicit package; imports stay lazy — ``wl --help`` imports the module
|
|
63
|
+
# only when the group is registered, never at registry import time.
|
|
64
|
+
"env": CommandSpec("env.cli", "env_group", package="wl_cli"),
|
|
65
|
+
"cc": CommandSpec("cc.cli", "cc_group", package="wl_cli"),
|
|
66
|
+
"mcp": CommandSpec("mcp.cli", "mcp_group", package="wl_cli"),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#: Live alias -> canonical command name, derived from the table.
|
|
70
|
+
_ALIAS_TO_COMMAND: dict[str, str] = {
|
|
71
|
+
alias: name for name, spec in COMMAND_TABLE.items() for alias in spec.aliases
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def list_commands() -> list[str]:
|
|
76
|
+
"""Return the canonical user-visible command names in registration order."""
|
|
77
|
+
return list(COMMAND_TABLE)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_command(name: str):
|
|
81
|
+
"""Return the Click command/group object for a canonical name or alias.
|
|
82
|
+
|
|
83
|
+
The owning module is imported on first access. Aliases resolve to their
|
|
84
|
+
canonical command before lookup.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: A canonical command name or one of its aliases.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The Click command/group object.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: If ``name`` is not a known command or alias.
|
|
94
|
+
"""
|
|
95
|
+
resolved = _ALIAS_TO_COMMAND.get(name, name)
|
|
96
|
+
spec = COMMAND_TABLE.get(resolved)
|
|
97
|
+
if spec is None:
|
|
98
|
+
raise ValueError(f"Command '{name}' not found")
|
|
99
|
+
module = importlib.import_module(f"{spec.package}.{spec.module_path}")
|
|
100
|
+
obj = getattr(module, spec.symbol)
|
|
101
|
+
if spec.instantiate:
|
|
102
|
+
obj = obj()
|
|
103
|
+
return obj
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Backwards-compatible re-export of the shared capability registry (#221).
|
|
2
|
+
|
|
3
|
+
The single source of truth now lives in :mod:`wl_cli.capabilities`: both
|
|
4
|
+
``wl env check`` (``wl_cli.env``) and this package's deprecated
|
|
5
|
+
``wl self capabilities`` alias (``wl_cli.commands``) must serve the same
|
|
6
|
+
dotted names, and the quality domain may not import the env domain (#220).
|
|
7
|
+
This module keeps the historical import path working; it defines no data.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ....capabilities import (
|
|
13
|
+
CAPABILITIES,
|
|
14
|
+
CAPABILITY_SINCE,
|
|
15
|
+
SCHEMA_VERSION,
|
|
16
|
+
capabilities_payload,
|
|
17
|
+
capability_details,
|
|
18
|
+
capability_names,
|
|
19
|
+
capability_since,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"CAPABILITIES",
|
|
24
|
+
"CAPABILITY_SINCE",
|
|
25
|
+
"SCHEMA_VERSION",
|
|
26
|
+
"capabilities_payload",
|
|
27
|
+
"capability_details",
|
|
28
|
+
"capability_names",
|
|
29
|
+
"capability_since",
|
|
30
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration for self commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from wl_cli.utils.config import get_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SelfConfig:
|
|
12
|
+
"""Configuration container for self commands."""
|
|
13
|
+
|
|
14
|
+
cache_enabled: bool
|
|
15
|
+
cache_timeout: int
|
|
16
|
+
parallel_enabled: bool
|
|
17
|
+
max_workers: int
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def load(cls) -> "SelfConfig":
|
|
21
|
+
"""Load configuration from config store."""
|
|
22
|
+
return cls(
|
|
23
|
+
cache_enabled=get_config("cache_enabled", True),
|
|
24
|
+
cache_timeout=get_config("cache_timeout", 300),
|
|
25
|
+
parallel_enabled=get_config("parallel_enabled", True),
|
|
26
|
+
max_workers=get_config("max_workers", 4),
|
|
27
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic version cache with TTL support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VersionCache:
|
|
10
|
+
"""Generic cache for version information with time-based expiration."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, default_timeout: int = 300) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Initialize the version cache.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
default_timeout: Default cache timeout in seconds (default: 5 minutes)
|
|
18
|
+
"""
|
|
19
|
+
self._cache: dict[str, Any] = {}
|
|
20
|
+
self._last_update: dict[str, float] = {}
|
|
21
|
+
self._default_timeout = default_timeout
|
|
22
|
+
|
|
23
|
+
def get(self, key: str) -> Any | None:
|
|
24
|
+
"""
|
|
25
|
+
Get a cached value if it exists and hasn't expired.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
key: Cache key to retrieve
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The cached value if valid, None otherwise
|
|
32
|
+
"""
|
|
33
|
+
if key not in self._cache:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
current_time = time.time()
|
|
37
|
+
if current_time - self._last_update[key] > self._default_timeout:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return self._cache[key]
|
|
41
|
+
|
|
42
|
+
def set(self, key: str, value: Any) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Store a value in the cache.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
key: Cache key
|
|
48
|
+
value: Value to cache
|
|
49
|
+
"""
|
|
50
|
+
self._cache[key] = value
|
|
51
|
+
self._last_update[key] = time.time()
|
|
52
|
+
|
|
53
|
+
def get_or_set(self, key: str, supplier) -> Any:
|
|
54
|
+
"""
|
|
55
|
+
Get from cache or supply a new value.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
key: Cache key
|
|
59
|
+
supplier: Function to call if value not cached or expired
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The cached or newly supplied value
|
|
63
|
+
"""
|
|
64
|
+
value = self.get(key)
|
|
65
|
+
if value is None:
|
|
66
|
+
value = supplier()
|
|
67
|
+
if value is not None:
|
|
68
|
+
self.set(key, value)
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
def invalidate(self, key: str) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Invalidate a specific cache entry.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
key: Cache key to invalidate
|
|
77
|
+
"""
|
|
78
|
+
if key in self._cache:
|
|
79
|
+
del self._cache[key]
|
|
80
|
+
del self._last_update[key]
|
|
81
|
+
|
|
82
|
+
def clear(self) -> None:
|
|
83
|
+
"""Clear all cache entries."""
|
|
84
|
+
self._cache.clear()
|
|
85
|
+
self._last_update.clear()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subcommand handlers for self command.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base_handler import BaseHandler
|
|
6
|
+
from .install_handler import InstallHandler
|
|
7
|
+
from .status_handler import StatusHandler
|
|
8
|
+
from .uninstall_handler import UninstallHandler
|
|
9
|
+
from .update_handler import UpdateHandler
|
|
10
|
+
from .version_handler import VersionHandler
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BaseHandler",
|
|
14
|
+
"VersionHandler",
|
|
15
|
+
"UpdateHandler",
|
|
16
|
+
"InstallHandler",
|
|
17
|
+
"UninstallHandler",
|
|
18
|
+
"StatusHandler",
|
|
19
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for self command handlers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
from wl_cli.utils.logging import log_error_pair
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseHandler(ABC):
|
|
12
|
+
"""Abstract base class for self command subcommand handlers."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def execute(self) -> None:
|
|
16
|
+
"""Execute the subcommand."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def handle_error(self, error: Exception, exit_code: int = 1) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Handle an error with logging and exit.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
error: The exception to handle
|
|
25
|
+
exit_code: Exit code to use (default: 1)
|
|
26
|
+
"""
|
|
27
|
+
log_error_pair(f"Unexpected error: {error}", f"意外错误:{error}")
|
|
28
|
+
sys.exit(exit_code)
|