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,197 @@
1
+ """Utility module for test commands."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+
7
+ def _probe_pytest_cov(
8
+ python_executable: str, env: dict[str, str] | None = None
9
+ ) -> bool:
10
+ """pytest-cov 是否可被**这个解释器**导入。
11
+
12
+ 判定必须落在真正执行 pytest 的解释器上,而不是运行 wl 的那个进程:wl 由
13
+ uv tool 环境提供、测试却跑在项目 .venv 里时两者是两个解释器。只看 wl 进程
14
+ 会让 `--no-cov` 误判 —— 漏判时子进程转而吃 pyproject 的 `--cov=wl_cli`(于是
15
+ 写 coverage 数据文件、可能撞上 fail_under);多判时子进程收到 pytest 认不出
16
+ 的 `--no-cov`,直接以 "unrecognized arguments" 失败。
17
+
18
+ `env` 必须与 exec 该解释器时拿到的环境一致,探针与子进程才看同一份
19
+ PATH/site-packages。#108 起所有调用点都传执行用 env。
20
+
21
+ 解释器起不来(不存在 / 无法执行)时判 False:宁可少传 `--no-cov`,也不能传
22
+ 一个 pytest 认不出的 flag。
23
+ """
24
+ import subprocess
25
+
26
+ try:
27
+ probe = subprocess.run(
28
+ [python_executable, "-c", "import pytest_cov"],
29
+ capture_output=True,
30
+ check=False,
31
+ timeout=30,
32
+ env=env,
33
+ )
34
+ return probe.returncode == 0
35
+ except (OSError, subprocess.SubprocessError):
36
+ return False
37
+
38
+
39
+ def _ensure_pytest_available(python_executable: str) -> str:
40
+ """Ensure pytest is available, install if not."""
41
+ import subprocess
42
+
43
+ # 检查是否在测试环境中运行
44
+ is_testing = "PYTEST_CURRENT_TEST" in os.environ
45
+
46
+ # 只在非测试环境中检查和安装pytest
47
+ if not is_testing:
48
+ try:
49
+ subprocess.run(
50
+ [python_executable, "-m", "pytest", "--version"],
51
+ capture_output=True,
52
+ check=True,
53
+ text=True,
54
+ errors="replace",
55
+ )
56
+ except subprocess.CalledProcessError:
57
+ python_executable = _install_pytest(python_executable)
58
+ except FileNotFoundError:
59
+ # 如果Python环境不存在,抛出FileNotFoundError以便测试捕获
60
+ raise
61
+
62
+ return python_executable
63
+
64
+
65
+ def _install_pytest(python_executable: str) -> str:
66
+ """Install pytest into the selected environment, or exit with an install hint.
67
+
68
+ uv-managed venvs (``uv venv``) ship no pip module, so ``uv pip install`` is
69
+ tried first (with ``--python`` pinning the exact interpreter ``wl test``
70
+ selected); classic pip venvs fall back to ``python -m pip install``.
71
+ """
72
+ import shutil
73
+ import subprocess
74
+ import sys
75
+
76
+ print(
77
+ f"Error: pytest not found in the selected Python environment: {python_executable}"
78
+ )
79
+ print("Installing pytest...")
80
+
81
+ commands: list[list[str]] = []
82
+ uv_path = shutil.which("uv")
83
+ if uv_path:
84
+ commands.append(
85
+ [uv_path, "pip", "install", "--python", python_executable, "pytest"]
86
+ )
87
+ commands.append([python_executable, "-m", "pip", "install", "pytest"])
88
+
89
+ install_errors: list[str] = []
90
+ for command in commands:
91
+ try:
92
+ subprocess.run(
93
+ command,
94
+ check=True,
95
+ capture_output=True,
96
+ text=True,
97
+ errors="replace",
98
+ timeout=120,
99
+ )
100
+ except (
101
+ subprocess.CalledProcessError,
102
+ FileNotFoundError,
103
+ PermissionError,
104
+ subprocess.TimeoutExpired,
105
+ ) as exc:
106
+ install_errors.append(_install_failure_summary(command[0], exc))
107
+ continue
108
+ # Verify the install actually made pytest importable: uv --python can
109
+ # target an environment other than the one tests will invoke.
110
+ try:
111
+ subprocess.run(
112
+ [python_executable, "-m", "pytest", "--version"],
113
+ capture_output=True,
114
+ check=True,
115
+ text=True,
116
+ timeout=30,
117
+ )
118
+ except (
119
+ subprocess.CalledProcessError,
120
+ FileNotFoundError,
121
+ PermissionError,
122
+ subprocess.TimeoutExpired,
123
+ ):
124
+ continue
125
+ print("pytest installed successfully.")
126
+ return python_executable
127
+
128
+ print("Error: could not install pytest automatically.")
129
+ print("pytest is a test-only dependency of wl_cli (the `test` extra).")
130
+ print(
131
+ 'Install it with: uv pip install -e ".[test]" (or: pip install -e ".[test]")'
132
+ )
133
+ if install_errors:
134
+ print(f"Installer diagnostics: {' | '.join(install_errors)}")
135
+ sys.exit(1)
136
+
137
+
138
+ def _install_failure_summary(tool: str, exc: BaseException) -> str:
139
+ """One-line summary of a failed installer attempt, for the exit hint."""
140
+ import subprocess
141
+
142
+ if isinstance(exc, subprocess.TimeoutExpired):
143
+ return f"{tool}: timed out after 120s"
144
+ if isinstance(exc, subprocess.CalledProcessError):
145
+ tail = (exc.stderr or exc.stdout or "").strip().splitlines()[-2:]
146
+ if tail:
147
+ return f"{tool}: {'; '.join(tail)}"
148
+ return f"{tool}: exit {exc.returncode}"
149
+ return f"{tool}: {exc}"
150
+
151
+
152
+ def _process_command_args(
153
+ self, kwargs: dict[str, Any]
154
+ ) -> tuple[bool, bool, bool, bool, bool, bool, bool, bool, list[str]]:
155
+ """Process command line arguments."""
156
+ report_mode = kwargs.get("report", False)
157
+ debug_mode = kwargs.get("debug", False)
158
+ step_by_step = kwargs.get("step_by_step", False)
159
+ use_cache_arg = kwargs.get("use_cache", False)
160
+ no_cache_arg = kwargs.get("no_cache", False)
161
+ # wl test: default no cache
162
+ # wl test -s: default use cache
163
+ if no_cache_arg:
164
+ no_cache = True
165
+ elif use_cache_arg:
166
+ no_cache = False
167
+ else:
168
+ no_cache = not step_by_step # wl test: no cache, wl test -s: use cache
169
+ clean_cache = kwargs.get("clean_cache", False)
170
+ cache_info = kwargs.get("cache_info", False)
171
+ quiet = kwargs.get("quiet", False)
172
+ verbose = kwargs.get("verbose", False)
173
+ passed_args = kwargs.get("unknown_args", kwargs.get("args", []))
174
+
175
+ # 处理特殊情况:如果args中有'debug',将其移除并设置debug_mode为True
176
+ if "debug" in passed_args:
177
+ passed_args.remove("debug")
178
+ debug_mode = True
179
+
180
+ # 只在非测试环境中打印和记录信息
181
+ is_testing = "PYTEST_CURRENT_TEST" in os.environ
182
+ if not is_testing:
183
+ print(f"Debug mode: {debug_mode}")
184
+ # 传递concise参数,默认为False
185
+ self.progress_manager._log_report_mode(report_mode, False)
186
+
187
+ return (
188
+ report_mode,
189
+ debug_mode,
190
+ step_by_step,
191
+ no_cache,
192
+ clean_cache,
193
+ cache_info,
194
+ quiet,
195
+ verbose,
196
+ passed_args,
197
+ )
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from .coverage_parser import coverage_totals, file_entries, read_json
4
+ from .env_info import get_env_info
5
+ from .junit_parser import parse_junit, read_xml
6
+ from .markdown_utils import fmt_pct, write_md
7
+
8
+ __all__ = [
9
+ "coverage_totals",
10
+ "file_entries",
11
+ "fmt_pct",
12
+ "get_env_info",
13
+ "parse_junit",
14
+ "read_json",
15
+ "read_xml",
16
+ "write_md",
17
+ ]
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, cast
6
+
7
+
8
+ def read_json(path: str) -> dict[str, Any]:
9
+ """读取JSON文件并返回其内容。"""
10
+ p = Path(path)
11
+ if not p.exists():
12
+ return {}
13
+ try:
14
+ return cast("dict[str, Any]", json.loads(p.read_text(encoding="utf-8")))
15
+ except Exception:
16
+ return {}
17
+
18
+
19
+ def coverage_totals(
20
+ data: dict[str, Any],
21
+ ) -> tuple[float | None, float | None, int, int, int]:
22
+ """计算覆盖率统计信息。"""
23
+ files = data.get("files", {})
24
+ num_files = len(files) if isinstance(files, dict) else 0
25
+ covered_lines = 0
26
+ total_lines = 0
27
+ covered_branches = 0
28
+ total_branches = 0
29
+ if isinstance(files, dict):
30
+ for info in files.values():
31
+ s = info.get("summary", {})
32
+ try:
33
+ covered_lines += int(s.get("covered_lines", 0))
34
+ total_lines += int(s.get("num_statements", s.get("num_lines", 0)))
35
+ covered_branches += int(s.get("covered_branches", 0))
36
+ total_branches += int(s.get("num_branches", 0))
37
+ except Exception:
38
+ pass
39
+ line_pct = (covered_lines / total_lines * 100.0) if total_lines else None
40
+ branch_pct = (covered_branches / total_branches * 100.0) if total_branches else None
41
+ return (
42
+ line_pct,
43
+ branch_pct,
44
+ total_lines,
45
+ total_branches,
46
+ num_files,
47
+ )
48
+
49
+
50
+ def file_entries(
51
+ data: dict[str, Any], project_root: str
52
+ ) -> list[tuple[str, float | None, float | None]]:
53
+ """获取文件覆盖率条目列表。"""
54
+ files = data.get("files", {})
55
+ res: list[tuple[str, float | None, float | None]] = []
56
+ if isinstance(files, dict):
57
+ for path, info in files.items():
58
+ rel = path
59
+ try:
60
+ rel = str(
61
+ Path(path).resolve().relative_to(Path(project_root).resolve())
62
+ )
63
+ except Exception:
64
+ pass
65
+ s = info.get("summary", {})
66
+
67
+ # Get line coverage percentage
68
+ lp = s.get("percent_covered")
69
+ if lp is None:
70
+ lp = info.get("percent_covered")
71
+ # If still None, calculate from covered_lines and num_statements
72
+ if lp is None:
73
+ covered_lines = s.get("covered_lines", 0)
74
+ num_statements = s.get("num_statements", s.get("num_lines", 0))
75
+ if num_statements > 0:
76
+ lp = (covered_lines / num_statements) * 100
77
+
78
+ # Get branch coverage percentage
79
+ bp = s.get("percent_branches")
80
+ if bp is None:
81
+ bp = info.get("percent_branches")
82
+ # If still None, calculate from covered_branches and num_branches
83
+ if bp is None:
84
+ covered_branches = s.get("covered_branches", 0)
85
+ num_branches = s.get("num_branches", 0)
86
+ if num_branches > 0:
87
+ bp = (covered_branches / num_branches) * 100
88
+
89
+ try:
90
+ lp_f = float(lp) if lp is not None else None
91
+ except Exception:
92
+ lp_f = None
93
+ try:
94
+ bp_f = float(bp) if bp is not None else None
95
+ except Exception:
96
+ bp_f = None
97
+ res.append((rel, lp_f, bp_f))
98
+ return res
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import sys
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ # Try to import pytest for version information
9
+ _pytest: Any | None
10
+
11
+ try:
12
+ import pytest
13
+
14
+ _pytest = pytest
15
+ except ImportError:
16
+ _pytest = None
17
+
18
+
19
+ def get_env_info() -> list[str]:
20
+ """获取测试环境信息。"""
21
+ now = datetime.now().isoformat(timespec="seconds")
22
+ py_ver = sys.version.replace("\n", " ")
23
+ os_info = f"{platform.system()} {platform.release()} ({platform.machine()})"
24
+
25
+ # Get pytest version if available
26
+ if _pytest is not None:
27
+ pytest_ver = getattr(_pytest, "__version__", "unknown")
28
+ else:
29
+ pytest_ver = "unknown"
30
+
31
+ return [
32
+ f"时间: {now}",
33
+ f"Python: {py_ver}",
34
+ f"OS: {os_info}",
35
+ f"pytest: {pytest_ver}",
36
+ ]
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import xml.etree.ElementTree
4
+ from pathlib import Path
5
+
6
+
7
+ def read_xml(path: str) -> xml.etree.ElementTree.Element | None:
8
+ """读取XML文件并返回其根元素。"""
9
+ p = Path(path)
10
+ if not p.exists():
11
+ return None
12
+ try:
13
+ tree = xml.etree.ElementTree.parse(str(p))
14
+ return tree.getroot()
15
+ except Exception:
16
+ return None
17
+
18
+
19
+ def parse_junit(
20
+ root: xml.etree.ElementTree.Element | None,
21
+ ) -> tuple[int, int, int, int, int, float, list[dict[str, str]]]:
22
+ """解析JUnit XML数据并返回测试统计信息。"""
23
+ if root is None:
24
+ return (0, 0, 0, 0, 0, 0.0, [])
25
+ suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
26
+ tests = failures = errors = skipped = warnings = 0
27
+ time = 0.0
28
+ cases: list[dict[str, str]] = []
29
+ for ts in suites:
30
+ tests += int(ts.attrib.get("tests", 0))
31
+ failures += int(ts.attrib.get("failures", 0))
32
+ errors += int(ts.attrib.get("errors", 0))
33
+ skipped += int(ts.attrib.get("skipped", 0))
34
+ # 增加warnings计数
35
+ warnings += int(ts.attrib.get("warnings", 0))
36
+ try:
37
+ time += float(ts.attrib.get("time", 0.0))
38
+ except Exception:
39
+ pass
40
+ for tc in ts.findall("testcase"):
41
+ name = tc.attrib.get("name", "")
42
+ classname = tc.attrib.get("classname", "")
43
+ file_attr = tc.attrib.get("file", "")
44
+ text = ""
45
+ kind = ""
46
+ fail = tc.find("failure")
47
+ err = tc.find("error")
48
+ # 增加warning元素的查找
49
+ warn = tc.find("warning")
50
+ if fail is not None:
51
+ kind = "failure"
52
+ text = (
53
+ fail.attrib.get("message", "") + "\n" + (fail.text or "")
54
+ ).strip()
55
+ elif err is not None:
56
+ kind = "error"
57
+ text = (err.attrib.get("message", "") + "\n" + (err.text or "")).strip()
58
+ elif warn is not None:
59
+ kind = "warning"
60
+ text = (
61
+ warn.attrib.get("message", "") + "\n" + (warn.text or "")
62
+ ).strip()
63
+ if kind:
64
+ if len(text) > 1200:
65
+ text = text[:1200] + "\n..."
66
+ cases.append(
67
+ {
68
+ "name": name,
69
+ "classname": classname,
70
+ "file": file_attr,
71
+ "kind": kind,
72
+ "text": text,
73
+ }
74
+ )
75
+ return (tests, failures, errors, skipped, warnings, time, cases)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+
7
+ def fmt_pct(x: Any) -> str:
8
+ """格式化百分比值。"""
9
+ try:
10
+ v = float(x)
11
+ return f"{v:.2f}%"
12
+ except Exception:
13
+ return "N/A"
14
+
15
+
16
+ def write_md(path: str, lines: list[str]) -> None:
17
+ """将文本行写入Markdown文件。"""
18
+ p = Path(path)
19
+ p.parent.mkdir(parents=True, exist_ok=True)
20
+ p.write_text("\n".join(lines) + "\n", encoding="utf-8")
@@ -0,0 +1,61 @@
1
+ """The test commands subsystem's project-root **policy** entry point.
2
+
3
+ This is not a root resolver: the root module (:mod:`wl_cli.utils.root`) owns
4
+ *locating* a root. What lives here is the test subsystem's own answer to "where
5
+ does this subsystem's ``src/`` and ``tests/`` tree live", which is pytest's
6
+ rootdir question rather than a definition of the project root.
7
+
8
+ Two rules make it up, and both stay in the subsystem on purpose (issue #120's
9
+ ruling on #116's note_13750):
10
+
11
+ - the ``tests/``/``src/`` pre-check -- a directory that already has one of those
12
+ *is* the root, with no marker lookup at all. The root module's ``PROJECT`` kind
13
+ is marker-based instead, so the two rules genuinely differ and the pre-check
14
+ cannot be folded into it;
15
+ - the caller-side fallback -- when no marker exists the subsystem answers the
16
+ starting directory rather than "not found" (ADR 0006: the fallback is the
17
+ caller's policy, never ``resolve``'s).
18
+
19
+ Issue #110 introduced this policy twice: an un-anchored spelling that read the
20
+ process working directory and a mirrored anchored spelling, so a detector
21
+ anchored outside the CWD would not leak the CWD back in. The hard cut (#123)
22
+ merges them into this single ``start``-parameterised form; ``start=None`` means
23
+ the working directory, which is exactly what the un-anchored spelling
24
+ hard-coded, and every other behaviour is unchanged.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ from pathlib import Path
31
+
32
+ from ....utils.root import RootKind
33
+ from ....utils.root_fallback import resolve_root_or_start
34
+
35
+
36
+ def project_root_for(start: Path | None = None) -> str:
37
+ """The test subsystem's project root for *start*, as a string.
38
+
39
+ ``start=None`` asks about the process working directory. The answer is
40
+ *start* itself when it already looks like a project (it has ``tests/`` or
41
+ ``src/``); otherwise the root module's ``PROJECT`` root; and when neither
42
+ exists, *start*. Never returns ``None``.
43
+
44
+ The last step is the shared CLI-top-level fallback policy, not a private
45
+ copy of it: :func:`~wl_cli.utils.root_fallback.resolve_root_or_start` (#126).
46
+ Its answer is taken as ``str`` (this subsystem's contract is a string) and
47
+ ``start`` is passed verbatim (this helper never canonicalised it), keeping
48
+ the answer byte-for-byte what the spelling it replaces returned.
49
+ """
50
+ if start is None:
51
+ try:
52
+ start = Path.cwd()
53
+ except Exception:
54
+ return os.getcwd()
55
+ try:
56
+ if (start / "tests").exists() or (start / "src").exists():
57
+ return str(start)
58
+ # ADR 0006: 回退是调用方策略;唯一实现在 utils/root_fallback.py(#126)
59
+ return str(resolve_root_or_start(start, RootKind.PROJECT).path)
60
+ except Exception:
61
+ return str(start)
@@ -0,0 +1,9 @@
1
+ """
2
+ Update command module for wl_cli.
3
+
4
+ This module provides the 'wl update' command family.
5
+ """
6
+
7
+ from .update_command import update_command
8
+
9
+ __all__ = ["update_command"]
@@ -0,0 +1,21 @@
1
+ """
2
+ Services for update command.
3
+ """
4
+
5
+ from wl_cli.commands.update.services.backup_manager import BackupManager
6
+ from wl_cli.commands.update.services.component_manager import ComponentManager
7
+ from wl_cli.commands.update.services.conflict_resolver import (
8
+ ConflictResolution,
9
+ ConflictResolver,
10
+ )
11
+ from wl_cli.commands.update.services.vendors_updater import VendorsUpdater
12
+ from wl_cli.commands.update.services.version_tracker import VersionTracker
13
+
14
+ __all__ = [
15
+ "ComponentManager",
16
+ "VersionTracker",
17
+ "VendorsUpdater",
18
+ "BackupManager",
19
+ "ConflictResolver",
20
+ "ConflictResolution",
21
+ ]