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,62 @@
1
+ """Format progress module."""
2
+
3
+ from pathlib import Path
4
+
5
+ from tqdm import tqdm
6
+
7
+
8
+ class FormatProgress:
9
+ """Progress manager for format operations."""
10
+
11
+ def truncate_filename(self, filename: str) -> str:
12
+ """Truncate filename to fit in progress bar."""
13
+ max_len = 28
14
+ if len(filename) > max_len:
15
+ return filename[: max_len - 3] + "..."
16
+ return filename
17
+
18
+ def create_progress_bar(self, total_files: int) -> tqdm:
19
+ """Create a progress bar for file processing."""
20
+ return tqdm(
21
+ total=total_files,
22
+ unit="file",
23
+ leave=False,
24
+ dynamic_ncols=True,
25
+ bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
26
+ )
27
+
28
+ def update_progress_bar(
29
+ self,
30
+ pbar: tqdm,
31
+ truncated_name: str,
32
+ status: str,
33
+ ) -> None:
34
+ """Update progress bar with current status."""
35
+ pbar.set_description(f"Processing {truncated_name:<28} [{status}]")
36
+ pbar.update(1)
37
+
38
+ def close_progress_bar(self, pbar: tqdm, total_files: int) -> None:
39
+ """Close the progress bar and ensure it shows 100%."""
40
+ try:
41
+ # Ensure progress bar shows 100% before closing
42
+ if total_files > 0:
43
+ pbar.n = total_files
44
+ pbar.refresh()
45
+ finally:
46
+ # Always close the progress bar
47
+ pbar.disable = True
48
+ pbar.close()
49
+
50
+ def print_format_error(self, file_path: Path, error_message: str) -> None:
51
+ """Print format error message."""
52
+ print()
53
+ print(f"Formatting failed for file: {file_path}")
54
+ # Print the specific error message if available
55
+ if error_message:
56
+ print(f"Error details: {error_message}")
57
+ print("Stopping at first error as requested.")
58
+
59
+ def print_processing_error(self, file_path: Path, error: Exception) -> None:
60
+ """Print processing error message."""
61
+ print()
62
+ print(f"Error processing {file_path}: {error}")
@@ -0,0 +1,31 @@
1
+ """Format reporter module."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class FormatReporter:
7
+ """Reporter for format command."""
8
+
9
+ def generate_format_report(self, quiet, unsafe, paths, env):
10
+ """Generate format report."""
11
+ # Create todos directory if it doesn't exist
12
+ todos_dir = Path("todos")
13
+ todos_dir.mkdir(exist_ok=True)
14
+
15
+ # Generate report content
16
+ report_lines = []
17
+ report_lines.append("# Format Report\n")
18
+ report_lines.append("## Configuration\n")
19
+ report_lines.append(f"- Quiet mode: {quiet}\n")
20
+ report_lines.append(f"- Unsafe fixes: {unsafe}\n")
21
+ report_lines.append(
22
+ f"- Custom paths: {paths if paths else 'None (using defaults)'}\n"
23
+ )
24
+
25
+ # Save report
26
+ report_path = todos_dir / "format_report.md"
27
+ with open(report_path, "w", encoding="utf-8") as f:
28
+ f.write("\n".join(report_lines))
29
+
30
+ if not quiet:
31
+ print(f"Format report generated at {report_path}")
@@ -0,0 +1,506 @@
1
+ """
2
+ Python code formatting utilities for WL Commands."""
3
+
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from ...utils.subprocess_utils import SubprocessExecutor, SubprocessResult
9
+ from ...utils.uv_run import run_uv_command
10
+ from .format_outcome import FormatFailure
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FormatCommandResult:
15
+ """Small result value for one formatter command (#164).
16
+
17
+ Carrying ``returncode`` and ``launch_error`` -- rather than a bool -- is
18
+ what lets the caller tell "ruff judged the file bad" from "ruff never ran"
19
+ without re-sniffing output text.
20
+
21
+ ``launcher_failure`` carries the one case those two fields cannot express
22
+ (#200): ``uv run ruff ...`` exiting non-zero because **uv** failed -- an
23
+ unsatisfiable dependency, an offline cache miss, a malformed ``uv.lock``
24
+ -- with ruff never started. Those exit codes are otherwise the same ones
25
+ ruff uses for its own verdicts (``1`` for "violations remain", ``2`` for a
26
+ file or config it refuses), so the streams are what tell them apart (see
27
+ :func:`_launcher_failure` and :func:`_confirm_launcher_failure`), never a
28
+ substring of the text.
29
+ """
30
+
31
+ ok: bool
32
+ output: str
33
+ returncode: int | None
34
+ launch_error: str | None
35
+ #: The launcher's own diagnostic when ``uv`` failed before its child could
36
+ #: produce a verdict; ``None`` when the child (whatever it said) ran.
37
+ launcher_failure: str | None = None
38
+
39
+ @property
40
+ def tool_missing(self) -> bool:
41
+ """True when the formatter never got to run (not a format verdict)."""
42
+ return self.launch_error is not None or self.returncode == 127
43
+
44
+
45
+ def _launcher_failure(stdout: str, stderr: str, returncode: int | None) -> str | None:
46
+ """``uv``'s own failure text, or ``None`` when the child produced a verdict.
47
+
48
+ #200: both ``uv`` failing and ``ruff check`` finding unfixable violations
49
+ exit ``1``. The streams separate them without sniffing either program's
50
+ wording:
51
+
52
+ * ruff prints **every** diagnostic to stdout, and its rc1 summary
53
+ (``Found N errors.``) there too, so a real verdict always leaves text on
54
+ stdout;
55
+ * uv reports its own failures on stderr and writes nothing to stdout.
56
+
57
+ A non-zero exit with an empty stdout and a non-empty stderr is therefore a
58
+ *candidate* launcher failure, and that stderr text is its reason.
59
+
60
+ rc ``>= 2`` cannot be settled here. ``uv`` exits ``2`` when it cannot start
61
+ the command at all (a malformed ``uv.lock``, an unrecognised flag), but
62
+ real ruff *also* exits ``2`` with an empty stdout and its own text on
63
+ stderr when it refuses a file (``error: Failed to parse ...``) or its
64
+ config (``ruff failed / Cause: Unknown rule selector``) -- measured
65
+ against real ruff, not assumed. :func:`_confirm_launcher_failure` settles
66
+ that case with a differential probe; this function only answers "could
67
+ have been the launcher".
68
+
69
+ Trade-off for rc1, deliberately fail-*open*: an rc1 that is silent on both
70
+ streams stays a ruff verdict. There is no evidence a launcher failed, and
71
+ guessing "formatter failure" there would abort ``wl lint``'s prefix --
72
+ skipping its own ruff run, mypy and the report, the regression #188 fixed.
73
+ If uv did fail silently, lint's own independent ruff run still reports the
74
+ breakage.
75
+ """
76
+ if returncode is None or returncode < 1:
77
+ return None
78
+ if stdout.strip() or not stderr.strip():
79
+ return None
80
+ return stderr.strip()
81
+
82
+
83
+ def _confirm_launcher_failure(
84
+ executor: SubprocessExecutor,
85
+ command: list,
86
+ env: dict,
87
+ result: SubprocessResult,
88
+ candidate: str,
89
+ ) -> str | None:
90
+ """Decide whether an ambiguous rc2 failure was uv's, not the child's.
91
+
92
+ Since ruff's own refusals share uv's rc2 shape (empty stdout, text on
93
+ stderr), the streams cannot separate "ruff refused this file/config" from
94
+ "ruff never started". The discriminator that does not read either
95
+ program's wording is differential: re-run the same child without the
96
+ ``uv run`` prefix. A child that produces a *different* outcome when run
97
+ directly never got that far through uv, so the launcher failed; the same
98
+ shape of failure -- the same exit code, nothing on stdout -- is the
99
+ child's own diagnostic.
100
+
101
+ #215: "same shape" deliberately excludes the stderr *text*. The direct
102
+ re-run resolves a different ``ruff`` than ``uv run`` does -- the one on
103
+ ``PATH``, which may be any release. Two ruff versions word the same refusal
104
+ (``--select INVALID``, a file it cannot parse) differently, so requiring
105
+ the texts to match promoted a genuine rc2 verdict to a launcher failure
106
+ whenever the developer's ``ruff`` drifted from the project's pin. Exit code
107
+ plus an empty stdout is what identifies the verdict; the wording belongs to
108
+ whichever release produced it.
109
+
110
+ A uv-managed ``ruff`` is usually *not* on PATH, so the direct re-run can
111
+ itself be unlaunchable. Then the question is turned around and asked of
112
+ the launcher: ``uv run ruff --version`` succeeds when uv can still start
113
+ ruff, which makes the rc2 ruff's own; if even that fails, the tool could
114
+ not be started in this environment and the launcher reading stands.
115
+
116
+ Only the ambiguous shape reaches here -- a run that left text on stdout is
117
+ a verdict, and rc0/rc1 are settled by :func:`_launcher_failure` -- and
118
+ every probe is captured (``quiet=True``), so nothing prints twice.
119
+ ``ruff check --fix`` and ``ruff format`` are idempotent, so re-running them
120
+ changes nothing the first run did not already change.
121
+
122
+ Returns *candidate* (the stderr text) when the launcher is at fault, and
123
+ ``None`` when the direct run fails the same way -- i.e. the diagnostic
124
+ belongs to the child.
125
+ """
126
+ if len(command) < 3 or command[0] != "uv" or command[1] != "run":
127
+ # Not a uv-launched command: no launcher to blame.
128
+ return candidate
129
+ try:
130
+ probe = executor.run(command[2:], env=env, quiet=True)
131
+ except Exception:
132
+ probe = None
133
+ if probe is None or probe.launch_error is not None or probe.returncode == 127:
134
+ # The child is not directly runnable (usually: uv-managed, not on
135
+ # PATH). Ask uv whether it can start the tool at all.
136
+ return None if _launcher_can_start_child(executor, command, env) else candidate
137
+ # Same exit code with nothing on stdout is the child's verdict shape, no
138
+ # matter which ruff release produced the stderr wording (#215).
139
+ child_reached_the_same_verdict = (
140
+ probe.returncode == result.returncode and not (probe.stdout or "").strip()
141
+ )
142
+ return None if child_reached_the_same_verdict else candidate
143
+
144
+
145
+ def _launcher_can_start_child(
146
+ executor: SubprocessExecutor, command: list, env: dict
147
+ ) -> bool:
148
+ """Whether the launcher can start its child at all, asked cheaply.
149
+
150
+ ``command`` is ``["uv", "run", "<tool>", ...]``; the probe keeps the
151
+ launcher prefix and the tool, and replaces the arguments with
152
+ ``--version`` -- a side-effect-free run that succeeds only if uv can still
153
+ get as far as executing the tool.
154
+ """
155
+ try:
156
+ probe = executor.run(
157
+ [*command[:2], command[2], "--version"], env=env, quiet=True
158
+ )
159
+ except Exception:
160
+ return False
161
+ return probe.launch_error is None and probe.returncode == 0
162
+
163
+
164
+ def _run_format_command(
165
+ command: list, env: dict, quiet: bool, passthrough: bool = False
166
+ ) -> FormatCommandResult:
167
+ """Run a formatting command and report what it actually did (#164)."""
168
+ # Use SubprocessExecutor for consistent command execution
169
+ executor = SubprocessExecutor()
170
+
171
+ # ``run_uv_command`` owns the "uv is not installed" retry, shared with the
172
+ # pre-commit run (#165); the executor stays here so a caller keeps one
173
+ # patch point for the command it starts.
174
+ final_result = run_uv_command(executor, command, env=env, quiet=not passthrough)
175
+ stdout = final_result.stdout or ""
176
+ stderr = final_result.stderr or ""
177
+ output = stdout + "\n" + stderr if stderr else stdout
178
+
179
+ # Handle command failure - only log the error, don't raise exception.
180
+ # ``passthrough`` runs already wrote their own output, so only the exit
181
+ # code is ours to add there; a captured run is echoed instead (#200
182
+ # MEDIUM-1), otherwise the diagnostics the verdict was built from would
183
+ # never reach a non-quiet user.
184
+ if not quiet:
185
+ if passthrough and final_result.returncode != 0:
186
+ error_msg = f"Command failed with return code {final_result.returncode}"
187
+ if stderr:
188
+ error_msg += f": {stderr.strip()}"
189
+ print(f"Warning: {error_msg}")
190
+ elif not passthrough and output.strip():
191
+ print(output.strip())
192
+
193
+ launcher_failure = _launcher_failure(stdout, stderr, final_result.returncode)
194
+ if launcher_failure is not None and final_result.returncode == 2:
195
+ # rc2 is shared by uv's own startup failures and ruff's own refusals;
196
+ # only a direct re-run of the same child can tell them apart (NIT-1).
197
+ launcher_failure = _confirm_launcher_failure(
198
+ executor, command, env, final_result, launcher_failure
199
+ )
200
+
201
+ return FormatCommandResult(
202
+ ok=final_result.returncode == 0,
203
+ output=output,
204
+ returncode=final_result.returncode,
205
+ launch_error=final_result.launch_error,
206
+ launcher_failure=launcher_failure,
207
+ )
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class RuffCheckVerdict(FormatFailure):
212
+ """A ``ruff check --fix`` verdict, not a formatter failure (#188).
213
+
214
+ ``ruff check`` exits ``1`` when its own run leaves something to report --
215
+ after every fix it could apply. That is a *lint* finding: ``wl lint``
216
+ re-runs ruff itself and owns the verdict, so the exit ``1`` must not abort
217
+ its format prefix. It stays a failure for ``wl format``, which promises the
218
+ check step came back clean, and it keeps ``kind == "ruff"`` so every
219
+ existing ``FormatOutcome`` reader still sees a formatter-shaped failure.
220
+
221
+ It is a subclass rather than a fourth ``kind`` because the closed
222
+ vocabulary lives in ``format_outcome.py``, which this ticket does not own.
223
+
224
+ What actually lands here (#200). The check step's rc1 is *not* limited to
225
+ ``F821``-style unfixable violations: ruff also reports a file it cannot
226
+ parse as ``invalid-syntax`` diagnostics on stdout and exits ``1``, so the
227
+ check step's own verdict is this one. Since
228
+ :meth:`FormatExecutor.format_python_file` returns at the failed check step,
229
+ ``ruff format`` never runs for that file and the syntax error is carried
230
+ *only* as this verdict -- so ``wl lint`` does **not** stop before its own
231
+ ruff run; lint's run reports the ``invalid-syntax`` finding and the run
232
+ stays red.
233
+
234
+ A plain :class:`FormatFailure` instead means the check step's own verdict
235
+ was unavailable: ``uv`` failed to start ruff (:attr:`FormatCommandResult.launcher_failure`,
236
+ #200), the launcher is missing, or ruff exited ``>= 2`` because it refused
237
+ the config/arguments. For a *directory* run :func:`format_with_ruff` also
238
+ runs ``ruff format``, whose parse failure is rc2 (``error: Failed to parse
239
+ ...``) and is that plain failure -- which is why a syntax error stops
240
+ ``wl lint`` there, but not on the single-file path above.
241
+ """
242
+
243
+
244
+ def _ruff_failure(
245
+ directory: str, result: FormatCommandResult, step: str
246
+ ) -> FormatFailure:
247
+ """Turn a failed formatter command into the failure vocabulary (#164)."""
248
+ if result.tool_missing or result.launcher_failure is not None:
249
+ # #200 NIT-2: name the actual startup failure. ``tool_missing`` is the
250
+ # closed vocabulary's only "never ran" slot, but saying "ruff could not
251
+ # be launched" for a uv failure reads as "install ruff"; a uv launcher
252
+ # failure gets its own wording, and a missing executable keeps the
253
+ # launch error it actually raised (F6: no over-attribution).
254
+ if result.launcher_failure is not None:
255
+ message = (
256
+ f"the uv launcher failed before ruff ran ({step}): "
257
+ f"{result.launcher_failure}"
258
+ )
259
+ else:
260
+ reason = result.launch_error or f"exit code {result.returncode}"
261
+ message = f"ruff could not be started ({step}): {reason}"
262
+ return FormatFailure(
263
+ path=Path(directory),
264
+ kind="tool_missing",
265
+ message=message,
266
+ )
267
+ message = result.output.strip() or (
268
+ f"{step} failed with return code {result.returncode}"
269
+ )
270
+ return FormatFailure(path=Path(directory), kind="ruff", message=message)
271
+
272
+
273
+ def _ruff_check_failure(directory: str, result: FormatCommandResult) -> FormatFailure:
274
+ """Classify a failed ``ruff check`` run (#188, #200).
275
+
276
+ Exit ``1`` from a ruff that *did* run is "violations remain" -- a lint
277
+ verdict, carried as :class:`RuffCheckVerdict`. Anything else is a
278
+ formatter failure the lint prefix must still stop on: a launcher that
279
+ never ran, ``uv`` itself failing with rc1 (``launcher_failure``, #200 --
280
+ indistinguishable from a verdict by exit code alone), or exit ``>= 2``
281
+ (ruff refused the config/arguments).
282
+ """
283
+ if (
284
+ not result.tool_missing
285
+ and result.launcher_failure is None
286
+ and result.returncode == 1
287
+ ):
288
+ message = result.output.strip() or "ruff check left unfixed violations"
289
+ return RuffCheckVerdict(path=Path(directory), kind="ruff", message=message)
290
+ return _ruff_failure(directory, result, "ruff check")
291
+
292
+
293
+ def format_with_ruff(
294
+ directory: str, env: dict, quiet: bool = False, unsafe: bool = False
295
+ ) -> tuple[FormatFailure, ...]:
296
+ """Format directory with ruff only; returns every command that failed."""
297
+ failures: list[FormatFailure] = []
298
+ try:
299
+ # Run ruff check with fix
300
+ ruff_check_cmd = ["uv", "run", "ruff", "check", "--fix"]
301
+ if unsafe:
302
+ ruff_check_cmd.append("--unsafe-fixes")
303
+ ruff_check_cmd.append(directory)
304
+
305
+ # #200 MEDIUM-1: both steps are captured even when not quiet. The
306
+ # passthrough run left stdout/stderr empty, so a launcher failure was
307
+ # indistinguishable from a verdict and ``wl lint`` / ``wl format <dir>``
308
+ # treated a ruff that never ran as a lint verdict. ``_run_format_command``
309
+ # echoes the captured text when not quiet.
310
+ check_result = _run_format_command(
311
+ ruff_check_cmd, env, quiet, passthrough=False
312
+ )
313
+ if not check_result.ok:
314
+ # #188: rc1 is "violations remain", which the lint prefix must not
315
+ # mistake for a formatter that could not run.
316
+ failures.append(_ruff_check_failure(directory, check_result))
317
+
318
+ # Run ruff format for code formatting
319
+ format_result = _run_format_command(
320
+ [
321
+ "uv",
322
+ "run",
323
+ "ruff",
324
+ "format",
325
+ directory,
326
+ ],
327
+ env,
328
+ quiet,
329
+ passthrough=False,
330
+ )
331
+ if not format_result.ok:
332
+ failures.append(_ruff_failure(directory, format_result, "ruff format"))
333
+
334
+ # Note: We no longer manually handle trailing whitespace and end-of-file newlines here
335
+ # because pre-commit's trailing-whitespace and end-of-file-fixer hooks should handle this
336
+ # This ensures consistency between wl format and running pre-commit directly
337
+ except subprocess.CalledProcessError as e:
338
+ if not quiet:
339
+ print(f"Warning: ruff formatting failed with return code {e.returncode}")
340
+ raise
341
+ except FileNotFoundError as e:
342
+ if not quiet:
343
+ print(f"Warning: uv or ruff not found in PATH: {e}")
344
+ failures.append(
345
+ FormatFailure(
346
+ path=Path(directory),
347
+ kind="tool_missing",
348
+ message=f"uv or ruff not found in PATH: {e}",
349
+ )
350
+ )
351
+ except Exception as e:
352
+ if not quiet:
353
+ print(f"Warning occurred during ruff formatting: {e}")
354
+ failures.append(
355
+ FormatFailure(
356
+ path=Path(directory),
357
+ kind="ruff",
358
+ message=f"Warning occurred during ruff formatting: {e}",
359
+ )
360
+ )
361
+ return tuple(failures)
362
+
363
+
364
+ def format_with_python_tools(
365
+ directory: str, env: dict, quiet: bool = False, unsafe: bool = False
366
+ ) -> tuple[FormatFailure, ...]:
367
+ """Format directory with Python tools (ruff only)."""
368
+ try:
369
+ # Format with ruff only
370
+ return format_with_ruff(directory, env, quiet, unsafe)
371
+ except subprocess.CalledProcessError as e:
372
+ if not quiet:
373
+ print(f"Warning: Python formatting failed with return code {e.returncode}")
374
+ raise
375
+ except Exception as e:
376
+ if not quiet:
377
+ print(f"Warning occurred during Python formatting: {e}")
378
+ return (
379
+ FormatFailure(
380
+ path=Path(directory),
381
+ kind="ruff",
382
+ message=f"Warning occurred during Python formatting: {e}",
383
+ ),
384
+ )
385
+
386
+
387
+ def format_tools_scripts(
388
+ tools_dir: str, env: dict, quiet: bool, unsafe: bool = False
389
+ ) -> None:
390
+ """Format Python files in tools directory with ruff."""
391
+ tools_path = Path(tools_dir)
392
+ if not tools_path.exists():
393
+ if not quiet:
394
+ print(f"Tools directory {tools_dir} does not exist, skipping...")
395
+ return
396
+
397
+ # Find all Python files in tools directory
398
+ python_files = list(tools_path.glob("**/*.py"))
399
+ if python_files:
400
+ # Fix issues in tools Python files with ruff check
401
+ try:
402
+ ruff_check_cmd = ["uv", "run", "ruff", "check", "--fix"]
403
+ if unsafe:
404
+ ruff_check_cmd.append("--unsafe-fixes")
405
+ ruff_check_cmd.extend([str(f) for f in python_files])
406
+
407
+ _run_format_command(ruff_check_cmd, env, quiet, passthrough=not quiet)
408
+ except Exception as e:
409
+ if not quiet:
410
+ print(f"Warning: ruff check failed for tools scripts: {e}")
411
+
412
+ # Format tools Python files with ruff format
413
+ try:
414
+ _run_format_command(
415
+ ["uv", "run", "ruff", "format"] + [str(f) for f in python_files],
416
+ env,
417
+ quiet,
418
+ passthrough=not quiet,
419
+ )
420
+ except Exception as e:
421
+ if not quiet:
422
+ print(f"Warning: ruff format failed for tools scripts: {e}")
423
+ else:
424
+ if not quiet:
425
+ print("No Python files found in tools directory")
426
+
427
+
428
+ def format_examples(
429
+ examples_dir: str, env: dict, quiet: bool, unsafe: bool = False
430
+ ) -> None:
431
+ """Format examples directory with ruff."""
432
+ examples_path = Path(examples_dir)
433
+ if not examples_path.exists():
434
+ if not quiet:
435
+ print(f"Examples directory {examples_dir} does not exist, skipping...")
436
+ return
437
+
438
+ # Fix issues in examples with ruff check
439
+ try:
440
+ ruff_check_cmd = ["uv", "run", "ruff", "check", "--fix"]
441
+ if unsafe:
442
+ ruff_check_cmd.append("--unsafe-fixes")
443
+ ruff_check_cmd.append(examples_dir)
444
+
445
+ _run_format_command(ruff_check_cmd, env, quiet, passthrough=not quiet)
446
+ except Exception as e:
447
+ if not quiet:
448
+ print(f"Warning: ruff check failed for examples: {e}")
449
+
450
+ # Format examples with ruff format
451
+ try:
452
+ _run_format_command(
453
+ ["uv", "run", "ruff", "format", examples_dir],
454
+ env,
455
+ quiet,
456
+ passthrough=not quiet,
457
+ )
458
+ except Exception as e:
459
+ if not quiet:
460
+ print(f"Warning: ruff format failed for examples: {e}")
461
+
462
+
463
+ def generate_type_stubs(src_dir: str, typings_dir: str, env: dict, quiet: bool) -> None:
464
+ """Generate type stubs."""
465
+ if not quiet:
466
+ print(f"Generating type stubs for {src_dir}...")
467
+
468
+ # Check if src_dir exists
469
+ src_path = Path(src_dir)
470
+ if not src_path.exists():
471
+ if not quiet:
472
+ print(f"Target {src_dir} does not exist, skipping...")
473
+ return
474
+
475
+ # Ensure typings directory exists
476
+ typings_path = Path(typings_dir)
477
+ typings_path.mkdir(parents=True, exist_ok=True)
478
+
479
+ try:
480
+ # Use uv run --with mypy python -c to call mypy.stubgen
481
+ # Use repr() to properly escape paths for the Python command
482
+ cmd = [
483
+ "uv",
484
+ "run",
485
+ "--with",
486
+ "mypy",
487
+ "python",
488
+ "-c",
489
+ f"from mypy.stubgen import main; main([{repr(src_dir)}, '-o', {repr(typings_dir)}])",
490
+ ]
491
+
492
+ _run_format_command(cmd, env, quiet, passthrough=False)
493
+ except Exception as e:
494
+ if not quiet:
495
+ print(f"Warning: Error generating type stubs: {e}")
496
+
497
+
498
+ __all__ = [
499
+ "RuffCheckVerdict",
500
+ "_run_format_command",
501
+ "format_with_ruff",
502
+ "format_with_python_tools",
503
+ "format_tools_scripts",
504
+ "format_examples",
505
+ "generate_type_stubs",
506
+ ]