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,458 @@
1
+ """Logging utilities for WL Commands."""
2
+
3
+ import inspect
4
+ import os
5
+ import sys
6
+ import traceback
7
+ from dataclasses import dataclass
8
+ from typing import Any, Literal, cast
9
+
10
+ from .language_utils import _should_display_language
11
+ from .log_levels import LogLevel
12
+ from .structured_logging.core import StructuredLogger
13
+ from .structured_logging.env import is_performance_test as _is_performance_test
14
+ from .structured_logging.env import is_pytest
15
+
16
+ # default_logger 是**延迟**构造的。早先这里是模块级直接构造,于是 `import wl_cli`
17
+ # 这一个动作就会经由 StructuredLogger → LoggingConfig → get_config() → ConfigManager
18
+ # 去解析配置:读盘,并(在当时 _load_config 的写法下)把 .wl/config.json 写进 CWD。
19
+ # 导入一个库不该有文件系统副作用,故模块级只放一个惰性代理。
20
+ _logger_instance: StructuredLogger | None = None
21
+
22
+
23
+ def _logger() -> StructuredLogger:
24
+ """Return the module-level logger, constructing it on first use."""
25
+ global _logger_instance
26
+ if _logger_instance is None:
27
+ _logger_instance = StructuredLogger("wl", min_level=LogLevel.INFO)
28
+ _logger_instance.enable_console = True
29
+ return _logger_instance
30
+
31
+
32
+ class _LazyLogger:
33
+ """Module-level proxy that builds the real logger on first attribute use.
34
+
35
+ Keeps the `default_logger` name — including unittest.mock's ability to
36
+ patch it — without paying StructuredLogger's filesystem work at import.
37
+
38
+ Attribute writes and deletes are forwarded to the real logger, so
39
+ ``default_logger.min_level = X`` and ``mock.patch.object(default_logger,
40
+ "info")`` behave exactly as they did when the module held a concrete
41
+ logger: patch installs the mock on the real instance, and its ``delattr``
42
+ round-trip restores the bound method.
43
+ """
44
+
45
+ def __getattr__(self, item: str) -> Any:
46
+ return getattr(_logger(), item)
47
+
48
+ def __setattr__(self, key: str, value: Any) -> None:
49
+ setattr(_logger(), key, value)
50
+
51
+ def __delattr__(self, key: str) -> None:
52
+ delattr(_logger(), key)
53
+
54
+
55
+ default_logger: StructuredLogger = cast("StructuredLogger", _LazyLogger())
56
+
57
+
58
+ def _is_log_level_enabled(level: int) -> bool:
59
+ """Check if the given log level is enabled based on the current minimum level."""
60
+ try:
61
+ min_level = default_logger.min_level
62
+ if isinstance(min_level, int | float):
63
+ return min_level <= level
64
+ except (TypeError, AttributeError):
65
+ pass
66
+ return True
67
+
68
+
69
+ def _add_caller_info(kwargs: dict[str, Any], frame_offset: int = 1) -> None:
70
+ """Add caller module/function/line information to kwargs."""
71
+ try:
72
+ frame = inspect.currentframe()
73
+ for _ in range(frame_offset):
74
+ if frame:
75
+ frame = frame.f_back
76
+ if frame:
77
+ caller_info = inspect.getframeinfo(frame)
78
+ kwargs.update(
79
+ {
80
+ "module": os.path.basename(caller_info.filename).split(".")[0],
81
+ "function": caller_info.function,
82
+ "line": caller_info.lineno,
83
+ }
84
+ )
85
+ except (TypeError, AttributeError):
86
+ pass
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # The single emit funnel (#135)
91
+ #
92
+ # The six verbs below are thin one-line aliases over `_emit`; the four guards
93
+ # (level / lang / perf / caller) and the caller-frame choice are assembled in
94
+ # `_emit` alone, driven by the per-verb policy table `_POLICIES`.
95
+ #
96
+ # The guards were never uniform across the verbs, and that drift is *behaviour*
97
+ # kept verbatim here -- do not "fix" it in this change (explicitly out of scope,
98
+ # tracked separately):
99
+ # * perf guard: only info / error / critical bail out under a performance test;
100
+ # debug / warning / exception log straight through.
101
+ # * caller info: debug / exception always attach it, error / critical attach it
102
+ # only outside pytest, info / warning never include it.
103
+ # ---------------------------------------------------------------------------
104
+
105
+ _CallerMode = Literal["never", "always", "outside_pytest"]
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class _EmitPolicy:
110
+ """Guard policy for one verb, consumed by :func:`_emit`.
111
+
112
+ ``logger_method`` is the :class:`StructuredLogger` method to call; note the
113
+ exception verb logs through ``error``.
114
+ """
115
+
116
+ logger_method: str
117
+ level: int
118
+ perf_guard: bool = False
119
+ caller: _CallerMode = "never"
120
+ formats_exception: bool = False
121
+
122
+
123
+ _POLICIES: dict[str, _EmitPolicy] = {
124
+ "debug": _EmitPolicy("debug", LogLevel.DEBUG, caller="always"),
125
+ "info": _EmitPolicy("info", LogLevel.INFO, perf_guard=True),
126
+ "warning": _EmitPolicy("warning", LogLevel.WARNING),
127
+ "error": _EmitPolicy(
128
+ "error", LogLevel.ERROR, perf_guard=True, caller="outside_pytest"
129
+ ),
130
+ "critical": _EmitPolicy(
131
+ "critical", LogLevel.CRITICAL, perf_guard=True, caller="outside_pytest"
132
+ ),
133
+ "exception": _EmitPolicy(
134
+ "error", LogLevel.ERROR, caller="always", formats_exception=True
135
+ ),
136
+ }
137
+
138
+ # The funnel sits between the verb and `_add_caller_info`, so this offset is one
139
+ # deeper than the pre-#135 value of 2. The extra hop is exactly the bump, which
140
+ # keeps the resolved caller frame identical to before -- both for a direct verb
141
+ # call and for a `log(level, ...)` dispatch.
142
+ _CALLER_FRAME_OFFSET = 3
143
+
144
+
145
+ def _attach_exception(kwargs: dict[str, Any], exc_info: Any) -> None:
146
+ """Attach the active (or explicitly supplied) exception to ``kwargs``."""
147
+ if exc_info is None:
148
+ exc_type, exc_value, exc_tb = sys.exc_info()
149
+ if exc_type:
150
+ kwargs.update(
151
+ {
152
+ "error": str(exc_value),
153
+ "traceback": "".join(
154
+ traceback.format_exception(exc_type, exc_value, exc_tb)
155
+ ),
156
+ }
157
+ )
158
+ else:
159
+ kwargs["error"] = str(exc_info)
160
+
161
+
162
+ def _emit(
163
+ verb: str,
164
+ message: str,
165
+ *,
166
+ lang: str,
167
+ kwargs: dict[str, Any],
168
+ exc_info: Any = None,
169
+ ) -> None:
170
+ """The single funnel every verb logs through.
171
+
172
+ Guard order matches the pre-#135 verbs: level, then the perf gate, then the
173
+ language gate, then caller info / exception formatting.
174
+ """
175
+ policy = _POLICIES[verb]
176
+ if not _is_log_level_enabled(policy.level):
177
+ return
178
+ if policy.perf_guard and _is_performance_test():
179
+ return
180
+ if not _should_display_language(lang):
181
+ return
182
+ if policy.caller == "always" or (
183
+ policy.caller == "outside_pytest" and not is_pytest()
184
+ ):
185
+ _add_caller_info(kwargs, frame_offset=_CALLER_FRAME_OFFSET)
186
+ if policy.formats_exception:
187
+ _attach_exception(kwargs, exc_info)
188
+ getattr(default_logger, policy.logger_method)(message, lang=lang, **kwargs)
189
+
190
+
191
+ # ``lang`` is keyword-only (#167). As a second positional parameter it silently
192
+ # absorbed a Chinese message -- ``log_info(en_msg, zh_msg)`` emitted nothing
193
+ # under any language setting, the #152 shape -- so the mistake now raises
194
+ # TypeError instead. ``log_*_pair`` is the bilingual entry point and is
195
+ # unaffected.
196
+ def log_debug(message: str, *, lang: str = "en", **kwargs) -> None:
197
+ """Log a debug message (caller info attached)."""
198
+ _emit("debug", message, lang=lang, kwargs=kwargs)
199
+
200
+
201
+ def log_info(message: str, *, lang: str = "en", **kwargs) -> None:
202
+ """Log an informational message (skipped under a performance test)."""
203
+ _emit("info", message, lang=lang, kwargs=kwargs)
204
+
205
+
206
+ def log_warning(message: str, *, lang: str = "en", **kwargs) -> None:
207
+ """Log a warning message."""
208
+ _emit("warning", message, lang=lang, kwargs=kwargs)
209
+
210
+
211
+ def log_error(message: str, *, lang: str = "en", **kwargs) -> None:
212
+ """Log an error message (caller info only outside pytest)."""
213
+ _emit("error", message, lang=lang, kwargs=kwargs)
214
+
215
+
216
+ def log_critical(message: str, *, lang: str = "en", **kwargs) -> None:
217
+ """Log a critical message (caller info only outside pytest)."""
218
+ _emit("critical", message, lang=lang, kwargs=kwargs)
219
+
220
+
221
+ # ``exc_info`` is keyword-only for the same reason ``lang`` is (#167): as a
222
+ # second positional parameter it silently absorbed the Chinese half of a
223
+ # translation pair -- ``log_exception(en_msg, zh_msg)`` bound ``zh_msg`` to
224
+ # ``exc_info``, attached it as the error text, and emitted nothing under the
225
+ # Chinese language setting (#172 wave 9). Keyword-only makes that shape a
226
+ # ``TypeError`` at the call instead of a silent misbinding.
227
+ def log_exception(
228
+ message: str, *, exc_info: Any = None, lang: str = "en", **kwargs
229
+ ) -> None:
230
+ """Log an exception, attaching the active/explicit exception details.
231
+
232
+ ``message`` is positional; ``exc_info`` and ``lang`` are keyword-only, so a
233
+ second positional argument (the old ``log_exception(en, zh)`` shape) fails
234
+ at the call rather than binding the Chinese text to ``exc_info``.
235
+ """
236
+ _emit("exception", message, lang=lang, kwargs=kwargs, exc_info=exc_info)
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # The bilingual double-write entry point (#146)
241
+ #
242
+ # A *pair* is the two adjacent statements the codebase used to spell out by
243
+ # hand -- an English line and a Chinese line carrying the same message:
244
+ #
245
+ # log_info("✓ Git repository initialized")
246
+ # log_info("✓ Git仓库初始化完成", lang="zh")
247
+ #
248
+ # `log_pair` collapses them into one call whose observable output is identical:
249
+ # the two `_emit` calls below are exactly the two the hand-written form made,
250
+ # in the same order, each judged by the same level / perf / language guards.
251
+ # The language decision stays in `_should_display_language` (reached through
252
+ # `_emit`) -- this module never re-derives it.
253
+ #
254
+ # Every entry point below calls `_emit` *directly* rather than delegating to a
255
+ # shared helper, because `_emit` resolves caller info at a fixed frame depth
256
+ # (_CALLER_FRAME_OFFSET): one extra hop would move debug/error/critical caller
257
+ # info onto the wrapper. Each `_emit` call also gets its own copy of `kwargs`,
258
+ # matching the fresh dict that `**kwargs` gave each hand-written call.
259
+ # ---------------------------------------------------------------------------
260
+
261
+
262
+ def log_pair(en: str, zh: str, level: str = "info", **kwargs) -> None:
263
+ """Log the English and Chinese variants of one message (#146).
264
+
265
+ Args:
266
+ en: The English variant, emitted with ``lang="en"``.
267
+ zh: The Chinese variant, emitted with ``lang="zh"``.
268
+ level: Verb to log through -- one of "debug", "info", "warning",
269
+ "error", "critical" or "exception". Defaults to "info".
270
+ **kwargs: Extra context, applied to both variants. ``exc_info`` is
271
+ forwarded to `_emit` for the "exception" verb.
272
+ """
273
+ exc_info = kwargs.pop("exc_info", None)
274
+ _emit(level, en, lang="en", kwargs=dict(kwargs), exc_info=exc_info)
275
+ _emit(level, zh, lang="zh", kwargs=dict(kwargs), exc_info=exc_info)
276
+
277
+
278
+ def log_debug_pair(en: str, zh: str, **kwargs) -> None:
279
+ """Debug-level `log_pair` (caller info attached to both variants)."""
280
+ _emit("debug", en, lang="en", kwargs=dict(kwargs))
281
+ _emit("debug", zh, lang="zh", kwargs=dict(kwargs))
282
+
283
+
284
+ def log_info_pair(en: str, zh: str, **kwargs) -> None:
285
+ """Info-level `log_pair` (both variants skipped under a performance test)."""
286
+ _emit("info", en, lang="en", kwargs=dict(kwargs))
287
+ _emit("info", zh, lang="zh", kwargs=dict(kwargs))
288
+
289
+
290
+ def log_warning_pair(en: str, zh: str, **kwargs) -> None:
291
+ """Warning-level `log_pair`."""
292
+ _emit("warning", en, lang="en", kwargs=dict(kwargs))
293
+ _emit("warning", zh, lang="zh", kwargs=dict(kwargs))
294
+
295
+
296
+ def log_error_pair(en: str, zh: str, **kwargs) -> None:
297
+ """Error-level `log_pair` (caller info on both, only outside pytest)."""
298
+ _emit("error", en, lang="en", kwargs=dict(kwargs))
299
+ _emit("error", zh, lang="zh", kwargs=dict(kwargs))
300
+
301
+
302
+ def log(level: int, message: str, **kwargs) -> None:
303
+ """
304
+ Log a message with specified level.
305
+
306
+ Dispatch stays by verb name (each verb funnels through `_emit`); the
307
+ `mock.patch("wl_cli.utils.logging.log_*")` contract depends on it.
308
+
309
+ Args:
310
+ level: Log level as integer (use LogLevel enum).
311
+ message (str): Message to log.
312
+ **kwargs: Additional context for the log message.
313
+ """
314
+ # Backward compatibility: handle old keyword argument names
315
+ # Old signature: def log(message: int, message_str: str, **kwargs)
316
+ if "message_str" in kwargs:
317
+ message = kwargs.pop("message_str")
318
+ old_level = kwargs.pop("message", None)
319
+ if old_level is not None and isinstance(old_level, int):
320
+ level = old_level
321
+
322
+ if level == LogLevel.DEBUG:
323
+ log_debug(message, **kwargs)
324
+ elif level == LogLevel.INFO:
325
+ log_info(message, **kwargs)
326
+ elif level == LogLevel.WARNING:
327
+ log_warning(message, **kwargs)
328
+ elif level == LogLevel.ERROR:
329
+ log_error(message, **kwargs)
330
+ elif level == LogLevel.CRITICAL:
331
+ log_critical(message, **kwargs)
332
+
333
+
334
+ # Context management utilities
335
+ def push_log_context(**context) -> None:
336
+ """
337
+ Push context to the default logger's context stack.
338
+
339
+ Args:
340
+ **context: Context key-value pairs.
341
+ """
342
+ if hasattr(default_logger, "push_context"):
343
+ default_logger.push_context(**context)
344
+
345
+
346
+ def pop_log_context() -> dict[str, Any] | None:
347
+ """
348
+ Pop context from the default logger's context stack.
349
+
350
+ Returns:
351
+ The popped context or None if stack is empty.
352
+ """
353
+ if hasattr(default_logger, "pop_context"):
354
+ return cast("dict[str, Any] | None", default_logger.pop_context())
355
+ return None
356
+
357
+
358
+ def clear_log_context() -> None:
359
+ """
360
+ Clear all context from the default logger's context stack.
361
+ """
362
+ if hasattr(default_logger, "clear_context"):
363
+ default_logger.clear_context()
364
+
365
+
366
+ # Enhanced get_logger function
367
+ def get_logger(
368
+ name: str = "wl", config: dict[str, Any] | None = None
369
+ ) -> StructuredLogger:
370
+ """
371
+ Get a logger instance with the specified name and optional configuration.
372
+
373
+ Args:
374
+ name (str, optional): Logger name. Defaults to "wl".
375
+ config (Optional[Dict[str, Any]]): Optional configuration dictionary.
376
+
377
+ Returns:
378
+ StructuredLogger: Logger instance.
379
+ """
380
+ if config:
381
+ # Create logger with custom configuration
382
+ logger = StructuredLogger(name)
383
+ # Apply configuration
384
+ if "log_level" in config:
385
+ if isinstance(config["log_level"], str):
386
+ logger.min_level = getattr(
387
+ LogLevel, config["log_level"].upper(), LogLevel.INFO
388
+ )
389
+ else:
390
+ logger.min_level = config["log_level"]
391
+ if "log_file" in config:
392
+ logger.log_file = config["log_file"]
393
+ return logger
394
+ else:
395
+ return StructuredLogger(name)
396
+
397
+
398
+ # Aliases for backward compatibility
399
+ info = log_info
400
+ debug = log_debug
401
+ warning = log_warning
402
+ error = log_error
403
+ critical = log_critical
404
+ exception = log_exception
405
+
406
+
407
+ # Module-level convenience functions
408
+ def configure_logging(config: dict[str, Any]) -> None:
409
+ """
410
+ Configure the default logger with the specified configuration.
411
+
412
+ Args:
413
+ config (Dict[str, Any]): Configuration dictionary.
414
+ """
415
+ global _logger_instance, default_logger
416
+ _logger_instance = get_logger("wl", config)
417
+ default_logger = _logger_instance
418
+
419
+
420
+ def get_log_level() -> int:
421
+ """
422
+ Get the current log level of the default logger.
423
+
424
+ Returns:
425
+ int: Current log level.
426
+ """
427
+ return cast("int", default_logger.min_level)
428
+
429
+
430
+ def set_log_level(level: int) -> None:
431
+ """
432
+ Set the log level of the default logger.
433
+
434
+ Args:
435
+ level (int): New log level.
436
+ """
437
+ default_logger.min_level = level
438
+
439
+
440
+ def enable_console_logging(enable: bool = True) -> None:
441
+ """
442
+ Enable or disable console logging for the default logger.
443
+
444
+ Args:
445
+ enable (bool): Whether to enable console logging.
446
+ """
447
+ if hasattr(default_logger, "enable_console"):
448
+ default_logger.enable_console = enable
449
+
450
+
451
+ def set_log_file(file_path: str | None) -> None:
452
+ """
453
+ Set the log file path for the default logger.
454
+
455
+ Args:
456
+ file_path (Optional[str]): Log file path or None to disable file logging.
457
+ """
458
+ default_logger.log_file = file_path
@@ -0,0 +1,142 @@
1
+ """Output context detection utility.
2
+
3
+ Detects execution context (Claude skill vs manual terminal) and provides
4
+ output verbosity configuration to optimize token consumption.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class OutputConfig:
14
+ """Configuration for output behavior based on execution context."""
15
+
16
+ verbose: bool
17
+ show_progress_bars: bool
18
+ concise_mode: bool
19
+
20
+
21
+ def detect_output_context() -> OutputConfig:
22
+ """Detect the execution context and determine output configuration.
23
+
24
+ Detection strategy:
25
+ 1. Check environment variables for explicit override (WL_VERBOSE, WL_QUIET)
26
+ 2. Check if running in Claude/Anthropic skill context (CLAUDE, ANTHROPIC)
27
+ 3. Check if stdout is a terminal (tty)
28
+ 4. Check if running in CI
29
+ 5. Default to verbose output if no indicators found
30
+
31
+ Returns:
32
+ OutputConfig with appropriate verbosity settings.
33
+ """
34
+ # Check for explicit environment variable overrides first
35
+ if "WL_VERBOSE" in os.environ:
36
+ return OutputConfig(verbose=True, show_progress_bars=True, concise_mode=False)
37
+ if "WL_QUIET" in os.environ:
38
+ return OutputConfig(verbose=False, show_progress_bars=False, concise_mode=True)
39
+
40
+ # Check if running in Claude skill context
41
+ in_claude = (
42
+ "CLAUDE" in os.environ or "ANTHROPIC" in os.environ or "WL_SKILL" in os.environ
43
+ )
44
+
45
+ # Check if stdout is a terminal
46
+ is_tty = sys.stdout.isatty()
47
+
48
+ # Check if running in CI
49
+ is_ci = (
50
+ "CI" in os.environ
51
+ or "GITHUB_ACTIONS" in os.environ
52
+ or "GITLAB_CI" in os.environ
53
+ )
54
+
55
+ # Decision logic:
56
+ # - In Claude context: use concise output, no progress bars
57
+ # - In CI/non-tty: use concise output
58
+ # - In terminal (tty): use full output with progress bars
59
+ if in_claude or is_ci or not is_tty:
60
+ return OutputConfig(verbose=False, show_progress_bars=False, concise_mode=True)
61
+ else:
62
+ return OutputConfig(verbose=True, show_progress_bars=True, concise_mode=False)
63
+
64
+
65
+ def get_output_config(
66
+ override_quiet: bool | None = None, override_verbose: bool | None = None
67
+ ) -> OutputConfig:
68
+ """Get output configuration with optional overrides.
69
+
70
+ Args:
71
+ override_quiet: If True, force concise output. If False, no override.
72
+ override_verbose: If True, force verbose output. If False, no override.
73
+
74
+ Returns:
75
+ OutputConfig with overrides applied if specified.
76
+ """
77
+ config = detect_output_context()
78
+
79
+ # Command-line flags override automatic detection
80
+ if override_verbose:
81
+ config.verbose = True
82
+ config.show_progress_bars = True
83
+ config.concise_mode = False
84
+ elif override_quiet:
85
+ config.verbose = False
86
+ config.show_progress_bars = False
87
+ config.concise_mode = True
88
+
89
+ return config
90
+
91
+
92
+ def should_show_progress_bars(
93
+ override_quiet: bool | None = None, override_verbose: bool | None = None
94
+ ) -> bool:
95
+ """Check if progress bars should be shown.
96
+
97
+ Convenience helper for places that only need this decision.
98
+
99
+ Args:
100
+ override_quiet: Force quiet from command-line flag.
101
+ override_verbose: Force verbose from command-line flag.
102
+
103
+ Returns:
104
+ True if progress bars should be shown, False otherwise.
105
+ """
106
+ config = get_output_config(override_quiet, override_verbose)
107
+ return config.show_progress_bars
108
+
109
+
110
+ def is_concise_mode(
111
+ override_quiet: bool | None = None, override_verbose: bool | None = None
112
+ ) -> bool:
113
+ """Check if we should run in concise mode.
114
+
115
+ Concise mode reduces verbose logging and unnecessary output
116
+ to save tokens when running in Claude skill context.
117
+
118
+ Args:
119
+ override_quiet: Force quiet from command-line flag.
120
+ override_verbose: Force verbose from command-line flag.
121
+
122
+ Returns:
123
+ True if concise mode should be used.
124
+ """
125
+ config = get_output_config(override_quiet, override_verbose)
126
+ return config.concise_mode
127
+
128
+
129
+ def is_verbose(
130
+ override_quiet: bool | None = None, override_verbose: bool | None = None
131
+ ) -> bool:
132
+ """Check if verbose output is enabled.
133
+
134
+ Args:
135
+ override_quiet: Force quiet from command-line flag.
136
+ override_verbose: Force verbose from command-line flag.
137
+
138
+ Returns:
139
+ True if verbose output should be shown.
140
+ """
141
+ config = get_output_config(override_quiet, override_verbose)
142
+ return config.verbose
@@ -0,0 +1 @@
1
+ """Performance monitoring utilities package."""