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,165 @@
|
|
|
1
|
+
"""Output formatter for wl-debug command.
|
|
2
|
+
|
|
3
|
+
Handles both text and JSON output modes with verbosity control.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sanitize_ansi(text: str) -> str:
|
|
13
|
+
"""Remove ANSI escape sequences from text.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
text: Text that may contain ANSI escape sequences
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Text with ANSI escape sequences removed
|
|
20
|
+
"""
|
|
21
|
+
ansi_escape = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
22
|
+
return ansi_escape.sub("", text)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OutputFormatter:
|
|
26
|
+
"""Output formatter for wl-debug command.
|
|
27
|
+
|
|
28
|
+
Supports text and JSON output modes with configurable verbosity levels.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
json_mode: bool = False,
|
|
34
|
+
verbosity: int = 0,
|
|
35
|
+
file=sys.stdout,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize output formatter.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
json_mode: Whether to output in JSON format
|
|
41
|
+
verbosity: Verbosity level (0, 1, or 2)
|
|
42
|
+
file: Output file object
|
|
43
|
+
"""
|
|
44
|
+
self.json_mode = json_mode
|
|
45
|
+
self.verbosity = max(0, min(2, verbosity)) # Ensure verbosity is 0-2
|
|
46
|
+
self.file = file
|
|
47
|
+
self._messages: list[dict[str, Any]] = []
|
|
48
|
+
self._finalized = False
|
|
49
|
+
|
|
50
|
+
def info(self, message: str, verbosity: int = 0, **kwargs) -> None:
|
|
51
|
+
"""Output an info message.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
message: Message text
|
|
55
|
+
verbosity: Minimum verbosity level required to show this message
|
|
56
|
+
**kwargs: Additional data to include in JSON output
|
|
57
|
+
"""
|
|
58
|
+
if verbosity <= self.verbosity:
|
|
59
|
+
self._output("INFO", message, **kwargs)
|
|
60
|
+
|
|
61
|
+
def error(self, message: str, verbosity: int = 0, **kwargs) -> None:
|
|
62
|
+
"""Output an error message.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
message: Message text
|
|
66
|
+
verbosity: Minimum verbosity level required to show this message
|
|
67
|
+
**kwargs: Additional data to include in JSON output
|
|
68
|
+
"""
|
|
69
|
+
if verbosity <= self.verbosity:
|
|
70
|
+
self._output("ERROR", message, **kwargs)
|
|
71
|
+
|
|
72
|
+
def success(self, message: str, verbosity: int = 0, **kwargs) -> None:
|
|
73
|
+
"""Output a success message.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
message: Message text
|
|
77
|
+
verbosity: Minimum verbosity level required to show this message
|
|
78
|
+
**kwargs: Additional data to include in JSON output
|
|
79
|
+
"""
|
|
80
|
+
if verbosity <= self.verbosity:
|
|
81
|
+
self._output("SUCCESS", message, **kwargs)
|
|
82
|
+
|
|
83
|
+
def _output(self, level: str, message: str, **kwargs) -> None:
|
|
84
|
+
"""Internal method to handle output.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
level: Message level (INFO, ERROR, SUCCESS)
|
|
88
|
+
message: Message text
|
|
89
|
+
**kwargs: Additional data
|
|
90
|
+
"""
|
|
91
|
+
if self._finalized:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Ensure message is a string
|
|
95
|
+
message_str = str(message) if message is not None else ""
|
|
96
|
+
|
|
97
|
+
# Convert kwargs to JSON-serializable types
|
|
98
|
+
safe_kwargs = {}
|
|
99
|
+
for key, value in kwargs.items():
|
|
100
|
+
try:
|
|
101
|
+
# Try to serialize first, if fails convert to string
|
|
102
|
+
json.dumps(value)
|
|
103
|
+
safe_kwargs[key] = value
|
|
104
|
+
except (TypeError, OverflowError):
|
|
105
|
+
safe_kwargs[key] = str(value)
|
|
106
|
+
|
|
107
|
+
if self.json_mode:
|
|
108
|
+
# Sanitize ANSI sequences in JSON mode only
|
|
109
|
+
sanitized_message = _sanitize_ansi(message_str)
|
|
110
|
+
# Also sanitize any string values in kwargs
|
|
111
|
+
sanitized_kwargs = {}
|
|
112
|
+
for k, v in safe_kwargs.items():
|
|
113
|
+
if isinstance(v, str):
|
|
114
|
+
sanitized_kwargs[k] = _sanitize_ansi(v)
|
|
115
|
+
else:
|
|
116
|
+
sanitized_kwargs[k] = v
|
|
117
|
+
|
|
118
|
+
self._messages.append(
|
|
119
|
+
{
|
|
120
|
+
"level": level,
|
|
121
|
+
"message": sanitized_message,
|
|
122
|
+
"verbosity": self.verbosity,
|
|
123
|
+
**sanitized_kwargs,
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
print(f"[{level}] {message_str}", file=self.file)
|
|
128
|
+
if safe_kwargs and self.verbosity >= 2:
|
|
129
|
+
for key, value in safe_kwargs.items():
|
|
130
|
+
print(f" {key}: {value}", file=self.file)
|
|
131
|
+
|
|
132
|
+
def finalize(self, success: bool = True, exit_code: int = 0, **kwargs) -> None:
|
|
133
|
+
"""Finalize the output.
|
|
134
|
+
|
|
135
|
+
In JSON mode, this will print the complete JSON structure.
|
|
136
|
+
In text mode, this can be used to output a final summary.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
success: Whether the operation was successful
|
|
140
|
+
exit_code: Exit code to include in JSON output
|
|
141
|
+
**kwargs: Additional data to include in the final output
|
|
142
|
+
"""
|
|
143
|
+
if self._finalized:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
self._finalized = True
|
|
147
|
+
|
|
148
|
+
if self.json_mode:
|
|
149
|
+
# Convert finalize kwargs to JSON-serializable types
|
|
150
|
+
safe_final_kwargs = {}
|
|
151
|
+
for key, value in kwargs.items():
|
|
152
|
+
try:
|
|
153
|
+
json.dumps(value)
|
|
154
|
+
safe_final_kwargs[key] = value
|
|
155
|
+
except (TypeError, OverflowError):
|
|
156
|
+
safe_final_kwargs[key] = str(value)
|
|
157
|
+
|
|
158
|
+
final_output = {
|
|
159
|
+
"success": success,
|
|
160
|
+
"exit_code": exit_code,
|
|
161
|
+
"messages": self._messages,
|
|
162
|
+
**safe_final_kwargs,
|
|
163
|
+
}
|
|
164
|
+
json.dump(final_output, self.file, ensure_ascii=False, indent=2)
|
|
165
|
+
print(file=self.file)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Source hasher module."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import hashlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SourceHasher:
|
|
9
|
+
"""Calculate combined hash of source files."""
|
|
10
|
+
|
|
11
|
+
DEFAULT_EXCLUDES = {
|
|
12
|
+
"__pycache__",
|
|
13
|
+
".pytest_cache",
|
|
14
|
+
".git",
|
|
15
|
+
# Directory-name token, not a derived path: matched against each path
|
|
16
|
+
# component while hashing, so it stays a bare name (#122).
|
|
17
|
+
".wl",
|
|
18
|
+
"node_modules",
|
|
19
|
+
"dist",
|
|
20
|
+
"build",
|
|
21
|
+
"venv",
|
|
22
|
+
".venv",
|
|
23
|
+
"env",
|
|
24
|
+
".mypy_cache",
|
|
25
|
+
".ruff_cache",
|
|
26
|
+
".cache",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def __init__(self, project_root: Path | None = None) -> None:
|
|
30
|
+
self.project_root = project_root or Path.cwd()
|
|
31
|
+
self.ignore_patterns = self._load_gitignore()
|
|
32
|
+
|
|
33
|
+
def compute_hash(self) -> str:
|
|
34
|
+
"""Calculate combined hash of all source files."""
|
|
35
|
+
try:
|
|
36
|
+
source_files = self._get_source_files()
|
|
37
|
+
hash_obj = hashlib.sha1()
|
|
38
|
+
for file_path in sorted(source_files):
|
|
39
|
+
content = file_path.read_bytes()
|
|
40
|
+
hash_obj.update(content)
|
|
41
|
+
return hash_obj.hexdigest()
|
|
42
|
+
except Exception:
|
|
43
|
+
# If any error occurs, return empty hash as fallback
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
def _load_gitignore(self) -> set[str]:
|
|
47
|
+
"""Load patterns from .gitignore file."""
|
|
48
|
+
try:
|
|
49
|
+
gitignore_path = self.project_root / ".gitignore"
|
|
50
|
+
if not gitignore_path.exists():
|
|
51
|
+
return set()
|
|
52
|
+
patterns = set()
|
|
53
|
+
with gitignore_path.open("r", encoding="utf-8") as f:
|
|
54
|
+
for line in f:
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if line and not line.startswith("#"):
|
|
57
|
+
patterns.add(line)
|
|
58
|
+
return patterns
|
|
59
|
+
except Exception:
|
|
60
|
+
# If any error occurs, return empty set as fallback
|
|
61
|
+
return set()
|
|
62
|
+
|
|
63
|
+
def _get_source_files(self) -> list[Path]:
|
|
64
|
+
"""Get list of source files to hash."""
|
|
65
|
+
try:
|
|
66
|
+
source_files = []
|
|
67
|
+
for py_file in self.project_root.rglob("*.py"):
|
|
68
|
+
relative_path = py_file.relative_to(self.project_root)
|
|
69
|
+
path_str = str(relative_path)
|
|
70
|
+
|
|
71
|
+
# Check default excludes
|
|
72
|
+
excluded = False
|
|
73
|
+
for part in relative_path.parts:
|
|
74
|
+
if part in self.DEFAULT_EXCLUDES:
|
|
75
|
+
excluded = True
|
|
76
|
+
break
|
|
77
|
+
if excluded:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Check gitignore patterns
|
|
81
|
+
for pattern in self.ignore_patterns:
|
|
82
|
+
if fnmatch.fnmatch(path_str, pattern) or any(
|
|
83
|
+
fnmatch.fnmatch(part, pattern) for part in relative_path.parts
|
|
84
|
+
):
|
|
85
|
+
excluded = True
|
|
86
|
+
break
|
|
87
|
+
if not excluded:
|
|
88
|
+
source_files.append(py_file)
|
|
89
|
+
return source_files
|
|
90
|
+
except Exception:
|
|
91
|
+
# If any error occurs, return empty list as fallback
|
|
92
|
+
return []
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Format command module."""
|
|
2
|
+
|
|
3
|
+
# One implementation class lives in ``format_command`` (#166); the package
|
|
4
|
+
# re-exports it so ``from ..format import FormatCommand`` (wl lint) keeps
|
|
5
|
+
# working against the same class the Click command uses.
|
|
6
|
+
#
|
|
7
|
+
# ``format_command`` itself is deliberately not re-exported (#172): binding the
|
|
8
|
+
# Click symbol here shadowed the same-named submodule, so ``format.format_command``
|
|
9
|
+
# meant two different things depending on how it was reached.
|
|
10
|
+
from .format_command import FormatCommand
|
|
11
|
+
from .format_coordinator import FormatCoordinator
|
|
12
|
+
|
|
13
|
+
__all__ = ["FormatCommand", "FormatCoordinator"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""The format cache adapter over the single cache seam (#213, ADR-0011).
|
|
2
|
+
|
|
3
|
+
Format results are cached per file: the key is the file's normalized path plus
|
|
4
|
+
its content, and each entry is its own file under ``<project>/.wl/cache/format``
|
|
5
|
+
(the old cache was one ``format_cache.json`` holding every entry). Key spelling,
|
|
6
|
+
atomic writes, freshness and the write error policy belong to
|
|
7
|
+
:class:`~wl_cli.utils.cache_ledger.CacheLedger`; this module only answers what is
|
|
8
|
+
*this* cache's: the inputs, the project-level location and the 7-day TTL.
|
|
9
|
+
|
|
10
|
+
The single-document cache rewrote only the fresh keys on every save, so stale
|
|
11
|
+
entries vanished by construction. Per-entry files have no such side effect, so
|
|
12
|
+
the adapter reclaims them once per run (``CacheLedger.purge_stale``) and renews
|
|
13
|
+
an entry's timestamp when it is served (``CacheLedger.touch``), keeping both the
|
|
14
|
+
bounded footprint and the "7 days after the last run that used it" semantics
|
|
15
|
+
(#213 review MEDIUM-2 / LOW-3).
|
|
16
|
+
|
|
17
|
+
The location is project-level (``.wl`` of the project root the process is in,
|
|
18
|
+
#125) and deliberately not unified with the user-level test/coverage caches:
|
|
19
|
+
ADR-0011 keeps each adapter's location as policy.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from ...utils.cache_ledger import CacheInputs, CacheLedger
|
|
25
|
+
from ...utils.root import RootKind
|
|
26
|
+
from ...utils.root_fallback import current_root
|
|
27
|
+
|
|
28
|
+
#: Format results stay fresh for 7 days -- this adapter's value, kept as-is.
|
|
29
|
+
TTL_SECONDS = 7 * 24 * 60 * 60
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FormatCacheAdapter:
|
|
33
|
+
"""Cache the "this file formatted cleanly" verdict, keyed by path + content."""
|
|
34
|
+
|
|
35
|
+
NAMESPACE = "format"
|
|
36
|
+
TTL_SECONDS = TTL_SECONDS
|
|
37
|
+
|
|
38
|
+
def __init__(self, store_dir: Path | None = None) -> None:
|
|
39
|
+
"""Initialize the adapter.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
store_dir: Entry directory. Defaults to
|
|
43
|
+
``<project root>/.wl/cache/format``, resolved on first use so
|
|
44
|
+
the adapter follows the project root of the process's current
|
|
45
|
+
directory (the behaviour the single-file cache had, #125), and
|
|
46
|
+
reused for the rest of the run (#213 review LOW-5).
|
|
47
|
+
"""
|
|
48
|
+
self._store_dir = store_dir
|
|
49
|
+
self._ledger_instance: CacheLedger | None = None
|
|
50
|
+
|
|
51
|
+
def lookup(self, file_path: Path, content: str) -> tuple[str, bool]:
|
|
52
|
+
"""Return this file's cache key and whether a fresh entry already exists.
|
|
53
|
+
|
|
54
|
+
A hit renews the entry's timestamp (``CacheLedger.touch``), so its
|
|
55
|
+
freshness is counted from the last run that used the verdict rather
|
|
56
|
+
than from the run that recorded it -- the old single-file cache
|
|
57
|
+
re-stamped what it served (#213 review LOW-3).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
file_path: The file about to be formatted.
|
|
61
|
+
content: Its current text, already read for the format run.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
``(key, hit)`` -- the key always, and True when the ledger holds a
|
|
65
|
+
fresh entry for it. A read failure is a miss, never an exception.
|
|
66
|
+
"""
|
|
67
|
+
ledger = self._ledger()
|
|
68
|
+
key = ledger.key(self.inputs(file_path, content))
|
|
69
|
+
return key, ledger.touch(key)
|
|
70
|
+
|
|
71
|
+
def inputs(self, file_path: Path, content: str) -> CacheInputs:
|
|
72
|
+
"""The ordered fragments that decide this file's key."""
|
|
73
|
+
path_str = str(file_path).replace("\\", "/")
|
|
74
|
+
return CacheInputs(self.NAMESPACE, (path_str, content))
|
|
75
|
+
|
|
76
|
+
def record(self, cache_key: str) -> bool:
|
|
77
|
+
"""Record a clean format result.
|
|
78
|
+
|
|
79
|
+
The ledger logs a write failure; the caller continues either way.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
cache_key: The key returned by :meth:`lookup`.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if the entry was written, False if the write failed.
|
|
86
|
+
"""
|
|
87
|
+
return self._ledger().put(cache_key, {"result": True})
|
|
88
|
+
|
|
89
|
+
def print_statistics(
|
|
90
|
+
self, cache_hits: int, total_files: int, error_occurred: bool
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Print how many files were served from the cache."""
|
|
93
|
+
cache_percent = (cache_hits / total_files) * 100
|
|
94
|
+
# Ensure we're on a new line before printing cache statistics
|
|
95
|
+
if not error_occurred:
|
|
96
|
+
print()
|
|
97
|
+
print(
|
|
98
|
+
f"Cache statistics: {cache_hits}/{total_files} files "
|
|
99
|
+
f"({cache_percent:.1f}%) loaded from cache"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _ledger(self) -> CacheLedger:
|
|
103
|
+
"""This run's ledger for the project's format cache.
|
|
104
|
+
|
|
105
|
+
Resolved once per adapter, which is once per ``wl format`` run: the
|
|
106
|
+
walk to the project root happens on first use, so the adapter still
|
|
107
|
+
follows the CWD of the call that started the run (#125), and every
|
|
108
|
+
later file in the same run reuses the answer instead of walking again
|
|
109
|
+
(#213 review LOW-5).
|
|
110
|
+
|
|
111
|
+
The first resolution also reclaims stale entries (#213 review
|
|
112
|
+
MEDIUM-2). Per-entry files never expire on their own, so without this
|
|
113
|
+
the store would grow with every changed file forever; the sweep runs
|
|
114
|
+
before any of this run's lookups, so what it deletes is exactly what
|
|
115
|
+
could no longer be a hit.
|
|
116
|
+
"""
|
|
117
|
+
ledger = self._ledger_instance
|
|
118
|
+
if ledger is None:
|
|
119
|
+
store_dir = self._store_dir
|
|
120
|
+
if store_dir is None:
|
|
121
|
+
store_dir = current_root(RootKind.PROJECT).wl_dir() / "cache" / "format"
|
|
122
|
+
ledger = CacheLedger(store_dir, self.TTL_SECONDS)
|
|
123
|
+
ledger.purge_stale()
|
|
124
|
+
self._ledger_instance = ledger
|
|
125
|
+
return ledger
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Format command implementation using Click decorators."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ...utils.logging import log_info_pair
|
|
10
|
+
from .format_coordinator import FormatCoordinator
|
|
11
|
+
from .format_executor import FormatExecutor
|
|
12
|
+
from .format_reporter import FormatReporter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FormatCommand:
|
|
16
|
+
"""Implementation of the format command."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize the format command implementation."""
|
|
20
|
+
self.coordinator = FormatCoordinator()
|
|
21
|
+
self.executor = FormatExecutor()
|
|
22
|
+
self.reporter = FormatReporter()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def name(self) -> str:
|
|
26
|
+
return "format"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def help(self) -> str:
|
|
30
|
+
return "Format code with ruff format - equivalent to make format"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def aliases(self) -> list[str]:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
def execute(
|
|
37
|
+
self,
|
|
38
|
+
quiet: bool = False,
|
|
39
|
+
paths: Sequence[str] | None = None,
|
|
40
|
+
unsafe: bool = True,
|
|
41
|
+
report: bool = False,
|
|
42
|
+
step_by_step: bool = False,
|
|
43
|
+
for_lint: bool = False,
|
|
44
|
+
project_root: Path | None = None,
|
|
45
|
+
) -> int:
|
|
46
|
+
"""
|
|
47
|
+
Format code - equivalent to make format
|
|
48
|
+
代码格式化 - 等效于 make format
|
|
49
|
+
|
|
50
|
+
Returns the exit code; only the Click wrapper turns it into a
|
|
51
|
+
``SystemExit`` (#164).
|
|
52
|
+
|
|
53
|
+
*project_root* is the directory the scope means (#188). ``wl lint``
|
|
54
|
+
passes its root down so the prefix formats ``root/src`` instead of
|
|
55
|
+
resolving ``src`` against the process cwd; ``None`` -- every direct
|
|
56
|
+
``wl format`` call -- keeps the historical cwd scope.
|
|
57
|
+
"""
|
|
58
|
+
if not quiet:
|
|
59
|
+
self._log_info("Formatting code...", "正在进行代码格式化...")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
return self.coordinator._execute_internal(
|
|
63
|
+
paths=list(paths) if paths else None,
|
|
64
|
+
quiet=quiet,
|
|
65
|
+
unsafe=unsafe,
|
|
66
|
+
report=report,
|
|
67
|
+
step_by_step=step_by_step,
|
|
68
|
+
for_lint=for_lint,
|
|
69
|
+
project_root=project_root,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
self._handle_file_not_found_error(quiet)
|
|
74
|
+
return 1
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self._handle_general_error(e, quiet)
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
def run(
|
|
80
|
+
self,
|
|
81
|
+
quiet: bool = False,
|
|
82
|
+
paths: Sequence[str] | None = None,
|
|
83
|
+
unsafe: bool = True,
|
|
84
|
+
report: bool = False,
|
|
85
|
+
step_by_step: bool = False,
|
|
86
|
+
for_lint: bool = False,
|
|
87
|
+
project_root: Path | None = None,
|
|
88
|
+
) -> int:
|
|
89
|
+
"""Lifecycle alias for :meth:`execute`.
|
|
90
|
+
|
|
91
|
+
``wl lint`` calls the format prefix through this name with
|
|
92
|
+
``for_lint=True``. The signature is spelled out -- rather than
|
|
93
|
+
``*args, **kwargs`` -- so mypy checks the caller: lint passes a
|
|
94
|
+
``list[str]``, which is why ``paths`` is a ``Sequence`` (#172), and
|
|
95
|
+
its project root, which #188 needs to keep the scope cwd-independent.
|
|
96
|
+
"""
|
|
97
|
+
return self.execute(
|
|
98
|
+
quiet=quiet,
|
|
99
|
+
paths=paths,
|
|
100
|
+
unsafe=unsafe,
|
|
101
|
+
report=report,
|
|
102
|
+
step_by_step=step_by_step,
|
|
103
|
+
for_lint=for_lint,
|
|
104
|
+
project_root=project_root,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def format_specified_paths(
|
|
108
|
+
self, paths, env, quiet, unsafe=False, step_by_step=False
|
|
109
|
+
):
|
|
110
|
+
"""Delegate to coordinator's format_specified_paths."""
|
|
111
|
+
return self.coordinator.format_specified_paths(
|
|
112
|
+
paths, env, quiet, unsafe, step_by_step=step_by_step
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def format_defaults(
|
|
116
|
+
self, current_path, env, quiet, unsafe, for_lint, step_by_step=False
|
|
117
|
+
):
|
|
118
|
+
"""Delegate to coordinator's format_defaults."""
|
|
119
|
+
return self.coordinator.format_defaults(
|
|
120
|
+
current_path, env, quiet, unsafe, for_lint, step_by_step=step_by_step
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _log_info(self, en_msg: str, zh_msg: str) -> None:
|
|
124
|
+
"""Log info message in both English and Chinese."""
|
|
125
|
+
log_info_pair(
|
|
126
|
+
en_msg,
|
|
127
|
+
zh_msg,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _handle_file_not_found_error(self, quiet: bool) -> None:
|
|
131
|
+
"""Handle FileNotFoundError."""
|
|
132
|
+
if not quiet:
|
|
133
|
+
self._log_info(
|
|
134
|
+
"Error: ruff is not installed or not found in PATH",
|
|
135
|
+
"错误:未安装 ruff 或在 PATH 中找不到",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _handle_general_error(self, e: Exception, quiet: bool) -> None:
|
|
139
|
+
"""Handle general exceptions."""
|
|
140
|
+
if not quiet:
|
|
141
|
+
self._log_info(
|
|
142
|
+
f"Error during formatting: {e}", f"错误:格式化期间出错: {e}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@click.command()
|
|
147
|
+
@click.argument("paths", nargs=-1)
|
|
148
|
+
@click.option("-q", "--quiet", is_flag=True, help="Suppress detailed output")
|
|
149
|
+
@click.option("--unsafe", is_flag=True, help="Enable ruff's unsafe fixes")
|
|
150
|
+
@click.option("--no-unsafe", "safe_mode", is_flag=True, help="Disable unsafe fixes")
|
|
151
|
+
@click.option(
|
|
152
|
+
"--report",
|
|
153
|
+
is_flag=True,
|
|
154
|
+
help="Generate format report in todos folder",
|
|
155
|
+
)
|
|
156
|
+
@click.option(
|
|
157
|
+
"-s",
|
|
158
|
+
"--step-by-step",
|
|
159
|
+
is_flag=True,
|
|
160
|
+
help="Format all files; report the first error",
|
|
161
|
+
)
|
|
162
|
+
def format_command(
|
|
163
|
+
paths: tuple[str, ...],
|
|
164
|
+
quiet: bool,
|
|
165
|
+
unsafe: bool,
|
|
166
|
+
safe_mode: bool,
|
|
167
|
+
report: bool,
|
|
168
|
+
step_by_step: bool,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Format command Click wrapper."""
|
|
171
|
+
cmd = FormatCommand()
|
|
172
|
+
if unsafe:
|
|
173
|
+
actual_unsafe = True
|
|
174
|
+
elif safe_mode:
|
|
175
|
+
actual_unsafe = False
|
|
176
|
+
else:
|
|
177
|
+
actual_unsafe = True
|
|
178
|
+
exit_code = cmd.execute(
|
|
179
|
+
paths=paths,
|
|
180
|
+
quiet=quiet,
|
|
181
|
+
unsafe=actual_unsafe,
|
|
182
|
+
report=report,
|
|
183
|
+
step_by_step=step_by_step,
|
|
184
|
+
)
|
|
185
|
+
# The only place the format command turns its contract into a process
|
|
186
|
+
# status (#164); everything below it returns an int. Zero returns normally,
|
|
187
|
+
# the same wrapper shape as lint/gate (#166).
|
|
188
|
+
if exit_code:
|
|
189
|
+
sys.exit(exit_code)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class FormatCommandImpl(FormatCoordinator):
|
|
193
|
+
"""Backward-compatible wrapper for FormatCommand using old interface."""
|
|
194
|
+
|
|
195
|
+
def __init__(self):
|
|
196
|
+
"""Initialize the format command implementation."""
|
|
197
|
+
super().__init__()
|