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,266 @@
1
+ """What a ``pre-commit`` run means, in one place (#165).
2
+
3
+ Before this module, three sets of rules read the same output: the format path
4
+ handler classified hooks (``- hook id:`` blocks + the auto-fix marker), its
5
+ filter decided what to show (``Failed`` lines), and the lint reporter decided
6
+ what to report (line tails ending in ``Passed``/``Failed``). The run was also
7
+ started two ways -- ``uv run pre-commit`` for format, a bare ``pre-commit`` for
8
+ lint. Now both commands call :func:`run_pre_commit` and read the answer from
9
+ :class:`PreCommitRun`.
10
+
11
+ This module knows about *one run*. It does not know where the config lives, and
12
+ it never reads the filesystem to find out -- see :func:`run_pre_commit`.
13
+ """
14
+
15
+ from collections.abc import Sequence
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ from .subprocess_utils import SubprocessResult
20
+ from .uv_run import run_uv_command, uv_unavailable
21
+
22
+ #: pre-commit detail line naming the hook a ``Failed`` block belongs to. It is
23
+ #: printed for every hook pre-commit *failed* and for none it passed, which is
24
+ #: what makes it the marker identifying a hook verdict in the run's output.
25
+ _HOOK_ID_PREFIX = "- hook id:"
26
+
27
+ #: pre-commit detail line emitted when a hook *rewrote* files -- an auto-fix,
28
+ #: the expected outcome of running a formatter, which must leave the format
29
+ #: gate green. The built-in fixers (``trailing-whitespace``,
30
+ #: ``end-of-file-fixer``) exit ``1`` after rewriting and print this marker next
31
+ #: to their ``- exit code: 1`` line; hooks that only *report* a failure print
32
+ #: the exit-code line without it.
33
+ _MODIFIED_MARKER = "- files were modified by this hook"
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class PreCommitRun:
38
+ """One finished ``pre-commit run``, and the verdict it carries.
39
+
40
+ The exit code alone cannot answer "did a hook judge a failure?": pre-commit
41
+ also exits non-zero when a hook merely rewrote files. The verdict, the
42
+ blocks and the filtered display text are all *derived* from ``result`` so
43
+ they can never disagree with each other.
44
+ """
45
+
46
+ result: SubprocessResult
47
+
48
+ @property
49
+ def stdout(self) -> str:
50
+ """Captured stdout (empty string, never ``None``)."""
51
+ return self.result.stdout or ""
52
+
53
+ @property
54
+ def stderr(self) -> str:
55
+ """Captured stderr (empty string, never ``None``).
56
+
57
+ Sits beside :attr:`stdout` because lint used to merge the two before
58
+ reading the verdict (#165 audit patch 1).
59
+ """
60
+ return self.result.stderr or ""
61
+
62
+ @property
63
+ def combined_output(self) -> str:
64
+ """stdout and stderr joined in that order, as lint used to see them."""
65
+ parts = [part for part in (self.stdout, self.stderr) if part]
66
+ return "\n".join(parts)
67
+
68
+ @property
69
+ def verdict_blocks(self) -> tuple[tuple[str, ...], ...]:
70
+ """One block per hook pre-commit reported, auto-fixes included.
71
+
72
+ A block is a maximal run of ``-``-prefixed lines carrying
73
+ pre-commit's own ``- hook id:`` line. Requiring that line is what keeps
74
+ other output from inventing a block: pre-commit prints it per *failing*
75
+ hook and never for a passing one, while hook output can contain
76
+ ``-``-prefixed lines of its own (``ruff`` prints `` --> file:1:8``) or a
77
+ line ending in the word ``Failed`` (the phantom block of #149 review
78
+ D2).
79
+ """
80
+ return _hook_verdict_blocks(self.stdout)
81
+
82
+ @property
83
+ def failed_blocks(self) -> tuple[tuple[str, ...], ...]:
84
+ """The verdict blocks that are real failures -- auto-fixes excluded.
85
+
86
+ Auto-fix blocks are dropped for the same reason they do not fail the
87
+ run (#149): they only rewrote files. ``FormatFailure.details`` is built
88
+ directly from this property (#164/#165).
89
+ """
90
+ return tuple(
91
+ block for block in self.verdict_blocks if not _is_auto_fix_block(block)
92
+ )
93
+
94
+ @property
95
+ def hooks_failed(self) -> bool:
96
+ """Classify the run as "a hook judged failure" or not.
97
+
98
+ ``pre-commit`` exits non-zero for two different reasons, and only one
99
+ of them may turn the format gate red (#149):
100
+
101
+ * a hook **rewrote files** (auto-fix) -- expected while formatting;
102
+ * a hook **judged the check failed** (``check-yaml`` parse error,
103
+ ``codespell`` hit, ...) -- must propagate.
104
+
105
+ The two are *not* told apart by the exit code. Captured from real
106
+ ``pre-commit`` runs, every built-in fixer exits ``1`` after rewriting
107
+ and prints ``- exit code: 1`` right beside
108
+ ``- files were modified by this hook``; a reporting hook prints the
109
+ exit-code line *without* that marker. So the modification marker
110
+ decides: **a hook that rewrote files never counts as a failure**
111
+ (review D1).
112
+
113
+ The accepted trade-off: a hook that rewrites files *and* leaves a real
114
+ violation behind (``ruff --fix`` with findings left over) reads as an
115
+ auto-fix and stays green. No hook in this repo's managed config behaves
116
+ that way -- its fixers (``end-of-file-fixer``, ``trailing-whitespace``,
117
+ ``ruff-format``) only rewrite, and ``ruff``/``codespell``/``check-yaml``
118
+ only report -- and recovering that case would cost a second full
119
+ pre-commit run on every dirty tree.
120
+
121
+ A non-zero exit with no hook verdict at all -- pre-commit refusing to
122
+ run, e.g. an invalid config -- is a real failure as well.
123
+ """
124
+ if self.result.returncode == 0:
125
+ return False
126
+
127
+ blocks = self.verdict_blocks
128
+ if blocks and all(_is_auto_fix_block(block) for block in blocks):
129
+ # Non-zero *only* because hooks rewrote files: not a failure.
130
+ return False
131
+ return True
132
+
133
+ @property
134
+ def filtered_output(self) -> str | None:
135
+ """Only the sections pre-commit marked failed -- lint's display view.
136
+
137
+ Passing hooks print nothing but their ``Passed`` header, so the filter
138
+ drops their header lines and keeps each failed hook's detail lines --
139
+ including stderr, which lint used to append to the stdout it filtered.
140
+ Returns ``None`` when nothing failed.
141
+
142
+ This is deliberately **not** what ``wl format`` shows (#172). Format's
143
+ display has to agree with the verdict it reports: it prints
144
+ :attr:`failed_blocks` (the same blocks :attr:`hooks_failed` was
145
+ computed from) and falls back to :attr:`combined_output` when
146
+ pre-commit failed before printing any hook verdict. Text-filtered
147
+ output can include auto-fix sections the verdict ignores, and can hide
148
+ a stderr-only failure; the blocks cannot.
149
+ """
150
+ return _filter_failed_sections(self.combined_output)
151
+
152
+
153
+ def _hook_verdict_blocks(output: str) -> tuple[tuple[str, ...], ...]:
154
+ """Group pre-commit output into one detail block per reported hook."""
155
+ blocks: list[list[str]] = []
156
+ current: list[str] = []
157
+ for line in output.splitlines():
158
+ stripped = line.strip()
159
+ if stripped.startswith("-"):
160
+ current.append(stripped)
161
+ elif current:
162
+ blocks.append(current)
163
+ current = []
164
+ if current:
165
+ blocks.append(current)
166
+ return tuple(
167
+ tuple(block)
168
+ for block in blocks
169
+ if any(line.startswith(_HOOK_ID_PREFIX) for line in block)
170
+ )
171
+
172
+
173
+ def _is_auto_fix_block(block: tuple[str, ...]) -> bool:
174
+ """True when this failing hook's only effect was rewriting files."""
175
+ return any(_MODIFIED_MARKER in line for line in block)
176
+
177
+
178
+ def _filter_failed_sections(text: str) -> str | None:
179
+ """Keep the failed hooks' header and detail lines; drop the passed ones."""
180
+ filtered: list[str] = []
181
+ in_failure = False
182
+ for line in text.splitlines():
183
+ stripped = line.strip()
184
+ if stripped.endswith("Passed"):
185
+ in_failure = False
186
+ continue
187
+ if stripped.endswith("Failed"):
188
+ in_failure = True
189
+ filtered.append(line)
190
+ elif in_failure:
191
+ filtered.append(line)
192
+ return "\n".join(filtered) if filtered else None
193
+
194
+
195
+ def run_pre_commit(
196
+ *,
197
+ files: Sequence[str] | None,
198
+ config: Path | None,
199
+ cwd: Path,
200
+ env: dict[str, str] | None = None,
201
+ quiet: bool = True,
202
+ prefer_uv: bool = True,
203
+ ) -> PreCommitRun:
204
+ """Run ``pre-commit run`` once and return its :class:`PreCommitRun`.
205
+
206
+ With ``prefer_uv`` the run starts as ``uv run pre-commit`` (the formatters'
207
+ launcher); otherwise it starts as the bare ``pre-commit`` on ``PATH`` (what
208
+ ``wl lint`` wants -- it already runs inside the project environment and
209
+ does not need uv re-resolving it). Either launcher falls back to the other
210
+ when ``uv_unavailable`` says it never ran -- a ``launch_error``, or the
211
+ shell's ``127`` "command not found" -- so a machine with only one of the
212
+ two still runs the hooks.
213
+
214
+ ``config=None`` means *let pre-commit use its own config discovery* and
215
+ nothing else. This function never probes the filesystem: what a missing
216
+ config means is the caller's explicit choice (#165 audit patch 2). ``wl
217
+ format`` skips the run when its managed config is absent; ``wl lint`` falls
218
+ back to the default discovery by passing ``None``.
219
+
220
+ Args:
221
+ files: Paths to check; ``None`` or an empty sequence means
222
+ ``--all-files``. An empty sequence used to reach pre-commit as
223
+ "no paths", which silently checks only the staged files instead of
224
+ everything (#172).
225
+ config: Explicit config path, or ``None`` for pre-commit's discovery.
226
+ cwd: Directory the run happens in (lint runs from the project root).
227
+ env: Environment for the child process.
228
+ quiet: Whether to capture the child's output.
229
+ prefer_uv: Start through ``uv run``; ``False`` starts with the bare
230
+ ``pre-commit`` executable.
231
+
232
+ Returns:
233
+ The finished run; its ``result`` may still carry ``launch_error``.
234
+ """
235
+ # Resolved at call time so patching the class -- the seam both callers
236
+ # already use for their other children (``lint_executor``'s ruff/mypy
237
+ # runs, the git-flow hint in the same format handler) -- intercepts this
238
+ # run too. Each caller still builds its own instance, here and in the
239
+ # handler; the class is the one patch point, not a shared instance.
240
+ from .subprocess_utils import SubprocessExecutor
241
+
242
+ command = ["pre-commit", "run"]
243
+ if not files:
244
+ command.append("--all-files")
245
+ else:
246
+ command.extend(files)
247
+ if config is not None:
248
+ command.extend(["--config", str(config)])
249
+
250
+ executor = SubprocessExecutor()
251
+ if prefer_uv:
252
+ result = run_uv_command(
253
+ executor, ["uv", "run", *command], env=env, cwd=cwd, quiet=quiet
254
+ )
255
+ else:
256
+ result = executor.run(command, env=env, cwd=cwd, quiet=quiet)
257
+ if uv_unavailable(result):
258
+ # The bare launcher never started: retry once through ``uv run``,
259
+ # the launcher ``wl format`` starts with.
260
+ try:
261
+ result = executor.run(
262
+ ["uv", "run", *command], env=env, cwd=cwd, quiet=quiet
263
+ )
264
+ except Exception:
265
+ pass
266
+ return PreCommitRun(result)
@@ -0,0 +1,72 @@
1
+ """Where the managed pre-commit config lives -- one answer, DEPLOY-anchored (#189).
2
+
3
+ The self-sync writes ``.wl/.pre-commit-config.yaml`` at the **DEPLOY** root
4
+ (``wl_dir_updater`` -> ``setup_handler`` -> ``generate_configs(context.deploy)``),
5
+ while ``wl format`` and ``wl lint`` used to look it up at the **PROJECT** root.
6
+ In the ``git root > sub-package with its own pyproject.toml`` layout the two
7
+ roots differ, so the managed config written at the git top was invisible from
8
+ the sub-package: ``wl format`` skipped every hook and exited 0, and ``wl lint``
9
+ handed pre-commit a root with no config (``InvalidConfigError``), staying red
10
+ forever.
11
+
12
+ :func:`managed_pre_commit_config` is the single definition of that location;
13
+ both consumers take the path from it and keep only their own *missing config*
14
+ policy (format skips the run, lint falls back to pre-commit's own discovery).
15
+ Nothing else is re-anchored: the file-tool scopes (ruff, mypy) stay where they
16
+ were -- only this one lookup moves from PROJECT to DEPLOY.
17
+
18
+ The anchor is deliberately **narrow**: DEPLOY is the git work-tree top whenever
19
+ there is one, so a ``.wl/.pre-commit-config.yaml`` placed by hand inside a
20
+ sub-package is *not* adopted in that layout -- the only managed config consulted
21
+ is the one at the work-tree top. That exclusion is the anchor alone: this module
22
+ never reads ``config.json`` and never probes for the config file, so whether a
23
+ by-hand copy happens to sit next to a ``config.json`` changes nothing (the
24
+ ``LEGACY`` kind is the only root-resolution rule that looks at ``config.json``).
25
+ With no work tree, DEPLOY falls back to the project-marker root, which can
26
+ legitimately be the sub-package itself. One step further, when *start* is in no
27
+ work tree and no project marker (``.wl`` directory / ``pyproject.toml`` file)
28
+ sits at or above it, the caller-side fallback policy
29
+ (:func:`~wl_cli.utils.root_fallback.resolve_root_or_start`, the one place ADR
30
+ 0006 puts that policy) substitutes *start* itself -- so a bare directory yields
31
+ ``<start>/.wl/.pre-commit-config.yaml``. This is a decision, not an accident:
32
+ one anchor is what makes format and lint agree. It is recorded in
33
+ ``docs/adr/0008-managed-pre-commit-config-anchored-at-deploy-root.md`` and
34
+ pinned by ``tests/test_managed_pre_commit_config_203.py``.
35
+ """
36
+
37
+ from pathlib import Path
38
+
39
+ from .root import RootKind
40
+ from .root_fallback import resolve_root_or_start
41
+
42
+ #: The managed control file's name inside a root's ``.wl`` directory.
43
+ _MANAGED_CONFIG_NAME = ".pre-commit-config.yaml"
44
+
45
+
46
+ def managed_pre_commit_config(start: Path) -> Path:
47
+ """The managed ``.wl/.pre-commit-config.yaml`` of *start*'s DEPLOY root.
48
+
49
+ *start* is the anchor the caller already holds -- ``wl format`` passes its
50
+ ``PROJECT`` root, ``wl lint`` the project root it runs from -- and the
51
+ **DEPLOY** root is resolved from it here, once, because that is where the
52
+ self-sync writes the file: the git work-tree top when there is one, else
53
+ the project marker root. Passing the PROJECT root is therefore right in
54
+ both layouts: when the two coincide the answer is unchanged (#125), and in
55
+ the sub-package layout it climbs to the git top where the file actually is
56
+ (#189).
57
+
58
+ The anchor has one step more than ``utils/root.py``'s own ``DEPLOY`` rule:
59
+ when *start* is in no git work tree and no project marker sits at or above
60
+ it, :func:`~wl_cli.utils.root_fallback.resolve_root_or_start` substitutes
61
+ *start* itself (ADR 0006's caller-side fallback, not part of the root
62
+ module's rule), so the derived path is ``<start>/.wl/.pre-commit-config.yaml``.
63
+
64
+ The path is **derived, never probed**: this function does not check whether
65
+ the config file exists, so each caller keeps its own decision about what a
66
+ missing config means. Deriving it is not I/O-free, though -- resolving the
67
+ DEPLOY root runs ``git rev-parse --show-toplevel`` and, when that misses,
68
+ stats the project markers on the way up. What it never does is *look for the
69
+ config*: no existence probe, no walk past the anchor hunting for another
70
+ copy.
71
+ """
72
+ return resolve_root_or_start(start, RootKind.DEPLOY).wl_dir() / _MANAGED_CONFIG_NAME