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.
Files changed (320) hide show
  1. py_wlcli-0.8.0.dist-info/METADATA +139 -0
  2. py_wlcli-0.8.0.dist-info/RECORD +320 -0
  3. py_wlcli-0.8.0.dist-info/WHEEL +4 -0
  4. py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
  5. py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
  6. wl_cli/__init__.py +27 -0
  7. wl_cli/__main__.py +130 -0
  8. wl_cli/capabilities.py +79 -0
  9. wl_cli/cc/__init__.py +5 -0
  10. wl_cli/cc/checks.py +1019 -0
  11. wl_cli/cc/cli.py +370 -0
  12. wl_cli/cc/mutate.py +1350 -0
  13. wl_cli/cli_click.py +156 -0
  14. wl_cli/commands/__init__.py +84 -0
  15. wl_cli/commands/base.py +80 -0
  16. wl_cli/commands/buildcommands/__init__.py +1 -0
  17. wl_cli/commands/buildcommands/build.py +71 -0
  18. wl_cli/commands/buildcommands/build_utils.py +47 -0
  19. wl_cli/commands/buildcommands/builders/__init__.py +18 -0
  20. wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
  21. wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
  22. wl_cli/commands/buildcommands/builders/environment.py +81 -0
  23. wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
  24. wl_cli/commands/buildcommands/builders/stubs.py +58 -0
  25. wl_cli/commands/buildcommands/dist.py +326 -0
  26. wl_cli/commands/buildcommands/test.py +89 -0
  27. wl_cli/commands/clean/__init__.py +1 -0
  28. wl_cli/commands/clean/build.py +151 -0
  29. wl_cli/commands/clean/clean.py +294 -0
  30. wl_cli/commands/clean/clean_utils.py +77 -0
  31. wl_cli/commands/clean/core.py +236 -0
  32. wl_cli/commands/clean/dead_code/__init__.py +0 -0
  33. wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
  34. wl_cli/commands/clean/dead_code/detector.py +161 -0
  35. wl_cli/commands/clean/dead_code/fixer.py +176 -0
  36. wl_cli/commands/clean/dead_code/reporter.py +295 -0
  37. wl_cli/commands/clean/dead_code/utils.py +0 -0
  38. wl_cli/commands/clean/lfs.py +215 -0
  39. wl_cli/commands/clean/python.py +81 -0
  40. wl_cli/commands/clean/rust.py +91 -0
  41. wl_cli/commands/clean/utils/__init__.py +34 -0
  42. wl_cli/commands/clean/venv.py +91 -0
  43. wl_cli/commands/cleanup.py +73 -0
  44. wl_cli/commands/click_base.py +79 -0
  45. wl_cli/commands/config/__init__.py +1 -0
  46. wl_cli/commands/config/config.py +291 -0
  47. wl_cli/commands/debug/__init__.py +6 -0
  48. wl_cli/commands/debug/debug.py +309 -0
  49. wl_cli/commands/debug/debug_cache.py +86 -0
  50. wl_cli/commands/debug/debug_subagent.py +156 -0
  51. wl_cli/commands/debug/output_formatter.py +165 -0
  52. wl_cli/commands/debug/source_hasher.py +92 -0
  53. wl_cli/commands/format/__init__.py +13 -0
  54. wl_cli/commands/format/format_cache_adapter.py +125 -0
  55. wl_cli/commands/format/format_command.py +197 -0
  56. wl_cli/commands/format/format_coordinator.py +191 -0
  57. wl_cli/commands/format/format_executor.py +470 -0
  58. wl_cli/commands/format/format_outcome.py +44 -0
  59. wl_cli/commands/format/format_path_handler.py +1016 -0
  60. wl_cli/commands/format/format_progress.py +62 -0
  61. wl_cli/commands/format/format_reporter.py +31 -0
  62. wl_cli/commands/format/python_formatter.py +506 -0
  63. wl_cli/commands/format/rust_formatter.py +106 -0
  64. wl_cli/commands/gate/__init__.py +1 -0
  65. wl_cli/commands/gate/gate_command.py +862 -0
  66. wl_cli/commands/gate/preflight.py +445 -0
  67. wl_cli/commands/git/__init__.py +1 -0
  68. wl_cli/commands/git/config.py +169 -0
  69. wl_cli/commands/git/git_facts.py +60 -0
  70. wl_cli/commands/git/git_flow.py +86 -0
  71. wl_cli/commands/git/hook.py +112 -0
  72. wl_cli/commands/git/policy.py +216 -0
  73. wl_cli/commands/git/push_update.py +82 -0
  74. wl_cli/commands/initenv/__init__.py +1 -0
  75. wl_cli/commands/initenv/common/__init__.py +23 -0
  76. wl_cli/commands/initenv/common/check_git_bash.py +78 -0
  77. wl_cli/commands/initenv/common/error_handler.py +68 -0
  78. wl_cli/commands/initenv/common/exceptions.py +52 -0
  79. wl_cli/commands/initenv/components/__init__.py +4 -0
  80. wl_cli/commands/initenv/components/cargo_sync.py +240 -0
  81. wl_cli/commands/initenv/components/config_manager.py +106 -0
  82. wl_cli/commands/initenv/components/directory_utils.py +71 -0
  83. wl_cli/commands/initenv/components/git/__init__.py +9 -0
  84. wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
  85. wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
  86. wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
  87. wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
  88. wl_cli/commands/initenv/components/git/git_utility.py +254 -0
  89. wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
  90. wl_cli/commands/initenv/components/git_initializer.py +212 -0
  91. wl_cli/commands/initenv/components/i18n_manager.py +104 -0
  92. wl_cli/commands/initenv/components/log_manager.py +241 -0
  93. wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
  94. wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
  95. wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
  96. wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
  97. wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
  98. wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
  99. wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
  100. wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
  101. wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
  102. wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
  103. wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
  104. wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
  105. wl_cli/commands/initenv/components/rust_initializer.py +145 -0
  106. wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
  107. wl_cli/commands/initenv/components/symlink_manager.py +52 -0
  108. wl_cli/commands/initenv/components/venv_manager.py +118 -0
  109. wl_cli/commands/initenv/initenv.py +169 -0
  110. wl_cli/commands/initenv/services/__init__.py +11 -0
  111. wl_cli/commands/initenv/services/environment_initializer.py +288 -0
  112. wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
  113. wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
  114. wl_cli/commands/lfs/README.md +221 -0
  115. wl_cli/commands/lfs/__init__.py +5 -0
  116. wl_cli/commands/lfs/cli.py +106 -0
  117. wl_cli/commands/lfs/config.py +164 -0
  118. wl_cli/commands/lfs/hook.py +61 -0
  119. wl_cli/commands/lfs/initializer.py +531 -0
  120. wl_cli/commands/lfs/manager.py +95 -0
  121. wl_cli/commands/lfs/policy.py +165 -0
  122. wl_cli/commands/lfs/watcher.py +294 -0
  123. wl_cli/commands/lint/__init__.py +1 -0
  124. wl_cli/commands/lint/lint_command.py +297 -0
  125. wl_cli/commands/lint/lint_executor.py +409 -0
  126. wl_cli/commands/lint/lint_reporter.py +579 -0
  127. wl_cli/commands/lint/lint_run.py +258 -0
  128. wl_cli/commands/lint/mypy_config.py +90 -0
  129. wl_cli/commands/registry.py +103 -0
  130. wl_cli/commands/self/__init__.py +7 -0
  131. wl_cli/commands/self/common/__init__.py +8 -0
  132. wl_cli/commands/self/common/capabilities.py +30 -0
  133. wl_cli/commands/self/common/config.py +27 -0
  134. wl_cli/commands/self/common/version_cache.py +85 -0
  135. wl_cli/commands/self/handlers/__init__.py +19 -0
  136. wl_cli/commands/self/handlers/base_handler.py +28 -0
  137. wl_cli/commands/self/handlers/install_handler.py +85 -0
  138. wl_cli/commands/self/handlers/status_handler.py +66 -0
  139. wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
  140. wl_cli/commands/self/handlers/update_handler.py +102 -0
  141. wl_cli/commands/self/handlers/version_handler.py +16 -0
  142. wl_cli/commands/self/self_command.py +237 -0
  143. wl_cli/commands/self/services/__init__.py +15 -0
  144. wl_cli/commands/self/services/installation_manager.py +187 -0
  145. wl_cli/commands/self/services/local_installer.py +165 -0
  146. wl_cli/commands/self/services/pypi_updater.py +278 -0
  147. wl_cli/commands/self/services/windows_update.py +174 -0
  148. wl_cli/commands/testcommands/__init__.py +8 -0
  149. wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
  150. wl_cli/commands/testcommands/batch_processor.py +278 -0
  151. wl_cli/commands/testcommands/cache_manager.py +1156 -0
  152. wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
  153. wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
  154. wl_cli/commands/testcommands/progress_manager.py +79 -0
  155. wl_cli/commands/testcommands/python_env_detector.py +207 -0
  156. wl_cli/commands/testcommands/report_generator.py +101 -0
  157. wl_cli/commands/testcommands/single_test_executor.py +250 -0
  158. wl_cli/commands/testcommands/speed_checker.py +118 -0
  159. wl_cli/commands/testcommands/test_command_builder.py +165 -0
  160. wl_cli/commands/testcommands/test_commands.py +786 -0
  161. wl_cli/commands/testcommands/test_reporter.py +181 -0
  162. wl_cli/commands/testcommands/test_result_handler.py +96 -0
  163. wl_cli/commands/testcommands/test_runner.py +1343 -0
  164. wl_cli/commands/testcommands/test_utils.py +197 -0
  165. wl_cli/commands/testcommands/utils/__init__.py +17 -0
  166. wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
  167. wl_cli/commands/testcommands/utils/env_info.py +36 -0
  168. wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
  169. wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
  170. wl_cli/commands/testcommands/utils/project_root.py +61 -0
  171. wl_cli/commands/update/__init__.py +9 -0
  172. wl_cli/commands/update/services/__init__.py +21 -0
  173. wl_cli/commands/update/services/backup_manager.py +207 -0
  174. wl_cli/commands/update/services/component_manager.py +83 -0
  175. wl_cli/commands/update/services/conflict_resolver.py +189 -0
  176. wl_cli/commands/update/services/vendors_updater.py +85 -0
  177. wl_cli/commands/update/services/version_tracker.py +127 -0
  178. wl_cli/commands/update/update_command.py +270 -0
  179. wl_cli/commands/update/utils/__init__.py +3 -0
  180. wl_cli/commands/version/__init__.py +9 -0
  181. wl_cli/commands/version/version_command.py +155 -0
  182. wl_cli/commands/workflow.py +477 -0
  183. wl_cli/env/__init__.py +5 -0
  184. wl_cli/env/cli.py +167 -0
  185. wl_cli/env_status.py +157 -0
  186. wl_cli/envelope.py +91 -0
  187. wl_cli/exceptions.py +85 -0
  188. wl_cli/lib/__init__.py +3 -0
  189. wl_cli/lib/rust_utils.py +35 -0
  190. wl_cli/mcp/__init__.py +6 -0
  191. wl_cli/mcp/capabilities.py +19 -0
  192. wl_cli/mcp/cli.py +22 -0
  193. wl_cli/mcp/gitlab.py +553 -0
  194. wl_cli/mcp/k8s.py +344 -0
  195. wl_cli/mcp/protocol.py +159 -0
  196. wl_cli/platform.py +139 -0
  197. wl_cli/quality/__init__.py +18 -0
  198. wl_cli/utils/__init__.py +7 -0
  199. wl_cli/utils/cache.py +491 -0
  200. wl_cli/utils/cache_ledger.py +355 -0
  201. wl_cli/utils/cache_store.py +546 -0
  202. wl_cli/utils/config/__init__.py +27 -0
  203. wl_cli/utils/config/config_loader.py +163 -0
  204. wl_cli/utils/config/core.py +862 -0
  205. wl_cli/utils/config/defaults.py +142 -0
  206. wl_cli/utils/config/env_exporter.py +86 -0
  207. wl_cli/utils/config/env_parser.py +108 -0
  208. wl_cli/utils/config/validation.py +280 -0
  209. wl_cli/utils/error_handler.py +165 -0
  210. wl_cli/utils/file_copier.py +56 -0
  211. wl_cli/utils/file_operations.py +506 -0
  212. wl_cli/utils/language_utils.py +122 -0
  213. wl_cli/utils/lazy_import.py +240 -0
  214. wl_cli/utils/log_levels.py +35 -0
  215. wl_cli/utils/log_rotators.py +74 -0
  216. wl_cli/utils/logging.py +458 -0
  217. wl_cli/utils/output_context.py +142 -0
  218. wl_cli/utils/performance/__init__.py +1 -0
  219. wl_cli/utils/performance/decorators.py +111 -0
  220. wl_cli/utils/performance/exporter.py +38 -0
  221. wl_cli/utils/performance/models.py +16 -0
  222. wl_cli/utils/performance/monitor.py +161 -0
  223. wl_cli/utils/performance_monitor.py +32 -0
  224. wl_cli/utils/platform_adapter.py +131 -0
  225. wl_cli/utils/pre_commit.py +266 -0
  226. wl_cli/utils/pre_commit_config.py +72 -0
  227. wl_cli/utils/root.py +521 -0
  228. wl_cli/utils/root_fallback.py +256 -0
  229. wl_cli/utils/run_evidence.py +236 -0
  230. wl_cli/utils/structured_logger.py +18 -0
  231. wl_cli/utils/structured_logging/__init__.py +19 -0
  232. wl_cli/utils/structured_logging/config.py +337 -0
  233. wl_cli/utils/structured_logging/core.py +177 -0
  234. wl_cli/utils/structured_logging/env.py +56 -0
  235. wl_cli/utils/structured_logging/filtering.py +75 -0
  236. wl_cli/utils/structured_logging/filters.py +55 -0
  237. wl_cli/utils/structured_logging/formatters.py +251 -0
  238. wl_cli/utils/structured_logging/handlers.py +308 -0
  239. wl_cli/utils/structured_logging/logger.py +354 -0
  240. wl_cli/utils/subprocess.py +71 -0
  241. wl_cli/utils/subprocess_utils.py +260 -0
  242. wl_cli/utils/test_utils/__init__.py +5 -0
  243. wl_cli/utils/test_utils/test_filter.py +36 -0
  244. wl_cli/utils/uv_run.py +79 -0
  245. wl_cli/utils/uv_tool.py +117 -0
  246. wl_cli/utils/uv_tree.py +71 -0
  247. wl_cli/utils/uv_workspace.py +174 -0
  248. wl_cli/utils/version/__init__.py +15 -0
  249. wl_cli/utils/version/pypi_version_checker.py +95 -0
  250. wl_cli/utils/version/version_comparator.py +133 -0
  251. wl_cli/utils/version/version_detectors.py +312 -0
  252. wl_cli/utils/version/version_service.py +131 -0
  253. wl_cli/utils/version/version_updater.py +198 -0
  254. wl_cli/utils/wl_dir_updater.py +1440 -0
  255. wl_cli/utils/workspace_detector/__init__.py +28 -0
  256. wl_cli/utils/workspace_detector/core.py +161 -0
  257. wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
  258. wl_cli/utils/workspace_detector/exceptions.py +5 -0
  259. wl_cli/utils/workspace_detector/types.py +17 -0
  260. wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
  261. wl_cli/vendors/config/.codespellrc +2 -0
  262. wl_cli/vendors/config/dependencies.toml +10 -0
  263. wl_cli/vendors/config/mypy.ini +62 -0
  264. wl_cli/vendors/config/pyproject_defaults.toml +28 -0
  265. wl_cli/vendors/config/template_config.toml +24 -0
  266. wl_cli/vendors/config/tool_configs.toml +76 -0
  267. wl_cli/vendors/git/.gitignore +122 -0
  268. wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
  269. wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
  270. wl_cli/vendors/hooks/commit-msg.sample +24 -0
  271. wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
  272. wl_cli/vendors/hooks/post-update.sample +8 -0
  273. wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
  274. wl_cli/vendors/hooks/pre-commit +45 -0
  275. wl_cli/vendors/hooks/pre-commit.legacy +31 -0
  276. wl_cli/vendors/hooks/pre-commit.sample +49 -0
  277. wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
  278. wl_cli/vendors/hooks/pre-push.sample +60 -0
  279. wl_cli/vendors/hooks/pre-rebase.sample +169 -0
  280. wl_cli/vendors/hooks/pre-receive.sample +24 -0
  281. wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
  282. wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
  283. wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
  284. wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
  285. wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
  286. wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
  287. wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
  288. wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
  289. wl_cli/vendors/hooks/unix/post-update.sample +8 -0
  290. wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
  291. wl_cli/vendors/hooks/unix/pre-commit +190 -0
  292. wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
  293. wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
  294. wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
  295. wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
  296. wl_cli/vendors/hooks/unix/pre-push +417 -0
  297. wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
  298. wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
  299. wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
  300. wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
  301. wl_cli/vendors/hooks/update.sample +128 -0
  302. wl_cli/vendors/hooks/win/work.bat +63 -0
  303. wl_cli/vendors/project/.gitignore +90 -0
  304. wl_cli/vendors/project/.python-version +1 -0
  305. wl_cli/vendors/project/LICENSE +21 -0
  306. wl_cli/vendors/project/README.md +0 -0
  307. wl_cli/vendors/project/pyproject.toml +27 -0
  308. wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
  309. wl_cli/vendors/project/pyproject_workspace.toml +14 -0
  310. wl_cli/vendors/readme/README.md +120 -0
  311. wl_cli/vendors/rust/Cargo.toml +14 -0
  312. wl_cli/vendors/rust/lib/__init__.py +3 -0
  313. wl_cli/vendors/rust/lib/rust_utils.py +31 -0
  314. wl_cli/vendors/rust/src/lib.rs +13 -0
  315. wl_cli/vendors/rust/src/main.rs +3 -0
  316. wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
  317. wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
  318. wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
  319. wl_cli/vendors/sop_doc/python-sop.md +342 -0
  320. 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__()