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,174 @@
1
+ """
2
+ Windows update functionality for self update command.
3
+ """
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+
11
+ from ....utils.platform_adapter import PlatformAdapter
12
+
13
+
14
+ class WindowsUpdate:
15
+ """Windows-specific update functionality for the wl command."""
16
+
17
+ def handle_access_error(self, uv_path: str = "") -> None:
18
+ """
19
+ Handle Windows access denied errors with either delayed update or manual instructions.
20
+ """
21
+ from wl_cli.utils.logging import log_info_pair
22
+
23
+ if PlatformAdapter.is_windows():
24
+ log_info_pair(
25
+ "Detected file in use error on Windows. Trying delayed update mechanism...",
26
+ "在Windows上检测到文件被占用错误。尝试延迟更新机制...",
27
+ )
28
+ if not uv_path:
29
+ # ``or ""`` keeps the declared ``str`` type honest; ``which``
30
+ # returns ``None`` when uv is absent. Behaviour is unchanged --
31
+ # both ``None`` and ``""`` fail the truthiness test below and
32
+ # fall through to the "Failed to find uv executable" branch.
33
+ uv_path = shutil.which("uv") or ""
34
+ if uv_path:
35
+ env = os.environ.copy()
36
+ env["PYTHONIOENCODING"] = "utf-8"
37
+ env["PYTHONLEGACYWINDOWSFSENCODING"] = "1"
38
+ self._run_delayed_update(uv_path, env)
39
+ return
40
+ else:
41
+ log_info_pair(
42
+ "Failed to find uv executable for delayed update.",
43
+ "无法找到uv可执行文件进行延迟更新。",
44
+ )
45
+
46
+ log_info_pair(
47
+ "Error updating wl command: Access denied. This is a common issue on Windows when trying to update a running tool.",
48
+ "错误:更新wl命令失败: 权限被拒绝。这是在Windows上尝试更新正在运行的工具时的常见问题。",
49
+ )
50
+ log_info_pair(
51
+ "Please try one of the following solutions:", "请尝试以下解决方案之一:"
52
+ )
53
+ log_info_pair(
54
+ "1. Close all wl command windows and run 'wl self update' again",
55
+ " 关闭所有wl命令窗口,然后再次运行'wl self update'",
56
+ )
57
+ log_info_pair("2. Run the command as administrator", " 以管理员身份运行命令")
58
+ log_info_pair(
59
+ "3. Use 'uv tool install --editable .' directly",
60
+ " 直接使用'uv tool install --editable .'命令",
61
+ )
62
+ sys.exit(1)
63
+
64
+ def _run_delayed_update(self, uv_path: str, env: dict) -> None:
65
+ """
66
+ Run delayed update on Windows by creating a separate script that executes after the main process exits.
67
+ """
68
+ from wl_cli.utils.logging import log_info_pair
69
+
70
+ env_repr = repr(env)
71
+ uv_path_escaped = uv_path.replace("\\", "\\\\")
72
+
73
+ script_content = f"""
74
+ import subprocess
75
+ import time
76
+ import sys
77
+ import os
78
+
79
+ print("Waiting for main process to exit...")
80
+ print("等待主进程退出...")
81
+ time.sleep(3)
82
+
83
+ os.environ.update({env_repr})
84
+
85
+ try:
86
+ print("Uninstalling existing wl_cli...")
87
+ print("正在卸载现有wl_cli...")
88
+ subprocess.run([\"{uv_path_escaped}\", \"tool\", \"uninstall\", \"wl_cli\"], check=False, capture_output=True, text=True, encoding="utf-8")
89
+
90
+ print("Installing wl_cli...")
91
+ print("正在安装wl_cli...")
92
+ primary_cmd = [\"{uv_path_escaped}\", \"tool\", \"install\", \"--editable\", \".\"]
93
+ subprocess.run(primary_cmd, check=True, capture_output=False, text=True, encoding="utf-8")
94
+
95
+ print("\\nWL command updated successfully!")
96
+ print("wl命令更新成功!")
97
+ print("You can close this window now.")
98
+ print("现在可以关闭此窗口。")
99
+ except subprocess.CalledProcessError as e:
100
+ print("\\nError updating wl command: " + str(e))
101
+ print("更新wl命令失败: " + str(e))
102
+ try:
103
+ print("\\nTrying alternative installation method...")
104
+ print("尝试备选安装方法...")
105
+ alternative_cmd = [\"{uv_path_escaped}\", \"tool\", \"install\", \"--editable\", \".\", \"--python-preference\", \"only-system\"]
106
+ subprocess.run(alternative_cmd, check=True, capture_output=False, text=True, encoding="utf-8")
107
+ print("\\nWL command updated successfully with alternative method!")
108
+ print("使用备选方法成功更新wl命令!")
109
+ except subprocess.CalledProcessError as alt_e:
110
+ print("\\nAlternative installation also failed: " + str(alt_e))
111
+ print("备选安装方法也失败: " + str(alt_e))
112
+ print("Please try manual installation:")
113
+ print("请尝试手动安装:")
114
+ print("uv tool install --editable .")
115
+ print("Press Enter to exit...")
116
+ input()
117
+ except Exception as e:
118
+ print("\\nUnexpected error: " + str(e))
119
+ print("意外错误: " + str(e))
120
+ print("Press Enter to exit...")
121
+ input()
122
+ """
123
+
124
+ with tempfile.NamedTemporaryFile(
125
+ mode="w", suffix=".py", delete=False, encoding="utf-8"
126
+ ) as f:
127
+ f.write(script_content)
128
+ temp_script_path = f.name
129
+
130
+ log_info_pair(
131
+ f"Running delayed update script: {temp_script_path}",
132
+ f"正在运行延迟更新脚本: {temp_script_path}",
133
+ )
134
+
135
+ try:
136
+ subprocess.Popen(
137
+ [sys.executable, temp_script_path],
138
+ shell=True,
139
+ creationflags=getattr(subprocess, "CREATE_NEW_CONSOLE", 0),
140
+ )
141
+ log_info_pair(
142
+ "Delayed update started. The update will continue in a new window.",
143
+ "延迟更新已启动。更新将在新窗口中继续进行。",
144
+ )
145
+ sys.exit(0)
146
+ except Exception as e:
147
+ log_info_pair(
148
+ f"Failed to start delayed update: {e}", f"启动延迟更新失败: {e}"
149
+ )
150
+ self._show_manual_instructions()
151
+
152
+ def _show_manual_instructions(self) -> None:
153
+ """
154
+ Show manual installation instructions to the user.
155
+ """
156
+ from wl_cli.utils.logging import log_info_pair
157
+
158
+ log_info_pair(
159
+ "Error updating wl command: Access denied. This is a common issue on Windows when trying to update a running tool.",
160
+ "错误:更新wl命令失败: 权限被拒绝。这是在Windows上尝试更新正在运行的工具时的常见问题。",
161
+ )
162
+ log_info_pair(
163
+ "Please try one of the following solutions:", "请尝试以下解决方案之一:"
164
+ )
165
+ log_info_pair(
166
+ "1. Close all wl command windows and run 'wl self update' again",
167
+ " 关闭所有wl命令窗口,然后再次运行'wl self update'",
168
+ )
169
+ log_info_pair("2. Run the command as administrator", " 以管理员身份运行命令")
170
+ log_info_pair(
171
+ "3. Use 'uv tool install --editable .' directly",
172
+ " 直接使用'uv tool install --editable .'命令",
173
+ )
174
+ sys.exit(1)
@@ -0,0 +1,8 @@
1
+ """
2
+ Test commands package initialization.
3
+ """
4
+
5
+ # Import the test command to register it
6
+ from .test_commands import TestCommand
7
+
8
+ __all__ = ["TestCommand"]
@@ -0,0 +1,198 @@
1
+ """Base dependency analyzer with shared AST-based dependency analysis logic."""
2
+
3
+ import ast
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from .utils.project_root import project_root_for
8
+
9
+
10
+ class BaseDependencyAnalyzer:
11
+ """Base class with shared dependency analysis logic using AST parsing.
12
+
13
+ Provides common dependency resolution methods that can be inherited by
14
+ classes that need dependency analysis functionality.
15
+ """
16
+
17
+ def __init__(
18
+ self, module_file_cache: dict[tuple[str, str], Path | None] | None = None
19
+ ):
20
+ """Initialize with optional module file cache.
21
+
22
+ Args:
23
+ module_file_cache: Optional cache for found module files
24
+ Caches (module_name, src_dir_str) -> Path found result
25
+ """
26
+ self._dependency_cache: dict[str, tuple[list[Path], float]] = {}
27
+ self._module_file_cache: dict[tuple[str, str], Path | None] = (
28
+ module_file_cache or {}
29
+ )
30
+
31
+ def _get_test_dependencies(self, test_file: Path) -> list[Path]:
32
+ """Get test file dependencies by analyzing imports, including indirect dependencies."""
33
+ # Check dependency cache first
34
+ cache_key = str(test_file)
35
+ if cache_key in self._dependency_cache:
36
+ # Cache entry stores (dependencies, mtime) tuple
37
+ cached_deps, cached_mtime = self._dependency_cache[cache_key]
38
+ # Check if file has been modified since caching
39
+ try:
40
+ current_mtime = os.path.getmtime(test_file)
41
+ # If mtime hasn't changed, use cached result
42
+ if abs(current_mtime - cached_mtime) < 1e-6:
43
+ return cached_deps
44
+ except OSError:
45
+ # If we can't stat the file, just use cached result
46
+ return cached_deps
47
+
48
+ dependencies: set[Path] = set()
49
+ # ADR 0006: 回退是调用方策略 —— 子系统策略helper 已显式兜底 CWD
50
+ project_root = project_root_for()
51
+ src_dir = Path(project_root) / "src"
52
+ test_dir = Path(project_root) / "tests"
53
+
54
+ # Analyze the test file itself
55
+ self._analyze_file_dependencies(test_file, dependencies, src_dir, test_dir)
56
+
57
+ # Cache the result with current mtime
58
+ try:
59
+ current_mtime = os.path.getmtime(test_file)
60
+ except OSError:
61
+ current_mtime = 0.0
62
+ self._dependency_cache[cache_key] = (list(dependencies), current_mtime)
63
+
64
+ # Simple LRU-style eviction: if cache exceeds 1000 entries, clear half
65
+ if len(self._dependency_cache) > 1000:
66
+ # Remove half the entries to keep cache size manageable
67
+ keys = list(self._dependency_cache.keys())
68
+ half = len(keys) // 2
69
+ for key in keys[:half]:
70
+ del self._dependency_cache[key]
71
+
72
+ return list(dependencies)
73
+
74
+ def _analyze_file_dependencies(
75
+ self, file_path: Path, dependencies: set[Path], src_dir: Path, test_dir: Path
76
+ ) -> None:
77
+ """Recursively analyze dependencies for a file."""
78
+ try:
79
+ # Read and parse file
80
+ content = file_path.read_text(encoding="utf-8")
81
+ tree = ast.parse(content)
82
+
83
+ # Analyze imports
84
+ for node in ast.walk(tree):
85
+ if isinstance(node, ast.Import):
86
+ self._analyze_import_node(node, dependencies, src_dir, test_dir)
87
+ elif isinstance(node, ast.ImportFrom):
88
+ self._analyze_import_from_node(
89
+ node, file_path, dependencies, src_dir, test_dir
90
+ )
91
+
92
+ except (OSError, SyntaxError, TypeError):
93
+ pass
94
+
95
+ def _analyze_import_node(
96
+ self, node: ast.Import, dependencies: set[Path], src_dir: Path, test_dir: Path
97
+ ) -> None:
98
+ """Analyze an Import node and add dependencies."""
99
+ for alias in node.names:
100
+ module_name = alias.name
101
+ # First try to find in src directory
102
+ dep_file = self._find_module_file(module_name, src_dir)
103
+ # If not found, try in test directory
104
+ if not dep_file:
105
+ dep_file = self._find_module_file(module_name, test_dir)
106
+ if dep_file and dep_file not in dependencies:
107
+ dependencies.add(dep_file)
108
+ # Recursively analyze this dependency
109
+ self._analyze_file_dependencies(
110
+ dep_file, dependencies, src_dir, test_dir
111
+ )
112
+
113
+ def _analyze_import_from_node(
114
+ self,
115
+ node: ast.ImportFrom,
116
+ file_path: Path,
117
+ dependencies: set[Path],
118
+ src_dir: Path,
119
+ test_dir: Path,
120
+ ) -> None:
121
+ """Analyze an ImportFrom node and add dependencies."""
122
+ if node.module:
123
+ module_name = node.module
124
+ # First try to find in src directory
125
+ dep_file = self._find_module_file(module_name, src_dir)
126
+ # If not found, try in test directory
127
+ if not dep_file:
128
+ dep_file = self._find_module_file(module_name, test_dir)
129
+ if dep_file and dep_file not in dependencies:
130
+ dependencies.add(dep_file)
131
+ # Recursively analyze this dependency
132
+ self._analyze_file_dependencies(
133
+ dep_file, dependencies, src_dir, test_dir
134
+ )
135
+ elif node.level > 0:
136
+ # Handle relative imports
137
+ relative_path = Path(file_path.parent)
138
+ for _ in range(node.level - 1):
139
+ relative_path = relative_path.parent
140
+ for alias in node.names:
141
+ # Try to find the relative module
142
+ relative_module_path = relative_path / f"{alias.name}.py"
143
+ if relative_module_path.exists():
144
+ if relative_module_path not in dependencies:
145
+ dependencies.add(relative_module_path)
146
+ self._analyze_file_dependencies(
147
+ relative_module_path, dependencies, src_dir, test_dir
148
+ )
149
+ # Try to find __init__.py in the relative directory
150
+ relative_dir_path = relative_path / alias.name
151
+ init_file = relative_dir_path / "__init__.py"
152
+ if init_file.exists():
153
+ if init_file not in dependencies:
154
+ dependencies.add(init_file)
155
+ self._analyze_file_dependencies(
156
+ init_file, dependencies, src_dir, test_dir
157
+ )
158
+
159
+ def _find_module_file(self, module_name: str, src_dir: Path) -> Path | None:
160
+ """Find module file from module name."""
161
+ # Create cache key
162
+ cache_key = (module_name, str(src_dir))
163
+
164
+ # Check cache first
165
+ if cache_key in self._module_file_cache:
166
+ return self._module_file_cache[cache_key]
167
+
168
+ # Convert module name to path
169
+ module_path = Path(module_name.replace(".", "/"))
170
+
171
+ # Look for __init__.py in module directory
172
+ init_file = src_dir / module_path / "__init__.py"
173
+ if init_file.exists():
174
+ self._module_file_cache[cache_key] = init_file
175
+ return init_file
176
+
177
+ # Look for .py file with module name
178
+ py_file = src_dir / f"{module_path}.py"
179
+ if py_file.exists():
180
+ self._module_file_cache[cache_key] = py_file
181
+ return py_file
182
+
183
+ # Try looking in parent directories for the module
184
+ current_dir = src_dir
185
+ while current_dir.parent != current_dir:
186
+ # Check if module exists in current directory
187
+ test_init_file = current_dir / module_path / "__init__.py"
188
+ if test_init_file.exists():
189
+ self._module_file_cache[cache_key] = test_init_file
190
+ return test_init_file
191
+ test_py_file = current_dir / f"{module_path}.py"
192
+ if test_py_file.exists():
193
+ self._module_file_cache[cache_key] = test_py_file
194
+ return test_py_file
195
+ current_dir = current_dir.parent
196
+
197
+ self._module_file_cache[cache_key] = None
198
+ return None
@@ -0,0 +1,278 @@
1
+ """Batch processor for step-by-step test execution."""
2
+
3
+ import concurrent.futures
4
+ import os
5
+ import sys
6
+ from collections.abc import Callable
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from pathlib import Path
9
+
10
+ from tqdm import tqdm
11
+
12
+ from ...utils.output_context import is_concise_mode, should_show_progress_bars
13
+ from .cache_manager import CacheManager
14
+
15
+
16
+ class BatchProcessor:
17
+ """Processes test files in batches for step-by-step execution."""
18
+
19
+ def __init__(self, cache_manager: CacheManager):
20
+ """Initialize with cache manager dependency."""
21
+ self.cache_manager = cache_manager
22
+
23
+ def process_specified_files(
24
+ self,
25
+ quiet: bool,
26
+ verbose: bool,
27
+ test_files: list[Path],
28
+ python_executable: str,
29
+ project_root: str,
30
+ env: dict[str, str],
31
+ debug_mode: bool,
32
+ no_cache: bool,
33
+ execute_single: Callable[[Path], tuple[bool, str]],
34
+ ) -> None:
35
+ """Process specified test files in step-by-step mode, stopping at first error."""
36
+ show_progress = should_show_progress_bars(
37
+ override_quiet=quiet, override_verbose=verbose
38
+ )
39
+ concise = is_concise_mode(override_quiet=quiet, override_verbose=verbose)
40
+ self.cache_manager.start_run()
41
+
42
+ if not test_files:
43
+ print("No test files found.")
44
+ return
45
+
46
+ # Calculate processing parameters
47
+ cpu_count = os.cpu_count() or 4
48
+ max_workers = min(16, cpu_count * 2)
49
+ batch_size = min(50, max_workers * 5)
50
+
51
+ error_occurred, error_message = self._process_batches(
52
+ test_files,
53
+ max_workers,
54
+ batch_size,
55
+ show_progress,
56
+ concise,
57
+ execute_single,
58
+ )
59
+
60
+ if error_occurred:
61
+ if show_progress:
62
+ print("\033[K", end="\r")
63
+ print(f"{error_message}")
64
+ print("Stopping at first error as requested.")
65
+ sys.exit(1)
66
+
67
+ print("All tests passed successfully!")
68
+ self.cache_manager.report_summary(test_files)
69
+
70
+ def process_all_files(
71
+ self,
72
+ quiet: bool,
73
+ verbose: bool,
74
+ test_files: list[Path],
75
+ python_executable: str,
76
+ project_root: str,
77
+ env: dict[str, str],
78
+ debug_mode: bool,
79
+ no_cache: bool,
80
+ execute_single: Callable[[Path], tuple[bool, str]],
81
+ ) -> None:
82
+ """Process all test files in step-by-step mode."""
83
+ show_progress = should_show_progress_bars(
84
+ override_quiet=quiet, override_verbose=verbose
85
+ )
86
+ concise = is_concise_mode(override_quiet=quiet, override_verbose=verbose)
87
+ self.cache_manager.start_run()
88
+
89
+ # Calculate processing parameters
90
+ cpu_count = os.cpu_count() or 4
91
+ max_workers = min(16, cpu_count * 2)
92
+ batch_size = min(50, max_workers * 5)
93
+
94
+ error_occurred, error_message = self._process_batches(
95
+ test_files,
96
+ max_workers,
97
+ batch_size,
98
+ show_progress,
99
+ concise,
100
+ execute_single,
101
+ )
102
+
103
+ if error_occurred:
104
+ if show_progress:
105
+ print("\033[K", end="\r")
106
+ print(f"{error_message}")
107
+ print("Stopping at first error as requested.")
108
+ sys.exit(1)
109
+
110
+ print("All tests passed successfully!")
111
+ self.cache_manager.report_summary(test_files)
112
+
113
+ def _process_batches(
114
+ self,
115
+ test_files: list[Path],
116
+ max_workers: int,
117
+ batch_size: int,
118
+ show_progress: bool,
119
+ concise: bool,
120
+ execute_single: Callable[[Path], tuple[bool, str]],
121
+ ) -> tuple[bool, str]:
122
+ """Process all test batches and return error status."""
123
+ error_occurred = False
124
+ error_message = ""
125
+
126
+ if show_progress:
127
+ with tqdm(
128
+ total=len(test_files),
129
+ unit="file",
130
+ dynamic_ncols=True,
131
+ bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
132
+ ) as pbar:
133
+ error_occurred, error_message = self._process_batches_with_progress(
134
+ test_files,
135
+ max_workers,
136
+ batch_size,
137
+ pbar,
138
+ concise,
139
+ execute_single,
140
+ )
141
+
142
+ else:
143
+ error_occurred, error_message, _ = self._process_batches_without_progress(
144
+ test_files,
145
+ max_workers,
146
+ batch_size,
147
+ concise,
148
+ execute_single,
149
+ )
150
+
151
+ return error_occurred, error_message
152
+
153
+ def _process_batches_with_progress(
154
+ self,
155
+ test_files: list[Path],
156
+ max_workers: int,
157
+ batch_size: int,
158
+ pbar: tqdm,
159
+ _concise_mode: bool,
160
+ execute_single: Callable[[Path], tuple[bool, str]],
161
+ ) -> tuple[bool, str]:
162
+ """Process batches with progress bar."""
163
+ error_occurred = False
164
+ error_message = ""
165
+
166
+ for i in range(0, len(test_files), batch_size):
167
+ batch_files = test_files[i : i + batch_size]
168
+ error_occurred, error_message = self._process_single_batch(
169
+ batch_files,
170
+ max_workers,
171
+ pbar,
172
+ _concise_mode,
173
+ execute_single,
174
+ )
175
+ if error_occurred:
176
+ break
177
+
178
+ return error_occurred, error_message
179
+
180
+ def _process_batches_without_progress(
181
+ self,
182
+ test_files: list[Path],
183
+ max_workers: int,
184
+ batch_size: int,
185
+ concise: bool,
186
+ execute_single: Callable[[Path], tuple[bool, str]],
187
+ ) -> tuple[bool, str, int]:
188
+ """Process batches without progress bar."""
189
+ error_occurred = False
190
+ error_message = ""
191
+ processed = 0
192
+
193
+ for i in range(0, len(test_files), batch_size):
194
+ batch_files = test_files[i : i + batch_size]
195
+ error_occurred, error_message = self._process_single_batch(
196
+ batch_files,
197
+ max_workers,
198
+ None,
199
+ concise,
200
+ execute_single,
201
+ )
202
+ processed += len(batch_files)
203
+ if not concise:
204
+ print(f"[{processed}/{len(test_files)}]", end=" ", flush=True)
205
+ if error_occurred:
206
+ break
207
+
208
+ return error_occurred, error_message, processed
209
+
210
+ def _process_single_batch(
211
+ self,
212
+ batch_files: list[Path],
213
+ max_workers: int,
214
+ pbar: tqdm | None,
215
+ _concise_mode: bool,
216
+ execute_single: Callable[[Path], tuple[bool, str]],
217
+ ) -> tuple[bool, str]:
218
+ """Process a single batch of test files."""
219
+ error_occurred = False
220
+ error_message = ""
221
+
222
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
223
+ future_to_file = {
224
+ executor.submit(execute_single, test_file): test_file
225
+ for test_file in batch_files
226
+ }
227
+
228
+ for future in concurrent.futures.as_completed(future_to_file):
229
+ test_file = future_to_file[future]
230
+ test_success, error_msg = self._process_future_result(
231
+ future, test_file, pbar
232
+ )
233
+ if not test_success:
234
+ error_occurred = True
235
+ error_message = f"Test failed for file: {test_file}\n{error_msg}"
236
+ for f in future_to_file:
237
+ if not f.done():
238
+ f.cancel()
239
+ break
240
+
241
+ return error_occurred, error_message
242
+
243
+ def _process_future_result(
244
+ self,
245
+ future: concurrent.futures.Future,
246
+ test_file: Path,
247
+ pbar: tqdm | None,
248
+ ) -> tuple[bool, str]:
249
+ """Process the result of a future."""
250
+ try:
251
+ test_success, error_msg = future.result()
252
+ if pbar:
253
+ truncated_name = self._truncate_filename(test_file.name)
254
+ self._update_test_progress(pbar, truncated_name, test_file)
255
+ return test_success, error_msg
256
+ except Exception as e:
257
+ error_message = f"Error running test for file {test_file}: {e}"
258
+ return False, error_message
259
+
260
+ def _truncate_filename(self, filename: str) -> str:
261
+ """Truncate filename to fit in progress bar."""
262
+ max_len = 28
263
+ if len(filename) > max_len:
264
+ return filename[: max_len - 3] + "..."
265
+ return filename
266
+
267
+ def _update_test_progress(
268
+ self,
269
+ pbar: tqdm,
270
+ truncated_name: str,
271
+ test_file: Path,
272
+ ) -> None:
273
+ """Update progress bar for test processing."""
274
+ if test_file in self.cache_manager.cache_hit_files:
275
+ pbar.set_description(f"Testing {truncated_name:<28} [cached]")
276
+ else:
277
+ pbar.set_description(f"Testing {truncated_name:<28}")
278
+ pbar.update(1)