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,183 @@
1
+ """Coverage cache management module (the coverage cache adapter).
2
+
3
+ This is one of the three adapters over the single cache seam
4
+ :class:`~wl_cli.utils.cache_ledger.CacheLedger` (#213, ADR-0011). It owns what
5
+ this cache decides for itself -- the key inputs, the user-level location and the
6
+ 7-day TTL. Key spelling, storage, freshness and the error policy are the
7
+ ledger's.
8
+
9
+ The key stays on file **stat** metadata (``mtime_ns`` / ``ctime_ns`` / ``size``)
10
+ rather than file content: ``ctime`` cannot be restored by the user, so a content
11
+ change always moves the key, and the coverage pass does not have to re-read every
12
+ source and test file just to build a key (the performance trade-off recorded in
13
+ #15). What #213 changed is the failure path: when a file cannot be ``stat``-ed
14
+ there is no truthful key, so **nothing is cached** for that run instead of
15
+ silently keying on the path alone.
16
+ """
17
+
18
+ from pathlib import Path
19
+ from typing import Any, cast
20
+
21
+ from ...utils.cache_ledger import CacheInputs, CacheLedger
22
+ from ...utils.config import get_config
23
+
24
+ _DEFAULT_CACHE_ROOT = Path.home() / ".cache" / "wl"
25
+
26
+ #: Returned by :meth:`CoverageCacheManager.get_cache_key` when a file's
27
+ #: ``stat`` failed, so no truthful key exists. ``KeyedStore`` refuses the empty
28
+ #: string as a key, and the two cache operations below guard on it explicitly:
29
+ #: a read is a miss and a write is a no-op. The call sites therefore need no
30
+ #: special case -- an unkeyed run simply never caches.
31
+ UNKEYED = ""
32
+
33
+
34
+ def _default_coverage_cache_dir() -> Path:
35
+ """Resolve the default coverage cache directory.
36
+
37
+ Mirrors the test-cache root resolution (the ``test_cache.cache_dir`` config
38
+ key, falling back to ``~/.cache/wl``) and appends ``test-cache/coverage``.
39
+
40
+ Returns:
41
+ Path to the default coverage cache directory.
42
+ """
43
+ configured = get_config("test_cache.cache_dir", None)
44
+ root = (
45
+ Path(configured).expanduser()
46
+ if isinstance(configured, str) and configured
47
+ else _DEFAULT_CACHE_ROOT
48
+ )
49
+ return root / "test-cache" / "coverage"
50
+
51
+
52
+ class CoverageCacheManager:
53
+ """Cache whole ``coverage.json`` payloads, keyed by the files they describe.
54
+
55
+ Responsibilities:
56
+ - Answer the key inputs: every source and test file's path plus its stat
57
+ metadata, in sorted path order
58
+ - Read and write coverage payloads through the ledger
59
+ - Own the location (user-level) and the TTL (7 days); the ledger owns the
60
+ mechanism
61
+ """
62
+
63
+ CACHE_EXPIRY_SECONDS = 7 * 86400 # 7 days
64
+ NAMESPACE = "coverage"
65
+
66
+ def __init__(self, cache_dir: Path | None = None) -> None:
67
+ """Initialize CoverageCacheManager.
68
+
69
+ Args:
70
+ cache_dir: Directory holding coverage cache entries. Defaults to
71
+ ``<test_cache.cache_dir>/test-cache/coverage``.
72
+ """
73
+ self._ledger = CacheLedger(
74
+ cache_dir if cache_dir is not None else _default_coverage_cache_dir(),
75
+ self.CACHE_EXPIRY_SECONDS,
76
+ )
77
+
78
+ def get_cache_key(self, source_files: list[Path], test_files: list[Path]) -> str:
79
+ """Build the coverage cache key from the files the coverage describes.
80
+
81
+ Args:
82
+ source_files: List of source code files
83
+ test_files: List of corresponding test files
84
+
85
+ Returns:
86
+ The ledger key (``coverage_`` plus the full SHA-256 digest), or
87
+ :data:`UNKEYED` when any file's ``stat`` failed. In that case the
88
+ caller gets a miss on read and a no-op on write: a key that ignored
89
+ a file's identity would serve one run's coverage to another run.
90
+ """
91
+ parts: list[str] = []
92
+ for file_path in (
93
+ *sorted(source_files, key=str),
94
+ *sorted(test_files, key=str),
95
+ ):
96
+ part = self._file_part(file_path)
97
+ if part is None:
98
+ return UNKEYED
99
+ parts.append(part)
100
+
101
+ return self._ledger.key(CacheInputs(self.NAMESPACE, tuple(parts)))
102
+
103
+ def get_cached_coverage(self, cache_key: str) -> dict[str, Any] | None:
104
+ """Get cached coverage data from the cache.
105
+
106
+ A read failure of any kind -- missing entry, unreadable file, corrupt
107
+ JSON, an expired entry -- is a miss, never an exception.
108
+
109
+ Args:
110
+ cache_key: The cache key to look up; :data:`UNKEYED` always misses.
111
+
112
+ Returns:
113
+ Cached coverage data as dictionary, or None if there is no usable
114
+ entry.
115
+ """
116
+ if not cache_key:
117
+ return None
118
+ entry = self._ledger.get(cache_key)
119
+ if entry is None:
120
+ return None
121
+
122
+ coverage_data = entry.get("coverage_data")
123
+ return cast("dict[str, Any] | None", coverage_data) if coverage_data else None
124
+
125
+ def save_cached_coverage(
126
+ self, cache_key: str, coverage_data: dict[str, Any]
127
+ ) -> bool:
128
+ """Save coverage data to the cache, atomically.
129
+
130
+ A failed write never breaks the coverage run: the ledger logs it and
131
+ returns ``False``.
132
+
133
+ Args:
134
+ cache_key: The cache key to use; :data:`UNKEYED` writes nothing.
135
+ coverage_data: Coverage data to cache.
136
+
137
+ Returns:
138
+ True if the entry was written; False when the key is
139
+ :data:`UNKEYED` or the write failed.
140
+ """
141
+ if not cache_key:
142
+ return False
143
+ return self._ledger.put(
144
+ cache_key,
145
+ {"coverage_data": coverage_data, "version": "2.0"},
146
+ )
147
+
148
+ @staticmethod
149
+ def _file_part(file_path: Path) -> str | None:
150
+ """One file's contribution to the key, or ``None`` when it is unknown.
151
+
152
+ The part is the normalized path plus the stat metadata the key has
153
+ always used (``mtime_ns`` / ``ctime_ns`` / ``size``). A ``stat``
154
+ failure returns ``None`` rather than a path-only part: the file's
155
+ identity is then unknown, and an entry keyed without it could be served
156
+ to a later run that is not this run (#213, replacing the old
157
+ ``except Exception: pass``).
158
+
159
+ Args:
160
+ file_path: The file to describe.
161
+
162
+ Returns:
163
+ The part, or ``None`` if ``stat`` failed.
164
+ """
165
+ try:
166
+ stat_result = file_path.stat()
167
+ except (OSError, ValueError):
168
+ # ``OSError``: the file vanished or cannot be inspected. ``ValueError``:
169
+ # the path itself is unusable (an embedded NUL), which can name no
170
+ # file and so can yield no truthful key either. Both mean the same
171
+ # thing here, and nothing else is expected from ``stat`` (#213
172
+ # review NIT-7).
173
+ return None
174
+
175
+ path_str = str(file_path).replace("\\", "/")
176
+ return "\0".join(
177
+ (
178
+ path_str,
179
+ str(stat_result.st_mtime_ns),
180
+ str(stat_result.st_ctime_ns),
181
+ str(stat_result.st_size),
182
+ )
183
+ )
@@ -0,0 +1,79 @@
1
+ """Progress manager module for test commands."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ class ProgressManager:
8
+ """Progress manager for displaying test progress."""
9
+
10
+ def _truncate_filename(self, filename):
11
+ """Truncate filename to fit in progress bar."""
12
+ max_len = 28
13
+ if len(filename) > max_len:
14
+ return filename[: max_len - 3] + "..."
15
+ return filename
16
+
17
+ def _update_test_progress(self, pbar, truncated_name, test_file, cache_hits):
18
+ """Update progress bar for test processing with cache details."""
19
+ # Check if this file used cached result
20
+ if test_file.name in cache_hits:
21
+ # Show cached status in progress bar
22
+ pbar.set_description(f"Testing {truncated_name:<28} [cached]")
23
+ else:
24
+ # Normal testing status
25
+ pbar.set_description(f"Testing {truncated_name:<28}")
26
+
27
+ pbar.update(1)
28
+
29
+ def _log_report_mode(self, report_mode: bool, concise: bool = False) -> None:
30
+ try:
31
+ from ...utils.logging import log_info_pair
32
+
33
+ if not concise:
34
+ log_info_pair(
35
+ f"Report mode: {report_mode}",
36
+ f"报告模式: {report_mode}",
37
+ )
38
+ except Exception:
39
+ pass
40
+
41
+ def _log_project_paths(
42
+ self, project_root: str, todo_dir: Path, concise: bool = False
43
+ ) -> None:
44
+ try:
45
+ from ...utils.logging import log_info_pair
46
+
47
+ if not concise:
48
+ log_info_pair(
49
+ f"Project root: {project_root}",
50
+ f"项目根目录: {project_root}",
51
+ )
52
+ log_info_pair(
53
+ f"Todos dir: {todo_dir}",
54
+ f"报告目录: {todo_dir}",
55
+ )
56
+ except Exception:
57
+ pass
58
+
59
+ def _build_env(self, project_root: str) -> dict:
60
+ env = os.environ.copy()
61
+ src_path = os.path.join(project_root, "src")
62
+ if "PYTHONPATH" in env:
63
+ env["PYTHONPATH"] = f"{src_path}{os.pathsep}{env['PYTHONPATH']}"
64
+ else:
65
+ env["PYTHONPATH"] = src_path
66
+ return env
67
+
68
+ def project_root(self) -> str:
69
+ """The project root the test run is anchored to, as a string.
70
+
71
+ A plain forwarder to the test subsystem's policy entry point
72
+ (:func:`~wl_cli.commands.testcommands.utils.project_root.project_root_for`),
73
+ which owns the ``tests/``/``src/`` pre-check and the CWD fallback
74
+ (ADR 0006: the fallback is the caller's policy, not the root module's).
75
+ It stays a method so callers keep a single, mockable seam.
76
+ """
77
+ from .utils.project_root import project_root_for
78
+
79
+ return project_root_for()
@@ -0,0 +1,207 @@
1
+ """
2
+ Python environment detection utilities for test command.
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ...utils.logging import log_debug
11
+ from ...utils.platform_adapter import PlatformAdapter
12
+ from ...utils.workspace_detector import WorkspaceDetector
13
+ from .utils.project_root import project_root_for
14
+
15
+
16
+ class PythonEnvDetector:
17
+ """Detect Python executable in different environments.
18
+
19
+ ``root`` is the directory the detection is anchored to; ``None`` -- the
20
+ default -- keeps the historical behaviour of resolving against the process
21
+ working directory, which is what ``wl test`` relies on (it is invoked *in*
22
+ the directory it is asked about). A caller whose subject runs somewhere else
23
+ must say so explicitly: ``wl gate``'s pytest leg is ``wl test`` launched in
24
+ the chain's anchor root, so its preflight has to ask about *that* root
25
+ rather than wherever the user happened to stand (issue #110).
26
+ """
27
+
28
+ def __init__(self, root: Path | None = None):
29
+ """Initialize Python environment detector.
30
+
31
+ Args:
32
+ root: Directory to anchor every lookup to. ``None`` means "the
33
+ process working directory", resolved lazily on each use so a
34
+ caller that never passes a root behaves exactly as before.
35
+ """
36
+ self.detector = WorkspaceDetector()
37
+ self._root = root
38
+
39
+ @property
40
+ def root(self) -> Path:
41
+ """The directory this detector resolves against."""
42
+ return Path.cwd() if self._root is None else self._root
43
+
44
+ def get_python_executable(self) -> str:
45
+ """
46
+ Get the Python executable path, using the same logic as wl build dist.
47
+
48
+ Returns:
49
+ str: Path to the Python executable
50
+ """
51
+ try:
52
+ python_exe, _ = self._resolve_python()
53
+ return python_exe or sys.executable
54
+ except Exception:
55
+ # 如果出现问题,回退到系统Python
56
+ return sys.executable
57
+
58
+ def _resolve_python(self) -> tuple[str | None, bool]:
59
+ """
60
+ Resolve Python executable path, with enhanced virtual environment detection.
61
+
62
+ Returns:
63
+ tuple: (python_path, is_virtual_env)
64
+ """
65
+ # 1. 检查是否在uv工作区环境中
66
+ is_workspace = self.detector.detect(self.root)
67
+ log_debug(f"uv workspace detected: {is_workspace}")
68
+
69
+ # 如果不在工作区,使用当前运行的Python解释器
70
+ if not is_workspace:
71
+ return sys.executable, False
72
+
73
+ # 2. 尝试通过不同方式查找虚拟环境中的Python
74
+ python_path = self._find_python_in_uv_workspace()
75
+ if python_path:
76
+ return python_path, True
77
+
78
+ python_path = self._find_python_in_custom_venv()
79
+ if python_path:
80
+ return python_path, True
81
+
82
+ python_path = self._find_python_in_environment_venv()
83
+ if python_path:
84
+ return python_path, True
85
+
86
+ # 3. 最后回退到系统Python
87
+ log_debug(f"Falling back to system Python: {sys.executable}")
88
+ return sys.executable, False
89
+
90
+ def _find_python_in_uv_workspace(self) -> str | None:
91
+ """查找uv工作区中的Python可执行文件。"""
92
+ venv_root = self.detector.get_venv_path(self.root)
93
+ if not venv_root:
94
+ return None
95
+
96
+ log_debug(f"get_venv_path result: {venv_root}")
97
+
98
+ # 尝试标准Python路径
99
+ python_exe = self._get_python_path_from_venv(venv_root)
100
+ if self._is_valid_python_executable(python_exe):
101
+ return python_exe
102
+
103
+ # 尝试python3路径
104
+ if not PlatformAdapter.is_windows():
105
+ python_exe_alt = str((venv_root / "bin" / "python3").absolute())
106
+ if self._is_valid_python_executable(python_exe_alt):
107
+ return python_exe_alt
108
+
109
+ # 尝试使用uv run
110
+ return self._get_python_using_uv_run()
111
+
112
+ def _find_python_in_custom_venv(self) -> str | None:
113
+ """使用_find_venv方法查找Python。"""
114
+ venv_path = self._find_venv()
115
+ if not venv_path:
116
+ return None
117
+
118
+ venv_root = Path(venv_path)
119
+ print(f"Debug: Found venv using _find_venv: {venv_root}")
120
+
121
+ python_exe = self._get_python_path_from_venv(venv_root)
122
+ if self._is_valid_python_executable(python_exe):
123
+ return python_exe
124
+
125
+ return None
126
+
127
+ def _find_python_in_environment_venv(self) -> str | None:
128
+ """通过VIRTUAL_ENV环境变量查找Python。"""
129
+ virtual_env = os.environ.get("VIRTUAL_ENV")
130
+ if not virtual_env:
131
+ return None
132
+
133
+ venv_root = Path(virtual_env)
134
+ print(f"Debug: Using VIRTUAL_ENV: {venv_root}")
135
+
136
+ python_exe = self._get_python_path_from_venv(venv_root)
137
+ if self._is_valid_python_executable(python_exe):
138
+ return python_exe
139
+
140
+ return None
141
+
142
+ def _get_python_path_from_venv(self, venv_root: Path) -> str:
143
+ """从虚拟环境根目录构造Python可执行文件路径。"""
144
+ if PlatformAdapter.is_windows():
145
+ return str((venv_root / "Scripts" / "python.exe").absolute())
146
+ else:
147
+ return str((venv_root / "bin" / "python").absolute())
148
+
149
+ def _is_valid_python_executable(self, python_path: str) -> bool:
150
+ """检查Python可执行文件是否存在。"""
151
+ log_debug(f"Checking Python executable at: {python_path}")
152
+ if Path(python_path).exists():
153
+ log_debug(f"Successfully found Python executable: {python_path}")
154
+ return True
155
+ log_debug(f"Python executable does not exist at: {python_path}")
156
+ return False
157
+
158
+ def _get_python_using_uv_run(self) -> str | None:
159
+ """使用uv run获取Python可执行文件路径。
160
+
161
+ ``cwd`` 与其它查找同源:``uv run`` 解析的是**运行目录**所属的项目,
162
+ 所以锚定 root 的调用必须在这里也用它,否则会问到另一个项目(#110)。
163
+ ``None`` 仍然是「继承进程工作目录」,与改动前逐字节相同。
164
+ """
165
+ try:
166
+ result = subprocess.run(
167
+ [
168
+ "uv",
169
+ "run",
170
+ "python",
171
+ "-c",
172
+ "import sys; print(sys.executable)",
173
+ ],
174
+ cwd=self._root,
175
+ capture_output=True,
176
+ text=True,
177
+ check=True,
178
+ )
179
+ uv_python = result.stdout.strip()
180
+ log_debug(f"Using uv run python result: {uv_python}")
181
+ if Path(uv_python).exists():
182
+ return uv_python
183
+ except Exception as e:
184
+ log_debug(f"uv run python failed: {e}")
185
+ return None
186
+
187
+ def _find_venv(self) -> str | None:
188
+ """
189
+ Find virtual environment path if it exists.
190
+
191
+ Returns:
192
+ str | None: Path to virtual environment or None if not found
193
+ """
194
+
195
+ project_root = project_root_for(self._root)
196
+
197
+ # Check common virtual environment locations
198
+ possible_venv_paths = [
199
+ os.path.join(project_root, ".venv"),
200
+ os.path.join(project_root, "venv"),
201
+ ]
202
+
203
+ for venv_path in possible_venv_paths:
204
+ if os.path.exists(os.path.join(venv_path, "pyvenv.cfg")):
205
+ return venv_path
206
+
207
+ return None
@@ -0,0 +1,101 @@
1
+ """Report generator for test results."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+
6
+ from ...utils.logging import log_info_pair
7
+ from .test_reporter import generate_reports
8
+
9
+
10
+ class ReportGenerator:
11
+ """Generates test reports after test completion."""
12
+
13
+ def generate(
14
+ self,
15
+ todo_dir: Path,
16
+ project_root: str,
17
+ verbose: bool = False,
18
+ ) -> None:
19
+ """Generate coverage and error reports."""
20
+ try:
21
+ cov_json = str(todo_dir / "coverage.json")
22
+ junit_xml = str(todo_dir / "junit.xml")
23
+ cov_md = str(todo_dir / "coverage.md")
24
+ err_md = str(todo_dir / "error.md")
25
+
26
+ self._log_report_paths(cov_json, junit_xml, verbose)
27
+
28
+ summary = generate_reports(
29
+ coverage_json_path=cov_json,
30
+ junit_xml_path=junit_xml,
31
+ out_cov=cov_md,
32
+ out_err=err_md,
33
+ project_root=project_root,
34
+ )
35
+
36
+ self._log_output_paths(cov_md, err_md, verbose)
37
+
38
+ if summary and verbose:
39
+ print(summary)
40
+
41
+ # Ensure files are fully written
42
+ time.sleep(0.5)
43
+ self._cleanup_temp_files(cov_json, junit_xml)
44
+ except Exception as e:
45
+ print(f"Failed to generate markdown reports: {e}")
46
+
47
+ def _log_report_paths(
48
+ self,
49
+ cov_json: str,
50
+ junit_xml: str,
51
+ verbose: bool = False,
52
+ ) -> None:
53
+ """Log report file paths."""
54
+ try:
55
+ if verbose:
56
+ log_info_pair(
57
+ f"Coverage JSON path: {cov_json}",
58
+ f"Coverage JSON路径: {cov_json}",
59
+ )
60
+ log_info_pair(
61
+ f"JUnit XML path: {junit_xml}",
62
+ f"JUnit XML路径: {junit_xml}",
63
+ )
64
+ log_info_pair(
65
+ f"Coverage JSON exists: {Path(cov_json).exists()}",
66
+ f"覆盖率JSON存在: {Path(cov_json).exists()}",
67
+ )
68
+ log_info_pair(
69
+ f"JUnit XML exists: {Path(junit_xml).exists()}",
70
+ f"JUnit XML存在: {Path(junit_xml).exists()}",
71
+ )
72
+ except Exception:
73
+ pass
74
+
75
+ def _log_output_paths(
76
+ self,
77
+ cov_md: str,
78
+ err_md: str,
79
+ verbose: bool = False,
80
+ ) -> None:
81
+ """Log output report paths."""
82
+ try:
83
+ if verbose:
84
+ log_info_pair(
85
+ f"Coverage MD path: {cov_md}",
86
+ f"Coverage MD存在: {Path(cov_md).exists()}",
87
+ )
88
+ log_info_pair(
89
+ f"Error MD path: {err_md}",
90
+ f"错误MD存在: {Path(err_md).exists()}",
91
+ )
92
+ except Exception:
93
+ pass
94
+
95
+ def _cleanup_temp_files(self, cov_json: str, junit_xml: str) -> None:
96
+ """Clean up temporary files after report generation."""
97
+ try:
98
+ Path(cov_json).unlink(missing_ok=True)
99
+ Path(junit_xml).unlink(missing_ok=True)
100
+ except Exception:
101
+ pass