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,191 @@
1
+ """Format coordinator module."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from ...utils.output_context import get_output_config
7
+ from .format_outcome import FormatOutcome
8
+ from .format_path_handler import FormatPathHandler
9
+ from .format_reporter import FormatReporter
10
+ from .python_formatter import RuffCheckVerdict
11
+
12
+
13
+ class FormatCoordinator:
14
+ """Coordinator for format command."""
15
+
16
+ def __init__(self) -> None:
17
+ """Initialize the format coordinator.
18
+
19
+ The ``-> None`` keeps mypy checking this body: without it the two
20
+ collaborators stay ``Any`` and the typed aliases below would silently
21
+ return unchecked values (the same trap ``LintCommand.__init__`` notes).
22
+ """
23
+ self.path_handler = FormatPathHandler()
24
+ self.reporter = FormatReporter()
25
+
26
+ def execute(self, *args, **kwargs) -> int:
27
+ """Execute the format command; returns the process exit code."""
28
+ return self._execute_internal(**kwargs)
29
+
30
+ def run(self, *args, **kwargs) -> int:
31
+ """Run the format command (alias for execute to maintain compatibility)."""
32
+ return self._execute_internal(**kwargs)
33
+
34
+ def _execute_internal(self, **kwargs) -> int:
35
+ """Internal method to execute the format command.
36
+
37
+ Returns ``0`` when the run is green (or ``--report``/``wl lint`` own the
38
+ exit code) and ``1`` when a formatting failure must propagate (#164).
39
+ """
40
+ quiet = kwargs.get("quiet", False)
41
+ # Auto-detected concise mode (non-TTY / CI / Claude) must not hide a
42
+ # failure's diagnostics -- only an explicit ``-q/--quiet`` may (F1).
43
+ requested_quiet = bool(quiet)
44
+ verbose = kwargs.get("verbose", False)
45
+ unsafe = kwargs.get("unsafe", True) # Default to True as per specifications
46
+ report = kwargs.get("report", False)
47
+ step_by_step = kwargs.get("step_by_step", False)
48
+ paths = kwargs.get("paths", [])
49
+ for_lint = kwargs.get("for_lint", False)
50
+ # The directory the scope means (#188). ``wl lint`` passes its root
51
+ # down, so a prefix started from a subdirectory (or from anywhere the
52
+ # CLI was invoked) formats ``root/src`` instead of resolving ``src``
53
+ # against the process cwd and printing "Path src does not exist".
54
+ # ``None`` keeps the historical cwd scope for plain ``wl format``.
55
+ project_root = kwargs.get("project_root")
56
+
57
+ # Auto-detect quiet mode from output context if not explicitly set
58
+ if not quiet:
59
+ # If not explicitly quiet, check if we're in concise mode
60
+ config = get_output_config(override_quiet=quiet, override_verbose=verbose)
61
+ if config.concise_mode:
62
+ quiet = True
63
+
64
+ # Handle --no-unsafe flag if present
65
+ if "no_unsafe" in kwargs:
66
+ unsafe = not kwargs["no_unsafe"]
67
+
68
+ env = os.environ.copy()
69
+ current_path = Path(project_root) if project_root is not None else Path.cwd()
70
+ # A caller that pinned the root gets its explicit paths anchored there
71
+ # (#188): the path handler probes ``Path(p).exists()``, so a bare
72
+ # ``src`` from a different cwd would still be dropped. Absolute paths
73
+ # mean the same directory to the handler and to every ruff process.
74
+ if project_root is not None and paths:
75
+ paths = [
76
+ p if Path(p).is_absolute() else str(current_path / p) for p in paths
77
+ ]
78
+
79
+ # Handle report generation
80
+ if report:
81
+ self.generate_format_report(quiet, unsafe, paths, env)
82
+ return 0
83
+
84
+ try:
85
+ outcome: FormatOutcome
86
+ if paths:
87
+ outcome = self.format_specified_paths(
88
+ paths, env, quiet, unsafe, step_by_step, for_lint
89
+ )
90
+ else:
91
+ outcome = self.format_defaults(
92
+ current_path, env, quiet, unsafe, for_lint, step_by_step
93
+ )
94
+ if outcome.ok:
95
+ if not quiet:
96
+ print("Formatting completed.")
97
+ return outcome.exit_code
98
+ if for_lint:
99
+ return self._lint_prefix_exit_code(outcome, quiet, requested_quiet)
100
+ if not requested_quiet:
101
+ self._render_failures(outcome)
102
+ return outcome.exit_code
103
+ except Exception as e:
104
+ if not quiet:
105
+ print(f"Warning: Error occurred during formatting: {e}")
106
+ return 1
107
+
108
+ def _lint_prefix_exit_code(
109
+ self, outcome: FormatOutcome, quiet: bool, requested_quiet: bool
110
+ ) -> int:
111
+ """Map a ``for_lint`` outcome to the prefix's exit code (#166).
112
+
113
+ ``wl lint`` runs format as a prefix. A ``ruff``/``tool_missing``
114
+ failure means the files could not be formatted, so it stops lint
115
+ before ruff re-runs and renders its diagnostics.
116
+
117
+ Two verdicts stay non-fatal, because lint itself owns the authoritative
118
+ run for each:
119
+
120
+ * a ``pre_commit``-only verdict (#149/D3). Since #180 the prefix does
121
+ not run pre-commit at all for lint, so this branch now only guards a
122
+ ``pre_commit`` failure such as one forwarded by a caller that still
123
+ produces it -- the exit contract is unchanged.
124
+ * a :class:`RuffCheckVerdict`-only outcome (#188): ``ruff check --fix``
125
+ exiting ``1`` means *some violations were not auto-fixable*, which is
126
+ the very thing lint's own ruff run reports. Aborting here is what
127
+ used to skip ruff's structured run, mypy, pre-commit and the report
128
+ whenever the report was most needed.
129
+ """
130
+ fatal = tuple(
131
+ failure
132
+ for failure in outcome.failures
133
+ if failure.kind != "pre_commit"
134
+ and not isinstance(failure, RuffCheckVerdict)
135
+ )
136
+ if fatal:
137
+ if not requested_quiet:
138
+ self._render_failures(outcome)
139
+ return outcome.exit_code
140
+ if not quiet:
141
+ print("Formatting completed.")
142
+ return 0
143
+
144
+ def _render_failures(self, outcome: FormatOutcome) -> None:
145
+ """Print the failures that made the run red (F1).
146
+
147
+ ``format_python_file`` captures ruff's output (its quiet flag is
148
+ forced), so without this the user saw ``Formatting code...`` followed
149
+ by exit 1 and nothing else. Directory runs may already have streamed
150
+ the tool output; their message then falls back to a one-line
151
+ ``<step> failed with return code N``, so nothing is duplicated.
152
+
153
+ ``pre_commit`` details are the exception (#172): the path handler
154
+ already printed every failing block, from the same ``failed_blocks``
155
+ the verdict was computed from, at the moment it read the run. Printing
156
+ them again here was the duplicate the ticket is about; the failure
157
+ line and message below still say which run went red.
158
+ """
159
+ for failure in outcome.failures:
160
+ print(f"Formatting failed ({failure.kind}): {failure.path}")
161
+ if failure.message:
162
+ print(failure.message)
163
+ if failure.kind == "pre_commit":
164
+ continue
165
+ for detail in failure.details:
166
+ print(detail)
167
+
168
+ # 添加别名方法以匹配测试期望的接口
169
+ def format_specified_paths(
170
+ self, paths, env, quiet, unsafe=False, step_by_step=False, for_lint=False
171
+ ) -> FormatOutcome:
172
+ """Alias for path_handler.format_specified_paths.
173
+
174
+ ``for_lint`` reaches the handler so the ``wl lint`` prefix skips
175
+ pre-commit for explicit paths too (#180), not only for the defaults.
176
+ """
177
+ return self.path_handler.format_specified_paths(
178
+ paths, env, quiet, unsafe, step_by_step, for_lint
179
+ )
180
+
181
+ def format_defaults(
182
+ self, current_path, env, quiet, unsafe, for_lint, step_by_step=False
183
+ ) -> FormatOutcome:
184
+ """Alias for path_handler.format_defaults."""
185
+ return self.path_handler.format_defaults(
186
+ current_path, env, quiet, unsafe, for_lint, step_by_step
187
+ )
188
+
189
+ def generate_format_report(self, quiet, unsafe, paths, env):
190
+ """Alias for reporter.generate_format_report."""
191
+ self.reporter.generate_format_report(quiet, unsafe, paths, env)
@@ -0,0 +1,470 @@
1
+ """Format executor module."""
2
+
3
+ import concurrent.futures
4
+ import os
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from pathlib import Path
7
+
8
+ from .format_cache_adapter import FormatCacheAdapter
9
+ from .format_outcome import FormatFailure
10
+ from .format_progress import FormatProgress
11
+ from .python_formatter import (
12
+ FormatCommandResult,
13
+ RuffCheckVerdict,
14
+ _run_format_command,
15
+ )
16
+
17
+
18
+ class FormatExecutor:
19
+ """Executor for format operations."""
20
+
21
+ def __init__(self):
22
+ self.cache = FormatCacheAdapter()
23
+ self.progress = FormatProgress()
24
+ self._has_psutil = False
25
+ try:
26
+ import psutil
27
+
28
+ self._has_psutil = True
29
+ self._psutil = psutil
30
+ except ImportError:
31
+ pass
32
+
33
+ def get_optimal_workers(self) -> int:
34
+ """Get optimal number of workers based on system resources."""
35
+ # Get CPU core count
36
+ cpu_count = os.cpu_count()
37
+
38
+ # Handle case where cpu_count is None
39
+ if cpu_count is None:
40
+ return 4
41
+
42
+ # Calculate base workers based on CPU cores
43
+ base_workers = min(32, cpu_count * 2)
44
+
45
+ # Adjust based on available memory if psutil is available
46
+ if self._has_psutil:
47
+ try:
48
+ # Get available memory in GB
49
+ available_memory = self._psutil.virtual_memory().available / (1024**3)
50
+
51
+ # Adjust based on available memory
52
+ if available_memory < 2:
53
+ # Low memory, reduce workers
54
+ return max(1, base_workers // 2)
55
+ elif available_memory < 4:
56
+ # Moderate memory, use base workers
57
+ return base_workers
58
+ else:
59
+ # Sufficient memory, can increase workers slightly
60
+ return min(32, base_workers * 2)
61
+ except Exception:
62
+ # If psutil call fails, fall back to base workers
63
+ pass
64
+
65
+ # Fall back to CPU-based calculation
66
+ return base_workers
67
+
68
+ def get_optimal_batch_size(self, workers: int) -> int:
69
+ """Get optimal batch size based on number of workers."""
70
+ # Calculate base batch size
71
+ base_batch_size = workers * 10
72
+
73
+ # Cap at 100 to avoid excessive memory usage
74
+ return min(100, base_batch_size)
75
+
76
+ def calculate_resource_based_settings(self) -> tuple[int, int]:
77
+ """Calculate resource-based settings for parallel processing."""
78
+ workers = self.get_optimal_workers()
79
+ batch_size = self.get_optimal_batch_size(workers)
80
+ return workers, batch_size
81
+
82
+ @staticmethod
83
+ def _ruff_failure(
84
+ path_obj: Path, result: FormatCommandResult, step: str
85
+ ) -> FormatFailure:
86
+ """Classify one failed ruff command into the failure vocabulary."""
87
+ if result.tool_missing or result.launcher_failure is not None:
88
+ # #200 NIT-2: name the actual startup failure. ``tool_missing`` is
89
+ # the closed vocabulary's only "never ran" slot, but saying "ruff
90
+ # could not be launched" for a uv failure reads as "install ruff";
91
+ # a uv launcher failure gets its own wording, and a missing
92
+ # executable keeps the launch error it actually raised (F6: no
93
+ # over-attribution).
94
+ if result.launcher_failure is not None:
95
+ message = (
96
+ f"the uv launcher failed before ruff ran ({step}): "
97
+ f"{result.launcher_failure}"
98
+ )
99
+ else:
100
+ reason = result.launch_error or f"exit code {result.returncode}"
101
+ message = f"ruff could not be started ({step}): {reason}"
102
+ return FormatFailure(
103
+ path=path_obj,
104
+ kind="tool_missing",
105
+ message=message,
106
+ )
107
+ message = result.output.strip() or (
108
+ f"{step} failed with return code {result.returncode}"
109
+ )
110
+ return FormatFailure(path=path_obj, kind="ruff", message=message)
111
+
112
+ @staticmethod
113
+ def _ruff_check_failure(
114
+ path_obj: Path, result: FormatCommandResult
115
+ ) -> FormatFailure:
116
+ """Classify a failed ``ruff check`` run on one explicit file (#196).
117
+
118
+ The explicit single-file entry used to package *every* non-zero
119
+ ``ruff check`` as a plain formatter failure, so ``wl lint foo.py``
120
+ stopped at its format prefix and never reached its own ruff run,
121
+ mypy, pre-commit or the report -- the same early exit #188 fixed for
122
+ directory and default scopes.
123
+
124
+ Exit ``1`` from a ruff that *did* run means "violations remain after
125
+ the fixes": that is a lint verdict, carried as
126
+ :class:`RuffCheckVerdict`, which the coordinator lets the lint run
127
+ judge. This deliberately includes a file ruff cannot parse -- it
128
+ reports ``invalid-syntax`` on stdout with rc1, so the syntax error is a
129
+ check verdict here and ``ruff format`` never runs for it (#200).
130
+
131
+ Anything else -- a launcher that never ran, ``uv`` itself failing with
132
+ rc1 (:attr:`FormatCommandResult.launcher_failure`, #200), or exit
133
+ ``>= 2`` (ruff refused the config/arguments) -- is still a genuine
134
+ formatter failure and still stops the lint prefix.
135
+ """
136
+ if (
137
+ not result.tool_missing
138
+ and result.launcher_failure is None
139
+ and result.returncode == 1
140
+ ):
141
+ message = result.output.strip() or "ruff check left unfixed violations"
142
+ return RuffCheckVerdict(path=path_obj, kind="ruff", message=message)
143
+ return FormatExecutor._ruff_failure(path_obj, result, "ruff check")
144
+
145
+ def format_python_file(
146
+ self, path_obj: Path, original_path: str, env: dict, quiet: bool, unsafe: bool
147
+ ) -> FormatFailure | None:
148
+ """Format a single Python file; ``None`` means it succeeded (#164)."""
149
+ try:
150
+ # In step-by-step mode, we want to suppress all output except errors
151
+ # But we still need to capture error output to display it later
152
+ step_by_step_quiet = quiet or True # Always quiet in step-by-step mode
153
+
154
+ # Format with ruff check
155
+ ruff_check_cmd = ["uv", "run", "ruff", "check", "--fix"]
156
+ if unsafe:
157
+ ruff_check_cmd.append("--unsafe-fixes")
158
+ ruff_check_cmd.append(str(path_obj))
159
+
160
+ check_result = _run_format_command(
161
+ ruff_check_cmd, env, step_by_step_quiet, passthrough=False
162
+ )
163
+ if not check_result.ok:
164
+ # In step-by-step mode, we return the error output
165
+ # It will be displayed by the caller. A check run that left
166
+ # unfixable violations is a lint verdict, not a format
167
+ # failure (#196): ``wl lint``'s own ruff run owns it.
168
+ return self._ruff_check_failure(path_obj, check_result)
169
+
170
+ # Format with ruff format
171
+ format_result = _run_format_command(
172
+ [
173
+ "uv",
174
+ "run",
175
+ "ruff",
176
+ "format",
177
+ str(path_obj),
178
+ ],
179
+ env,
180
+ step_by_step_quiet,
181
+ passthrough=False,
182
+ )
183
+ if not format_result.ok:
184
+ # In step-by-step mode, we return the error output
185
+ # It will be displayed by the caller
186
+ return self._ruff_failure(path_obj, format_result, "ruff format")
187
+
188
+ return None
189
+ except Exception as e:
190
+ # In step-by-step mode, we return the error message
191
+ # It will be displayed by the caller
192
+ return FormatFailure(
193
+ path=path_obj,
194
+ kind="ruff",
195
+ message=f"Warning: Failed to format {original_path}: {e}",
196
+ )
197
+
198
+ @staticmethod
199
+ def _as_failure(result: object, file_path: Path) -> FormatFailure | None:
200
+ """Normalise ``format_python_file``'s answer to ``failure | None``.
201
+
202
+ ``None`` is success under the #164 contract. The tuple/bool branches
203
+ keep older test doubles (which still answer ``(bool, message)``) usable;
204
+ production only ever takes the ``FormatFailure | None`` path.
205
+ """
206
+ if result is None or isinstance(result, FormatFailure):
207
+ return result
208
+ if isinstance(result, tuple):
209
+ success, message = result
210
+ return None if success else FormatFailure(file_path, "ruff", message)
211
+ if isinstance(result, bool):
212
+ return None if result else FormatFailure(file_path, "ruff", "")
213
+ # F2: fail *closed*. An unrecognised return type is a contract breach,
214
+ # not evidence of success -- silently treating it as green is a
215
+ # fail-open channel into the exit code.
216
+ raise TypeError(
217
+ "format_python_file returned "
218
+ f"{type(result).__name__}; expected FormatFailure | None"
219
+ )
220
+
221
+ def process_single_file(
222
+ self,
223
+ file_path: Path,
224
+ env: dict,
225
+ quiet: bool,
226
+ unsafe: bool,
227
+ cache: dict[str, bool],
228
+ cache_hits: list[int],
229
+ ) -> tuple[FormatFailure | None, Path, str, str]:
230
+ """Process a single file with caching; first item is the failure.
231
+
232
+ ``cache`` is only a per-run memo of the keys recorded during this run --
233
+ the durable cache is the ledger, so a hit survives across processes.
234
+ """
235
+ try:
236
+ # Check if we're in test environment
237
+ is_test = "PYTEST_CURRENT_TEST" in os.environ
238
+
239
+ # Skip caching in test environment to speed up tests
240
+ if not is_test:
241
+ # Read file content for cache key generation with UTF-8 encoding
242
+ content = file_path.read_text(encoding="utf-8")
243
+ cache_key, cached_hit = self.cache.lookup(file_path, content)
244
+
245
+ # Check if result is in the ledger or in this run's memo
246
+ if cached_hit or cache_key in cache:
247
+ cache_hits[0] += 1
248
+ return None, file_path, "cache", ""
249
+ else:
250
+ cache_key = None
251
+
252
+ # Format the file
253
+ failure = self._as_failure(
254
+ self.format_python_file(file_path, str(file_path), env, quiet, unsafe),
255
+ file_path,
256
+ )
257
+
258
+ # Record the result if successful and not in test environment
259
+ if failure is None and cache_key and not is_test:
260
+ cache[cache_key] = True
261
+ self.cache.record(cache_key)
262
+
263
+ return (
264
+ failure,
265
+ file_path,
266
+ "processed" if failure is None else "failed",
267
+ failure.message if failure is not None else "",
268
+ )
269
+ except Exception as e:
270
+ error_message = f"Error processing {file_path}: {e}"
271
+ if not quiet:
272
+ print(error_message)
273
+ return (
274
+ FormatFailure(file_path, "ruff", error_message),
275
+ file_path,
276
+ "error",
277
+ error_message,
278
+ )
279
+
280
+ def process_files_parallel_quiet(
281
+ self,
282
+ files: list[Path],
283
+ env: dict,
284
+ quiet: bool,
285
+ unsafe: bool,
286
+ cache: dict[str, bool],
287
+ cache_hits: list[int],
288
+ ) -> tuple[FormatFailure, ...]:
289
+ """Process files in parallel without progress bar.
290
+
291
+ Aggregates **every** failure in input order; the step-by-step layer
292
+ decides where to truncate (#164, audit patch 2 / review F3).
293
+ """
294
+ max_workers, batch_size = self.calculate_resource_based_settings()
295
+ total_files = len(files)
296
+ failures: list[tuple[int, FormatFailure]] = []
297
+
298
+ # Process files in batches
299
+ for i in range(0, total_files, batch_size):
300
+ batch_files = files[i : i + batch_size]
301
+
302
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
303
+ # Submit tasks for current batch
304
+ future_to_index = {
305
+ executor.submit(
306
+ self.process_single_file,
307
+ file_path,
308
+ env,
309
+ quiet,
310
+ unsafe,
311
+ cache,
312
+ cache_hits,
313
+ ): i + offset
314
+ for offset, file_path in enumerate(batch_files)
315
+ }
316
+
317
+ # Process results as they complete
318
+ for future in concurrent.futures.as_completed(future_to_index):
319
+ index = future_to_index[future]
320
+ try:
321
+ failure, _, _, _ = future.result()
322
+ if failure is not None:
323
+ failures.append((index, failure))
324
+ except Exception as e:
325
+ failures.append(
326
+ (
327
+ index,
328
+ FormatFailure(
329
+ files[index],
330
+ "ruff",
331
+ f"Error processing {files[index]}: {e}",
332
+ ),
333
+ )
334
+ )
335
+
336
+ failures.sort(key=lambda item: item[0])
337
+ return tuple(failure for _, failure in failures)
338
+
339
+ def process_files_parallel_with_progress(
340
+ self,
341
+ files: list[Path],
342
+ env: dict,
343
+ quiet: bool,
344
+ unsafe: bool,
345
+ cache: dict[str, bool],
346
+ cache_hits: list[int],
347
+ ) -> tuple[tuple[FormatFailure, ...], int, bool]:
348
+ """Process files in parallel with progress bar.
349
+
350
+ First tuple item holds every failure in input order (#164).
351
+ """
352
+ total_files = len(files)
353
+ processed_files = 0
354
+ error_occurred = False
355
+ failures: list[tuple[int, FormatFailure]] = []
356
+ max_workers, batch_size = self.calculate_resource_based_settings()
357
+
358
+ # Create progress bar with optimized settings
359
+ pbar = self.progress.create_progress_bar(total_files)
360
+
361
+ try:
362
+ # Process files in batches
363
+ for i in range(0, total_files, batch_size):
364
+ batch_files = files[i : i + batch_size]
365
+
366
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
367
+ # Submit tasks for current batch
368
+ future_to_index = {
369
+ executor.submit(
370
+ self.process_single_file,
371
+ file_path,
372
+ env,
373
+ quiet,
374
+ unsafe,
375
+ cache,
376
+ cache_hits,
377
+ ): i + offset
378
+ for offset, file_path in enumerate(batch_files)
379
+ }
380
+
381
+ # Process results as they complete
382
+ for future in concurrent.futures.as_completed(future_to_index):
383
+ index = future_to_index[future]
384
+ file_path = files[index]
385
+ try:
386
+ failure, _, status, error_message = future.result()
387
+ processed_files += 1
388
+
389
+ # Update progress bar
390
+ truncated_name = self.progress.truncate_filename(
391
+ file_path.name
392
+ )
393
+ self.progress.update_progress_bar(
394
+ pbar,
395
+ truncated_name,
396
+ status,
397
+ )
398
+
399
+ if failure is not None:
400
+ error_occurred = True
401
+ self.progress.print_format_error(
402
+ file_path, error_message
403
+ )
404
+ failures.append((index, failure))
405
+ except Exception as e:
406
+ error_occurred = True
407
+ self.progress.print_processing_error(file_path, e)
408
+ failures.append(
409
+ (
410
+ index,
411
+ FormatFailure(
412
+ file_path,
413
+ "ruff",
414
+ f"Error processing {file_path}: {e}",
415
+ ),
416
+ )
417
+ )
418
+ finally:
419
+ # Ensure progress bar shows 100% before closing
420
+ self.progress.close_progress_bar(pbar, total_files)
421
+
422
+ failures.sort(key=lambda item: item[0])
423
+ return (
424
+ tuple(failure for _, failure in failures),
425
+ processed_files,
426
+ error_occurred,
427
+ )
428
+
429
+ def run_format_parallel(
430
+ self, files: list[Path], env: dict, quiet: bool, unsafe: bool
431
+ ) -> tuple[FormatFailure, ...]:
432
+ """Run format commands in parallel with caching.
433
+
434
+ Returns every failure in input order; an empty tuple means every file
435
+ succeeded. Truncation for step-by-step mode happens in the path
436
+ handler, not here (#164, review F3).
437
+ """
438
+ # Check if we're in test environment
439
+ is_test = "PYTEST_CURRENT_TEST" in os.environ
440
+
441
+ # Skip caching in test environment to speed up tests. Nothing is loaded
442
+ # up front: the ledger answers each file's hit, and the entries are
443
+ # written per file as they succeed (the cache is per-entry files, not
444
+ # one document rewritten at the end).
445
+ cache: dict[str, bool] = {}
446
+ cache_hits = [0] # Use list to allow modification in nested function
447
+ total_files = len(files)
448
+ failures: tuple[FormatFailure, ...] = ()
449
+ error_occurred = False
450
+ processed_files = 0
451
+
452
+ if files:
453
+ if quiet:
454
+ # Process files in parallel without progress bar
455
+ failures = self.process_files_parallel_quiet(
456
+ files, env, quiet, unsafe, cache, cache_hits
457
+ )
458
+ else:
459
+ # Process files in parallel with progress bar
460
+ failures, processed_files, error_occurred = (
461
+ self.process_files_parallel_with_progress(
462
+ files, env, quiet, unsafe, cache, cache_hits
463
+ )
464
+ )
465
+
466
+ # Print cache statistics only if not in test environment
467
+ if not quiet and total_files > 0 and not is_test:
468
+ self.cache.print_statistics(cache_hits[0], total_files, error_occurred)
469
+
470
+ return failures