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,409 @@
1
+ """
2
+ Lint executor module.
3
+ Runs every tool one ``wl lint`` needs -- ruff, pre-commit, mypy -- exactly once.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ from ...utils.pre_commit import PreCommitRun, run_pre_commit
9
+ from ...utils.pre_commit_config import managed_pre_commit_config
10
+ from ...utils.root import Root, RootKind
11
+ from ...utils.subprocess_utils import SubprocessExecutor, SubprocessResult
12
+ from .lint_run import LintRun
13
+ from .mypy_config import mypy_config_file
14
+
15
+
16
+ def _scope_path(root: Root, token: str) -> Path:
17
+ """The directory *token* names under *root*.
18
+
19
+ ``src/`` is the one scope directory the root module names
20
+ (:meth:`Root.src_dir`), so ask it for that one; every other token --
21
+ ``tests/`` or a caller-supplied path -- has no derived-path method and is
22
+ joined onto the root the plain way. Routing the ``src`` probe through the
23
+ module keeps the root layout with a single owner (#114) without widening
24
+ ``Root``'s interface.
25
+ """
26
+ if Path(token).parts == ("src",):
27
+ return root.src_dir()
28
+ return root.path / token
29
+
30
+
31
+ def _scope_token_path(token: str, root: Root | None) -> Path:
32
+ """The path *token* names when the command runs from the anchored root.
33
+
34
+ Both an explicit user path and a fallback default resolve through here, so
35
+ the two probes cannot drift apart (#174). *root* is ``None`` when the
36
+ caller pinned no project root; the token is then used as-is and resolves
37
+ against the cwd, which is where the command will run.
38
+ """
39
+ candidate = Path(token)
40
+ if root is not None and not candidate.is_absolute():
41
+ return _scope_path(root, token)
42
+ return candidate
43
+
44
+
45
+ def _existing_scope_tokens(tokens: tuple[str, ...], root: Root | None) -> list[str]:
46
+ """The *tokens* that resolve to an existing path, probed like explicit paths.
47
+
48
+ A probe that raises counts as missing, matching the explicit-path loop's
49
+ blanket guard: an unreadable path must not crash command construction.
50
+ """
51
+ existing = []
52
+ for token in tokens:
53
+ try:
54
+ if _scope_token_path(token, root).exists():
55
+ existing.append(token)
56
+ except Exception:
57
+ pass
58
+ return existing
59
+
60
+
61
+ def _cwd_anchored_scope_tokens(tokens: list[str]) -> list[str]:
62
+ """*tokens* re-anchored on the cwd, as absolute paths where they exist there.
63
+
64
+ The floor for an explicit scope that resolved nowhere under the project
65
+ root (#179). A token can still name a real path relative to where the user
66
+ typed the command -- ``cd pkg && wl lint commands`` -- while the ruff run
67
+ happens at the project root; handing it over unchanged only produced
68
+ ``E902``. Resolving it here hands ruff the same directory, absolute, so
69
+ every process agrees on it (the symlink reasoning of #170 review F4).
70
+
71
+ A token that exists nowhere -- an absolute path included -- is returned
72
+ untouched, so ruff reports it instead of lint silently widening to ``"."``.
73
+ """
74
+ anchored = []
75
+ for token in tokens:
76
+ try:
77
+ candidate = Path(token)
78
+ resolved = candidate.resolve()
79
+ if not candidate.is_absolute() and resolved.exists():
80
+ anchored.append(str(resolved))
81
+ else:
82
+ anchored.append(token)
83
+ except Exception:
84
+ # Same blanket guard as the probes: an unreadable path must not
85
+ # crash command construction; ruff reports it.
86
+ anchored.append(token)
87
+ return anchored
88
+
89
+
90
+ def _resolved_scope_tokens(tokens: tuple[str, ...], root: Root | None) -> list[str]:
91
+ """Every token, in order: anchored on *root* or passed through (#188).
92
+
93
+ Filtering the missing tokens out -- the old ``existing or cwd_anchored``
94
+ -- silently narrowed a partially-missing scope: ``wl lint src typo`` ran
95
+ ``ruff check src`` and never mentioned ``typo``. Each token is now kept:
96
+
97
+ * one that resolves under *root* is handed over as typed;
98
+ * one that does not is re-anchored on the cwd (absolute when it is real
99
+ there -- ``cd pkg && wl lint commands``, the #179 floor);
100
+ * one that exists nowhere is passed through untouched, so ruff reports it
101
+ as ``E902`` and mypy as ``Cannot read file ...`` instead of lint
102
+ checking less than asked.
103
+
104
+ Probes share :func:`_scope_token_path` with the existence filters, so the
105
+ two can never disagree about what "resolves under the root" means.
106
+ """
107
+ resolved: list[str] = []
108
+ for token in tokens:
109
+ try:
110
+ if _scope_token_path(token, root).exists():
111
+ resolved.append(token)
112
+ continue
113
+ except Exception:
114
+ # Same blanket guard as the probes: an unreadable path must not
115
+ # crash command construction; the tool reports it.
116
+ pass
117
+ resolved.append(_cwd_anchored_scope_tokens([token])[0])
118
+ return resolved
119
+
120
+
121
+ class LintExecutor:
122
+ """Executor for lint commands."""
123
+
124
+ def prepare_ruff_command(
125
+ self,
126
+ paths: list[str] | None,
127
+ fix: bool,
128
+ quiet: bool,
129
+ *,
130
+ structured: bool = False,
131
+ project_root: Path | None = None,
132
+ ) -> list[str]:
133
+ """Prepare the ruff command.
134
+
135
+ *project_root* is the directory this command will be **run from**:
136
+ the explicit-path probes below resolve against it the same way mypy's
137
+ explicit paths do (#179). Without it, a bare token was probed against
138
+ the process cwd even though the command later ran with
139
+ ``cwd=project_root``, so from a subdirectory ``wl lint src`` could drop
140
+ an existing scope and widen to the whole repo. ``None`` keeps the
141
+ historical cwd probe, which is correct when the caller runs the command
142
+ from wherever it built it.
143
+
144
+ A requested scope is never widened to ``"."`` either, and no token is
145
+ dropped (#188): a partially-missing scope used to run only the tokens
146
+ that existed. Each token that does not resolve under *project_root* is
147
+ re-probed against the cwd: one that is real there -- ``cd pkg && wl
148
+ lint commands`` -- goes to ruff as an absolute path, so the run at the
149
+ root reaches it, while a token that exists nowhere is passed through
150
+ for ruff to report (``E902``). This is this function's own floor,
151
+ **not** mypy's #174 one: mypy's *fallback* scope is filtered, whereas
152
+ ruff has no default scope to fall back to and none is invented. With
153
+ no explicit paths at all the default stays ``"."`` (the documented
154
+ default, which ``wl gate`` depends on).
155
+
156
+ ``structured`` asks ruff for the report's ``--output-format full``
157
+ text. One such run carries every finding -- header, ``-->`` location,
158
+ snippet, caret, ``help:``, and ruff's own closing summary -- which is
159
+ what lets the report group the verbatim blocks into its per-directory
160
+ sections without re-running ruff once per section (#170 audit patch 4 /
161
+ review F1-F2). ``--fix`` is orthogonal: ruff applies the fixes, then
162
+ reports whatever diagnostics remain, so the same single run stays
163
+ enough.
164
+
165
+ ``--quiet`` is *not* passed on a structured run even when the caller
166
+ asked for it: ruff's quiet mode suppresses the summary, and with it the
167
+ ``Found N errors.`` / ``[*] N fixable ... (N hidden fixes ...)`` lines
168
+ the report must keep (#170 review F2). The caller's quiet still governs
169
+ wl's own logging and the capture.
170
+ """
171
+ # Prepare ruff command (offline, no uv fetch)
172
+ cmd = ["ruff", "check"]
173
+
174
+ # The directory the command will run from, as a value the scope probe
175
+ # can resolve against. ``None`` when the caller pinned no root -- then
176
+ # a bare token is probed against the cwd exactly as before
177
+ # ``project_root`` existed.
178
+ root = (
179
+ Root(project_root, RootKind.PROJECT) if project_root is not None else None
180
+ )
181
+
182
+ # Every token the caller named is handed over, or the whole directory
183
+ # when none were named. A token missing under the root is re-anchored
184
+ # on the cwd (absolute when real there) rather than widened to ".";
185
+ # a token real in neither place is passed through for ruff to report.
186
+ if paths:
187
+ cmd.extend(_resolved_scope_tokens(tuple(paths), root))
188
+ else:
189
+ cmd.append(".")
190
+
191
+ # One structured run feeds every report section
192
+ if structured:
193
+ cmd.extend(["--output-format", "full"])
194
+
195
+ # Add fix flag if requested
196
+ if fix:
197
+ cmd.append("--fix")
198
+
199
+ # Add quiet flag if requested, but never for the structured run whose
200
+ # summary and fixable count the report renders.
201
+ if quiet and not structured:
202
+ cmd.append("--quiet")
203
+
204
+ return cmd
205
+
206
+ def prepare_mypy_command(
207
+ self,
208
+ paths: list[str] | None,
209
+ quiet: bool,
210
+ project_root: Path | None = None,
211
+ *,
212
+ fallback_paths: tuple[str, ...] = ("src/", "tests/"),
213
+ ) -> list[str]:
214
+ """Prepare the mypy command.
215
+
216
+ *project_root* is the directory this command will be **run from**: the
217
+ config lookup and the path-existence checks below resolve against it.
218
+ It defaults to the current working directory, which is only correct
219
+ when the caller runs the command from wherever it built it -- the gate
220
+ builds its command before running it with the project root as cwd.
221
+
222
+ *fallback_paths* is the scope used when the requested *paths* cannot be
223
+ used, or when none were requested; it defaults to the historical
224
+ ``src/ tests/``. A caller whose scope is a *contract* narrows it:
225
+ ``wl gate`` checks ``src/`` and nothing else, so a root that cannot
226
+ satisfy that scope must fail on its own terms rather than silently
227
+ widen the leg to directories the change under test has nothing to do
228
+ with (review F0 -- a monorepo whose git work-tree root has no
229
+ ``src/``).
230
+
231
+ An **explicit** path is never filtered (#188, the same floor as ruff's
232
+ command): a token that resolves nowhere under the root is handed over
233
+ -- cwd-anchored when it is real there -- so mypy says ``Cannot read
234
+ file ...`` instead of lint quietly checking only the tokens that
235
+ happened to exist. Only the caller's *fallback* is filtered, below.
236
+
237
+ The fallback itself is filtered to the tokens that resolve to an
238
+ existing path, probed exactly like an explicit one (#174): a project
239
+ with only ``src/`` must not ask mypy to read ``tests/``, which would
240
+ exit 2 and leave ``wl lint`` permanently red. When *none* of the
241
+ fallback tokens exists, the original fallback is passed unchanged --
242
+ the scope is never silently widened or emptied, so mypy still fails
243
+ with ``Cannot read file ...`` and ``wl gate``'s ``src/`` contract
244
+ still fails on its own terms. That floor assumes a non-empty
245
+ *fallback_paths*, as every current caller passes one; an empty
246
+ fallback would necessarily yield an empty scope.
247
+ """
248
+ # Prepare mypy command
249
+ root_path = Path.cwd() if project_root is None else project_root
250
+ # The same directory as a value, for the scope probes below. ``None``
251
+ # when the caller pinned no root -- then a bare token is probed against
252
+ # the cwd exactly as before ``project_root`` existed.
253
+ root = Root(root_path, RootKind.PROJECT) if project_root is not None else None
254
+ cmd = ["mypy"]
255
+
256
+ # Absolute, never cwd-relative (#170 review F4). The command is built
257
+ # from *project_root* and then run with it as cwd, but the two can
258
+ # disagree through a symlink (``/tmp`` vs ``/private/tmp`` on macOS):
259
+ # a relative token computed against one spelling and resolved against
260
+ # the other points nowhere, and mypy then fails to start. An absolute
261
+ # path means the same file to every process.
262
+ wl_mypy_config = mypy_config_file(root_path, relative=False)
263
+ if wl_mypy_config is not None:
264
+ cmd.extend(["--config-file", str(wl_mypy_config)])
265
+
266
+ # Add every requested path, or the caller's fallback scope when none
267
+ # were requested. An explicit token is never dropped (#188): mypy
268
+ # reports the missing ones instead of lint silently narrowing.
269
+ if paths:
270
+ cmd.extend(_resolved_scope_tokens(tuple(paths), root))
271
+ else:
272
+ # Keep only fallback directories that exist (#174). ``or`` is the
273
+ # fail-closed floor: an all-missing fallback passes through
274
+ # unchanged, so mypy reports the missing scope itself.
275
+ cmd.extend(_existing_scope_tokens(fallback_paths, root) or fallback_paths)
276
+
277
+ # Add quiet flag if requested
278
+ if quiet:
279
+ cmd.append("--quiet")
280
+
281
+ return cmd
282
+
283
+ def run_mypy_command(
284
+ self, cmd: list[str], project_root: Path, quiet: bool
285
+ ) -> SubprocessResult:
286
+ """Run the mypy command and return the result (统一子进程执行)."""
287
+ executor = SubprocessExecutor()
288
+ capture = quiet
289
+ return executor.run(command=cmd, cwd=project_root, quiet=capture)
290
+
291
+ def run_ruff_command(
292
+ self, cmd: list[str], project_root: Path, quiet: bool, noreport: bool
293
+ ) -> SubprocessResult:
294
+ """Run the ruff command and return the result (统一子进程执行)."""
295
+ executor = SubprocessExecutor()
296
+ capture = quiet or not noreport
297
+ return executor.run(command=cmd, cwd=project_root, quiet=capture)
298
+
299
+ def run_pre_commit_command(self, project_root: Path) -> PreCommitRun:
300
+ """Run the whole pre-commit suite once, for the report.
301
+
302
+ The verdict and the display text come from :class:`PreCommitRun`
303
+ (#165). Lint's only own decision is what a missing config means: it
304
+ falls back to pre-commit's default discovery instead of skipping the
305
+ run (#165 audit patch 2), which is why ``config`` is ``None`` there.
306
+
307
+ *Where* the managed config lives is not decided here: the shared
308
+ locator answers it with the ``DEPLOY`` anchor (#189). Looking it up at
309
+ the ``PROJECT`` root -- as this did -- missed the file whenever the
310
+ project root is a sub-package below the git top, and pre-commit then
311
+ failed with ``InvalidConfigError`` instead of discovering anything.
312
+ """
313
+ try:
314
+ managed = managed_pre_commit_config(project_root)
315
+ config = managed if managed.exists() else None
316
+ # Bare ``pre-commit`` first (#172): lint already runs inside the
317
+ # project environment, so ``uv run`` would only re-resolve it.
318
+ # ``run_pre_commit`` falls back to ``uv run`` if the launcher is
319
+ # absent. Format keeps the uv-first default for its uv-managed env.
320
+ return run_pre_commit(
321
+ files=None,
322
+ config=config,
323
+ cwd=project_root,
324
+ quiet=True,
325
+ prefer_uv=False,
326
+ )
327
+ except FileNotFoundError as e:
328
+ # Defensive parity with the reporter this replaced: a launcher that
329
+ # cannot be found reads as "not installed", not as a config error.
330
+ return PreCommitRun(
331
+ SubprocessResult(
332
+ returncode=-1, stdout="", stderr=str(e), launch_error=str(e)
333
+ )
334
+ )
335
+ except Exception as e:
336
+ # The same catch-all the reporter had: an unreadable config path is
337
+ # a reported pre-commit failure, never a lint crash.
338
+ return PreCommitRun(
339
+ SubprocessResult(
340
+ returncode=-1,
341
+ stdout=f"Error running pre-commit dry-run: {e}",
342
+ stderr="",
343
+ )
344
+ )
345
+
346
+ def run_all(
347
+ self,
348
+ project_root: Path,
349
+ paths: list[str] | None = None,
350
+ *,
351
+ quiet: bool = False,
352
+ fix: bool = False,
353
+ with_mypy: bool = True,
354
+ with_pre_commit: bool = True,
355
+ structured_ruff: bool = True,
356
+ format_code: int = 0,
357
+ ) -> LintRun:
358
+ """Run each requested tool once and return the finished :class:`LintRun`.
359
+
360
+ The caller picks the tool set (#170 audit patch 3): ``--noreport`` asks
361
+ for ruff alone, ``--report`` and the default path ask for all three.
362
+ ``structured_ruff`` is the same choice seen from the report's side --
363
+ only a rendered report needs ruff's ``--output-format full``
364
+ diagnostics.
365
+
366
+ Order matches the report: ruff, then pre-commit, then mypy. Pre-commit
367
+ hooks may rewrite files, so mypy must run after them to judge what is
368
+ actually on disk.
369
+ """
370
+ ruff_cmd = self.prepare_ruff_command(
371
+ paths,
372
+ fix,
373
+ quiet,
374
+ structured=structured_ruff,
375
+ project_root=project_root,
376
+ )
377
+ ruff = SubprocessExecutor().run(
378
+ command=ruff_cmd,
379
+ cwd=project_root,
380
+ # A structured run is captured even outside ``--quiet``: the
381
+ # report renders it instead of the child streaming it.
382
+ quiet=quiet or structured_ruff,
383
+ )
384
+
385
+ pre_commit = (
386
+ self.run_pre_commit_command(project_root) if with_pre_commit else None
387
+ )
388
+
389
+ mypy: SubprocessResult | None = None
390
+ if with_mypy:
391
+ # ``quiet=False`` on purpose: the report's "MyPy Results" section
392
+ # has always shown mypy's own summary line, even under ``--quiet``.
393
+ mypy_cmd = self.prepare_mypy_command(
394
+ None, quiet=False, project_root=project_root
395
+ )
396
+ mypy = SubprocessExecutor().run(
397
+ command=mypy_cmd, cwd=project_root, quiet=True
398
+ )
399
+
400
+ return LintRun(
401
+ ruff=ruff,
402
+ project_root=project_root,
403
+ mypy=mypy,
404
+ pre_commit=pre_commit,
405
+ paths=tuple(paths or ()),
406
+ quiet=quiet,
407
+ fix=fix,
408
+ format_code=format_code,
409
+ )