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,36 @@
1
+ """Test filter for managing which test files to skip."""
2
+
3
+ from typing import cast
4
+
5
+
6
+ class TestFilter:
7
+ """Filter for test files to skip during test runs."""
8
+
9
+ def __init__(self):
10
+ """Initialize the test filter."""
11
+ self._excludes: list[str] = self._load_excludes()
12
+
13
+ def _load_excludes(self) -> list[str]:
14
+ """Load the list of test files to exclude from configuration."""
15
+ from ...utils.config import get_config
16
+
17
+ excludes = get_config("test_excludes", [])
18
+ return cast("list[str]", excludes)
19
+
20
+ def _get_default_excludes(self) -> list[str]:
21
+ """Get the default list of test files to exclude."""
22
+ return []
23
+
24
+ def get_excludes(self) -> list[str]:
25
+ """Get the list of test files to exclude."""
26
+ return self._excludes
27
+
28
+ def add_exclude(self, test_file: str) -> None:
29
+ """Add a test file to the exclusion list."""
30
+ if test_file not in self._excludes:
31
+ self._excludes.append(test_file)
32
+
33
+ def remove_exclude(self, test_file: str) -> None:
34
+ """Remove a test file from the exclusion list."""
35
+ if test_file in self._excludes:
36
+ self._excludes.remove(test_file)
wl_cli/utils/uv_run.py ADDED
@@ -0,0 +1,79 @@
1
+ """The ``uv run`` launcher fallback, shared by its callers (#165).
2
+
3
+ ``uv run <tool>`` has one failure mode that says nothing about ``<tool>``:
4
+ ``uv`` itself could not be started, so the child never ran. Callers recover by
5
+ re-running the child directly. The rule lives here -- in ``utils`` -- so that
6
+ ``utils/pre_commit.py`` can share it with the formatters without importing a
7
+ formatter back out of ``commands``.
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ from .subprocess_utils import SubprocessExecutor, SubprocessResult
13
+
14
+
15
+ def uv_unavailable(result: SubprocessResult) -> bool:
16
+ """Whether the ``uv`` launcher never got as far as running its child.
17
+
18
+ Deliberately **not** a substring sniff of ``result.stderr``. The old test
19
+ read ``"not found" in stderr and "uv" in stderr``, which fires on any *child*
20
+ that quotes the command line back -- ``uv run: ruff: not found`` is uv
21
+ working perfectly and ruff missing, yet it sent the tool re-running outside
22
+ its environment. What actually means "the launcher was not there" is
23
+ :attr:`SubprocessResult.launch_error`, the field set only when the
24
+ executable could not be started at all (#100). ``127`` is kept as a
25
+ belt-and-braces spelling of a shell's "command not found", which an executor
26
+ without ``shell=True`` never produces itself.
27
+
28
+ The same discriminator, under the ``Ruff`` name, guards the dead-code
29
+ detector and fixer (#150/#153) -- see ``DeadCodeFixer._ruff_unavailable``
30
+ for why those copies stay spelled out (#155 M2).
31
+
32
+ Args:
33
+ result: The finished subprocess result.
34
+
35
+ Returns:
36
+ True when the process never ran.
37
+ """
38
+ if result.launch_error is not None:
39
+ return True
40
+ return result.returncode == 127
41
+
42
+
43
+ def run_uv_command(
44
+ executor: SubprocessExecutor,
45
+ command: list[str],
46
+ *,
47
+ env: dict[str, str] | None = None,
48
+ cwd: Path | None = None,
49
+ quiet: bool = False,
50
+ ) -> SubprocessResult:
51
+ """Run ``command``, retrying it without its ``uv run`` prefix if uv is absent.
52
+
53
+ The executor is passed in rather than built here: each caller keeps its own
54
+ patch point and its own environment preparation (the formatters prepare one
55
+ per command; the pre-commit run prepares one per run).
56
+
57
+ Args:
58
+ executor: The executor that runs both attempts.
59
+ command: The command, optionally led by ``uv run``.
60
+ env: Environment for the child.
61
+ cwd: Working directory for the child.
62
+ quiet: Whether to capture the child's output.
63
+
64
+ Returns:
65
+ The result of the direct re-run when the launcher was unavailable and
66
+ the re-run did not itself raise; otherwise the original result.
67
+ """
68
+ result = executor.run(command, env=env, cwd=cwd, quiet=quiet)
69
+ if (
70
+ len(command) >= 2
71
+ and command[0] == "uv"
72
+ and command[1] == "run"
73
+ and uv_unavailable(result)
74
+ ):
75
+ try:
76
+ return executor.run(command[2:], env=env, cwd=cwd, quiet=quiet)
77
+ except Exception:
78
+ pass
79
+ return result
@@ -0,0 +1,117 @@
1
+ """UV Tool environment utilities for WL Commands."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def is_running_in_uv_tool() -> bool:
8
+ """Check if we are running in a UV Tool environment.
9
+
10
+ Returns:
11
+ bool: True if running in UV Tool environment, False otherwise.
12
+ """
13
+ return "UV_TOOL_DIR" in os.environ or "UV_PYTHON" in os.environ
14
+
15
+
16
+ def get_uv_tool_python_path() -> str | None:
17
+ """Get the Python executable path from UV Tool environment.
18
+
19
+ Returns:
20
+ Optional[str]: Python executable path if in UV Tool environment, None otherwise.
21
+ """
22
+ if "UV_PYTHON" in os.environ:
23
+ return os.environ["UV_PYTHON"]
24
+
25
+ if "UV_TOOL_DIR" in os.environ:
26
+ # Try to find python executable in UV tool directory
27
+ uv_tool_dir = Path(os.environ["UV_TOOL_DIR"])
28
+
29
+ # First, try to get Python executable from sys.executable
30
+ import sys
31
+
32
+ if sys.executable:
33
+ return sys.executable
34
+
35
+ # If sys.executable is not available, try common locations
36
+ # Try parent directory of UV_TOOL_DIR
37
+ python_executable = uv_tool_dir.parent.parent / "python"
38
+ if python_executable.exists():
39
+ return str(python_executable)
40
+
41
+ # Try Windows-specific path
42
+ python_executable = uv_tool_dir.parent.parent / "python.exe"
43
+ if python_executable.exists():
44
+ return str(python_executable)
45
+
46
+ # Try to find python in PATH
47
+ import shutil
48
+
49
+ python_path = shutil.which("python")
50
+ if python_path:
51
+ return python_path
52
+
53
+ return None
54
+
55
+
56
+ def get_uv_tool_bin_path() -> Path | None:
57
+ """Get the bin directory path from UV Tool environment.
58
+
59
+ Returns:
60
+ Optional[Path]: Bin directory path if in UV Tool environment, None otherwise.
61
+ """
62
+ if "UV_TOOL_DIR" in os.environ:
63
+ uv_tool_dir = Path(os.environ["UV_TOOL_DIR"])
64
+ return uv_tool_dir
65
+
66
+ return None
67
+
68
+
69
+ def get_uv_tool_env() -> dict[str, str]:
70
+ """Get environment variables for UV Tool environment.
71
+
72
+ Returns:
73
+ dict[str, str]: Environment variables for UV Tool environment.
74
+ """
75
+ env = os.environ.copy()
76
+
77
+ # Add UV Tool bin directory to PATH if not already present
78
+ uv_tool_bin = get_uv_tool_bin_path()
79
+ if uv_tool_bin:
80
+ path = env.get("PATH", "")
81
+ if str(uv_tool_bin) not in path:
82
+ env["PATH"] = f"{uv_tool_bin}{os.pathsep}{path}"
83
+
84
+ return env
85
+
86
+
87
+ def should_use_direct_execution(command: list[str]) -> bool:
88
+ """Check if a command should be executed directly in UV Tool environment.
89
+
90
+ Args:
91
+ command: Command to check.
92
+
93
+ Returns:
94
+ bool: True if command should be executed directly, False otherwise.
95
+ """
96
+ if not is_running_in_uv_tool():
97
+ return False
98
+
99
+ # Check if it's an 'uv run' command
100
+ if len(command) >= 2 and command[0] == "uv" and command[1] == "run":
101
+ return True
102
+
103
+ return False
104
+
105
+
106
+ def get_direct_command(command: list[str]) -> list[str]:
107
+ """Get the direct command by removing 'uv run' prefix.
108
+
109
+ Args:
110
+ command: Original command with 'uv run' prefix.
111
+
112
+ Returns:
113
+ list[str]: Command without 'uv run' prefix.
114
+ """
115
+ if len(command) >= 2 and command[0] == "uv" and command[1] == "run":
116
+ return command[2:]
117
+ return command
@@ -0,0 +1,71 @@
1
+ """Shared parsing of ``uv tree`` output.
2
+
3
+ ``uv tree`` prints one root row per workspace member, normally as
4
+ ``<name> v<version>``, followed by indented dependency rows drawn with box
5
+ characters. Two detectors used to parse that output by hand with slightly
6
+ different heuristics, so a root package whose name begins with ``v`` --
7
+ ``vite-app``, ``vue-lib``, ``virtualenv-*`` -- got two different wrong answers
8
+ (#178). This module owns the one parser; both entry points delegate here.
9
+ """
10
+
11
+ #: Leading characters of a dependency row in ``uv tree`` output.
12
+ _TREE_PREFIXES: tuple[str, ...] = ("├", "└", "│", "─", "┌", "┐", "┬", "┴", "┼")
13
+
14
+
15
+ def is_valid_version(version: str) -> bool:
16
+ """检查版本字符串是否有效。"""
17
+ if not version:
18
+ return False
19
+ return all(c.isalnum() or c in ".-+" for c in version)
20
+
21
+
22
+ def _is_version_only_line(line: str) -> bool:
23
+ """判断整行是否只是版本号(如 ``v1.2.3``)。
24
+
25
+ ``uv tree`` 不会把裸版本号输出为根行,但旧解析器会把它们排除,这里保留该
26
+ 约定。判定要求 ``v`` 前缀且后接数字,因此 ``vite-app`` 这类真实包名仍然
27
+ 会被当作根包。
28
+ """
29
+ if not line.startswith(("v", "V")):
30
+ return False
31
+ candidate = line[1:]
32
+ return candidate[:1].isdigit() and is_valid_version(candidate)
33
+
34
+
35
+ def _is_root_row(line: str) -> bool:
36
+ """判断一行是否可能是根包行(已去掉行尾空白)。"""
37
+ if not line or line[0].isspace():
38
+ return False
39
+ if line.startswith(_TREE_PREFIXES):
40
+ return False
41
+ return not (line.startswith("Resolved") or line.startswith("(*)"))
42
+
43
+
44
+ def parse_uv_tree_roots(output: str | list[str]) -> list[str]:
45
+ """从 ``uv tree`` 输出中解析根包名。
46
+
47
+ Args:
48
+ output: ``uv tree`` 的原始输出,或其按行拆分后的列表。
49
+
50
+ Returns:
51
+ 按输出顺序排列的根包名。``<name> v<version>`` 行贡献 ``name``;``" v"``
52
+ 之后的部分必须形如版本(数字开头且通过 :func:`is_valid_version`),
53
+ 因此名字里含 ``" v"`` 的包不会被静默截断。
54
+ """
55
+ lines = output.splitlines() if isinstance(output, str) else output
56
+ roots: list[str] = []
57
+ for raw in lines:
58
+ stripped = raw.rstrip()
59
+ if not _is_root_row(stripped):
60
+ continue
61
+ line = stripped.strip()
62
+ parts = line.rsplit(" v", 1)
63
+ if len(parts) == 2:
64
+ name, version = parts[0].strip(), parts[1].strip()
65
+ if name and version[:1].isdigit() and is_valid_version(version):
66
+ roots.append(name)
67
+ continue
68
+ if _is_version_only_line(line):
69
+ continue
70
+ roots.append(line)
71
+ return roots
@@ -0,0 +1,174 @@
1
+ """UV workspace detection utilities for WL Commands."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from .logging import log_info
7
+ from .subprocess_utils import SubprocessExecutor
8
+ from .uv_tree import is_valid_version, parse_uv_tree_roots
9
+ from .workspace_detector.detection_strategies import UV_PROBE_TIMEOUT_SECONDS
10
+
11
+
12
+ class UVWorkspaceDetector:
13
+ """A utility class for detecting UV workspaces."""
14
+
15
+ def __init__(self):
16
+ self._subprocess_executor = SubprocessExecutor()
17
+ self._workspace_cache: dict[str, bool] = {}
18
+
19
+ def is_workspace(self, project_root: Path | None = None) -> bool:
20
+ """
21
+ Detect if we are in a uv workspace.
22
+
23
+ Args:
24
+ project_root: The project root directory. If None, uses current directory.
25
+
26
+ Returns:
27
+ True if in a workspace, False otherwise.
28
+ """
29
+ try:
30
+ if project_root is None:
31
+ project_root = Path.cwd()
32
+
33
+ project_root = project_root.resolve()
34
+ root_str = str(project_root)
35
+
36
+ # Check cache first
37
+ if root_str in self._workspace_cache:
38
+ return self._workspace_cache[root_str]
39
+
40
+ # Method 1: Check for uv.lock file which indicates a workspace
41
+ uv_lock_path = project_root / "uv.lock"
42
+ if uv_lock_path.exists() and uv_lock_path.is_file():
43
+ log_info("Debug: uv.lock file found, workspace detected")
44
+ self._workspace_cache[root_str] = True
45
+ return True
46
+
47
+ # Method 2: Use uv init command to check if we're already in a workspace
48
+ try:
49
+ # A wedged uv must read as "not detected", not as an
50
+ # infinite wait -- same bound as WorkspaceDetector (#184).
51
+ result = self._subprocess_executor.run(
52
+ ["uv", "init", "--dry-run"],
53
+ cwd=project_root,
54
+ quiet=True,
55
+ timeout=UV_PROBE_TIMEOUT_SECONDS,
56
+ )
57
+
58
+ if (
59
+ result.success
60
+ and "is already a member of workspace" in result.stderr
61
+ ):
62
+ log_info("Debug: 'is already a member of workspace' detected")
63
+ self._workspace_cache[root_str] = True
64
+ return True
65
+ except (subprocess.SubprocessError, OSError, ValueError):
66
+ # Fall back to other methods if uv init fails
67
+ pass
68
+
69
+ # Method 3: Use uv tree command to check for multiple root packages
70
+ is_workspace_result = self._detect_workspace_via_tree(project_root)
71
+ self._workspace_cache[root_str] = is_workspace_result
72
+ return is_workspace_result
73
+
74
+ except (OSError, ValueError) as e:
75
+ log_info(f"Debug: Workspace detection error: {e}")
76
+ # Cache the result even for errors to avoid repeated failures
77
+ # Use a default key if root_str is not defined
78
+ cache_key = root_str if "root_str" in locals() else "error_default"
79
+ self._workspace_cache[cache_key] = False
80
+ return False
81
+
82
+ def _detect_workspace_via_tree(self, project_root: Path) -> bool:
83
+ """
84
+ Detect workspace using uv tree command.
85
+
86
+ Args:
87
+ project_root: The project root directory.
88
+
89
+ Returns:
90
+ True if workspace detected, False otherwise.
91
+ """
92
+ try:
93
+ result = self._subprocess_executor.run(
94
+ ["uv", "tree"],
95
+ cwd=project_root,
96
+ quiet=True,
97
+ timeout=UV_PROBE_TIMEOUT_SECONDS,
98
+ )
99
+
100
+ if not result.success:
101
+ log_info(f"Debug: uv tree command failed with code {result.returncode}")
102
+ return False
103
+
104
+ # Check for a workspace by counting root packages
105
+ # In a workspace, there would be multiple top-level packages
106
+ lines = result.stdout.split("\n") if result.stdout else []
107
+ root_packages = self._parse_root_packages(lines)
108
+
109
+ log_info(f"Debug: uv tree root packages: {root_packages}")
110
+ log_info(f"Debug: root packages count: {len(root_packages)}")
111
+
112
+ # There should be more than one root package in a workspace
113
+ if len(root_packages) > 1:
114
+ log_info("Debug: Multiple root packages found, workspace detected")
115
+ return True
116
+
117
+ except (subprocess.SubprocessError, OSError, ValueError) as e:
118
+ log_info(f"Debug: Workspace detection via tree error: {e}")
119
+ pass # Ignore errors in workspace detection
120
+
121
+ log_info("Debug: No workspace detected via tree")
122
+ return False
123
+
124
+ def _parse_root_packages(self, lines: list[str]) -> list[str]:
125
+ """
126
+ Parse root packages from uv tree output.
127
+
128
+ Delegates to the shared parser so both detectors agree (#178).
129
+
130
+ Args:
131
+ lines: Lines from uv tree output.
132
+
133
+ Returns:
134
+ List of root package names.
135
+ """
136
+ return parse_uv_tree_roots(lines)
137
+
138
+ def _is_valid_version(self, version: str) -> bool:
139
+ """
140
+ Check if a version string is valid.
141
+
142
+ Args:
143
+ version: Version string to check.
144
+
145
+ Returns:
146
+ True if valid, False otherwise.
147
+ """
148
+ return is_valid_version(version)
149
+
150
+ def clear_cache(self) -> None:
151
+ """Clear the workspace detection cache."""
152
+ self._workspace_cache.clear()
153
+
154
+
155
+ # Global instance
156
+ _uv_workspace_detector = UVWorkspaceDetector()
157
+
158
+
159
+ def is_uv_workspace(project_root: Path | None = None) -> bool:
160
+ """
161
+ Detect if we are in a uv workspace.
162
+
163
+ Args:
164
+ project_root: The project root directory. If None, uses current directory.
165
+
166
+ Returns:
167
+ True if in a workspace, False otherwise.
168
+ """
169
+ return _uv_workspace_detector.is_workspace(project_root)
170
+
171
+
172
+ def clear_uv_workspace_cache() -> None:
173
+ """Clear the UV workspace detection cache."""
174
+ _uv_workspace_detector.clear_cache()
@@ -0,0 +1,15 @@
1
+ """Version management service."""
2
+
3
+ from .pypi_version_checker import PyPIVersionChecker
4
+ from .version_comparator import VersionComparator
5
+ from .version_detectors import VersionDetector
6
+ from .version_service import VersionService
7
+ from .version_updater import VersionUpdater
8
+
9
+ __all__ = [
10
+ "VersionService",
11
+ "VersionComparator",
12
+ "VersionDetector",
13
+ "VersionUpdater",
14
+ "PyPIVersionChecker",
15
+ ]
@@ -0,0 +1,95 @@
1
+ """
2
+ PyPI version checking utilities for publish command.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ from typing import cast
8
+ from urllib import request
9
+ from urllib.error import URLError
10
+
11
+ from ...exceptions import CommandError
12
+ from ..logging import log_info_pair
13
+
14
+
15
+ class PyPIVersionChecker:
16
+ """Check versions against PyPI servers."""
17
+
18
+ def _get_pypi_version(self) -> str | None:
19
+ """Get the latest version from PyPI."""
20
+ package_name = "py_wlcli"
21
+ try:
22
+ url = f"https://pypi.org/pypi/{package_name}/json"
23
+ with request.urlopen(url) as response:
24
+ data = json.loads(response.read())
25
+ return cast("str | None", data["info"]["version"])
26
+ except (OSError, json.JSONDecodeError, KeyError, URLError):
27
+ # If we can't get the PyPI version, return None
28
+ return None
29
+
30
+ async def _get_pypi_version_async(self) -> str | None:
31
+ """Async get the latest version from PyPI."""
32
+ return await asyncio.to_thread(self._get_pypi_version)
33
+
34
+ def check_version_with_pypi(
35
+ self, repository: str, current_version: str, comparator
36
+ ) -> None:
37
+ """Check the current version against PyPI to ensure proper versioning."""
38
+ package_name = "py_wlcli" # This should ideally be read from project config
39
+
40
+ try:
41
+ # Determine the repository URL
42
+ if repository == "pypi":
43
+ url = f"https://pypi.org/pypi/{package_name}/json"
44
+ else:
45
+ # For other repositories, we might need to get the URL from twine config
46
+ # For now, we'll assume it's TestPyPI
47
+ url = f"https://test.pypi.org/pypi/{package_name}/json"
48
+
49
+ log_info_pair(
50
+ f"Checking version on {repository} server...",
51
+ f"正在检查 {repository} 服务器上的版本...",
52
+ )
53
+
54
+ # Make request to PyPI API
55
+ with request.urlopen(url) as response:
56
+ data = json.loads(response.read())
57
+
58
+ # Get the latest version from PyPI
59
+ pypi_version = data["info"]["version"]
60
+ log_info_pair(
61
+ f"Latest version on {repository}: {pypi_version}",
62
+ f"{repository} 上的最新版本: {pypi_version}",
63
+ )
64
+
65
+ # Compare versions
66
+ if not comparator.is_version_increment_valid(pypi_version, current_version):
67
+ raise CommandError(
68
+ f"Version check failed: Local version {current_version} "
69
+ f"is not a valid increment from PyPI version {pypi_version}. "
70
+ f"Version must be incremented and not skip numbers."
71
+ )
72
+
73
+ log_info_pair(
74
+ "✓ Version check passed",
75
+ "✓ 版本检查通过",
76
+ )
77
+
78
+ except URLError as e:
79
+ log_info_pair(
80
+ f"Warning: Could not check version on PyPI: {e}",
81
+ f"警告: 无法检查 PyPI 上的版本: {e}",
82
+ )
83
+ except (json.JSONDecodeError, KeyError) as e:
84
+ log_info_pair(
85
+ f"Warning: Version check failed: {e}",
86
+ f"警告: 版本检查失败: {e}",
87
+ )
88
+
89
+ async def check_version_with_pypi_async(
90
+ self, repository: str, current_version: str, comparator
91
+ ) -> None:
92
+ """Async check the current version against PyPI to ensure proper versioning."""
93
+ return await asyncio.to_thread(
94
+ self.check_version_with_pypi, repository, current_version, comparator
95
+ )