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,862 @@
1
+ """Configuration management utilities for WL Commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import json
7
+ import os
8
+ import threading
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ..logging import log_error_pair
14
+ from ..platform_adapter import PlatformAdapter
15
+ from ..root import RootKind
16
+ from ..root_fallback import resolve_root_or_start
17
+ from .config_loader import ConfigLoader
18
+ from .defaults import DEFAULT_LOG_FILE, RUNTIME_DERIVED_KEYS
19
+ from .env_exporter import EnvExporter
20
+ from .validation import validate_config
21
+
22
+ # Directory and file paths
23
+ CONFIG_FILE_NAME = "config.json"
24
+ LOG_DIRECTORY = "log"
25
+ LOG_FILE_NAME = "wl_action.log"
26
+
27
+ # Environment variables
28
+ WL_CONFIG_PATH_ENV = "WL_CONFIG_PATH"
29
+ WL_ENVIRONMENT_ENV = "WL_ENVIRONMENT"
30
+
31
+ # Environment names
32
+ ENVIRONMENT_DEVELOPMENT = "development"
33
+ ENVIRONMENT_PRODUCTION = "production"
34
+ ENVIRONMENT_TESTING = "testing"
35
+
36
+ # CI environment variables
37
+ CI_ENV_VARS = ["CI", "GITHUB_ACTIONS", "JENKINS_HOME"]
38
+
39
+ # Test environment variable
40
+ TEST_ENV_VAR = "PYTEST_CURRENT_TEST"
41
+
42
+
43
+ class ConfigPathResolver:
44
+ """Resolves configuration file paths with cross-platform support."""
45
+
46
+ def get_default_config_path(self) -> str:
47
+ """Get default configuration file path.
48
+
49
+ First checks environment variable WL_CONFIG_PATH_ENV, then resolves the
50
+ **deploy root** through the root module (the git work-tree root inside a
51
+ repository, else the project root -- see
52
+ :class:`~wl_cli.utils.root.RootKind`), falling back to the current
53
+ directory. Anchoring the config at the deploy root keeps it in the *same*
54
+ ``.wl`` as the hooks leg (#95 / #96); resolving it at the project root
55
+ split one ``.wl`` into two, so a workspace member repo silently stopped
56
+ reading the workspace config.
57
+
58
+ ``resolve`` answers ``None`` when there is neither a git work tree nor a
59
+ project marker; the substitution that used to hide inside the deploy
60
+ resolver (it returned the starting directory) is supplied by the single
61
+ CLI-top-level fallback policy,
62
+ :func:`~wl_cli.utils.root_fallback.resolve_root_or_start` (#126), per
63
+ ``docs/adr/0006-root-resolution-layering.md``.
64
+ """
65
+ config_path = os.environ.get(WL_CONFIG_PATH_ENV)
66
+ if config_path:
67
+ return config_path
68
+
69
+ try:
70
+ start = Path.cwd()
71
+ except OSError:
72
+ # A deleted working directory (#95): the chain this replaced
73
+ # (deploy resolver -> project walk) degraded to a relative ``.wl``
74
+ # rather than raising, so keep that result instead of letting
75
+ # ``Path.cwd()`` escape.
76
+ start = Path(".")
77
+
78
+ # ADR 0006: 回退是调用方策略;唯一实现在 utils/root_fallback.py(#126).
79
+ # This is the one landing point that does NOT canonicalise ``start``: it
80
+ # passes the un-resolved ``Path.cwd()``, so the fallback answer keeps the
81
+ # spelling the old no-argument resolver returned.
82
+ deploy_root = resolve_root_or_start(start, RootKind.DEPLOY)
83
+
84
+ return str(deploy_root.wl_dir() / CONFIG_FILE_NAME)
85
+
86
+ def get_user_home_dir(self) -> str:
87
+ """Get user home directory in a cross-platform way.
88
+
89
+ Mirrors the platform order it has always had: ``Path.home()`` first, then
90
+ ``PATH``-independent ``~`` expansion with the Windows ``USERPROFILE`` /
91
+ ``HOMEDRIVE`` + ``HOMEPATH`` handling, and finally the caller-side
92
+ fallback to the current directory. That last step is a *fallback*, not
93
+ part of "which home does this platform have". ADR 0006 keeps it written
94
+ out here rather than inside a resolver, and #126 deliberately does **not**
95
+ fold it into the single root-fallback policy: the question here is the
96
+ user's **home**, a different domain from the project **root**, so merging
97
+ the two would conflate them (see ADR 0006 修订 1).
98
+ """
99
+ try:
100
+ return str(Path.home())
101
+ except Exception:
102
+ # Fallback for when Path.home() fails
103
+ home_dir = os.path.expanduser("~")
104
+
105
+ # Windows-specific additional handling
106
+ if PlatformAdapter.is_windows() and (not home_dir or home_dir == "~"):
107
+ home_dir = os.environ.get("USERPROFILE", "")
108
+ if not home_dir:
109
+ home_drive = os.environ.get("HOMEDRIVE", "")
110
+ home_path_env = os.environ.get("HOMEPATH", "")
111
+ if home_drive and home_path_env:
112
+ home_dir = os.path.join(home_drive, home_path_env)
113
+
114
+ # Final fallback
115
+ if not home_dir or home_dir == "~":
116
+ # ADR 0006: 这是 home 解析兜底(C 类),非「根回退」,按
117
+ # ADR 修订 1 的裁决保留、不收编(#126)。
118
+ home_dir = os.getcwd()
119
+
120
+ return home_dir
121
+
122
+
123
+ class ConfigMerger:
124
+ """Recursively merge configuration dictionaries with proper precedence."""
125
+
126
+ def merge(self, base: dict[str, Any], *overlays: dict[str, Any]) -> dict[str, Any]:
127
+ """Recursively merge multiple configuration dictionaries.
128
+
129
+ Later overlays override earlier ones. For nested dictionaries,
130
+ merging is done recursively rather than replacing the whole dict.
131
+
132
+ Args:
133
+ base: Base configuration
134
+ *overlays: Overlay configurations to merge on top
135
+
136
+ Returns:
137
+ New merged configuration dictionary (base is not modified)
138
+ """
139
+ result = copy.deepcopy(base)
140
+ for overlay in overlays:
141
+ self._recursive_merge(result, overlay)
142
+ return result
143
+
144
+ def _recursive_merge(self, target: dict[str, Any], source: dict[str, Any]) -> None:
145
+ """Recursively merge source into target."""
146
+ for key, value in source.items():
147
+ if (
148
+ key in target
149
+ and isinstance(target[key], dict)
150
+ and isinstance(value, dict)
151
+ ):
152
+ self._recursive_merge(target[key], value)
153
+ else:
154
+ target[key] = value
155
+
156
+ def merge_changed_keys(
157
+ self,
158
+ target: dict[str, Any],
159
+ source: dict[str, Any],
160
+ original: dict[str, Any],
161
+ ) -> None:
162
+ """Merge only changed keys from source into target.
163
+
164
+ Only adds/updates keys that are not in original or have changed value.
165
+ Used for saving configuration to preserve original user settings.
166
+
167
+ Args:
168
+ target: Target dictionary to update
169
+ source: Source dictionary with current values
170
+ original: Original dictionary to compare against
171
+ """
172
+ for key, value in source.items():
173
+ if key not in original:
174
+ # Key doesn't exist in original, add it
175
+ target[key] = value
176
+ elif isinstance(value, dict) and isinstance(original[key], dict):
177
+ # Both are dicts, recursively check nested keys
178
+ if key not in target:
179
+ target[key] = {}
180
+ self.merge_changed_keys(target[key], value, original[key])
181
+ elif value != original.get(key):
182
+ # Value has changed, update it
183
+ target[key] = value
184
+
185
+
186
+ class ConfigSaver:
187
+ """Save configuration to file with smart change tracking.
188
+
189
+ Only saves changed keys compared to the original project configuration,
190
+ preserving default values that aren't saved to disk.
191
+ """
192
+
193
+ def __init__(self, config_file_path: str) -> None:
194
+ """Initialize configuration saver.
195
+
196
+ Args:
197
+ config_file_path: Path to the configuration file to save
198
+ """
199
+ self.config_file_path = config_file_path
200
+ self.merger = ConfigMerger()
201
+
202
+ def save(
203
+ self,
204
+ full_config: dict[str, Any],
205
+ original_project_config: dict[str, Any],
206
+ *,
207
+ force: bool = False,
208
+ ) -> bool:
209
+ """Save configuration changes to file.
210
+
211
+ Only saves keys that have changed from the original project config.
212
+ Creates parent directory if it doesn't exist.
213
+
214
+ Args:
215
+ full_config: Complete merged configuration from all sources
216
+ original_project_config: Original project configuration as loaded from disk
217
+ force: Write even in a test environment. The test-environment skip
218
+ below is a default for *incidental* saves; an explicit
219
+ "materialise this file" call must not inherit it, or the caller
220
+ gets ``True`` for a file that was never written (#202).
221
+
222
+ Returns:
223
+ True when the file was written (or the write was intentionally
224
+ skipped in a test environment without ``force``); False when the
225
+ write failed. The error is logged -- callers such as ``wl config
226
+ set`` and the .wl self-sync must be able to report the failure
227
+ instead of claiming success (#177).
228
+ """
229
+ # Don't save to disk in test environment -- unless the caller is
230
+ # explicitly materialising a file (see ``force``).
231
+ if not force and os.environ.get(TEST_ENV_VAR):
232
+ return True
233
+
234
+ try:
235
+ config_path = Path(self.config_file_path)
236
+ config_dir = config_path.parent
237
+ if config_dir and not config_dir.exists():
238
+ config_dir.mkdir(parents=True, exist_ok=True)
239
+
240
+ # Only save the original project config plus any new keys that were set
241
+ # This prevents default values from being saved to the config file
242
+ if original_project_config:
243
+ # Use deep copy to ensure no mutation of original input
244
+ config_to_save = copy.deepcopy(original_project_config)
245
+ self.merger.merge_changed_keys(
246
+ config_to_save, full_config, original_project_config
247
+ )
248
+ else:
249
+ # If no original project config, just save a deep copy of current config
250
+ config_to_save = copy.deepcopy(full_config)
251
+
252
+ with open(self.config_file_path, "w", encoding="utf-8") as f:
253
+ json.dump(config_to_save, f, indent=2, ensure_ascii=False)
254
+ f.write("\n")
255
+ return True
256
+ except OSError as e:
257
+ # Saving is still best-effort, but no longer silent: the caller gets
258
+ # False and the user gets a bilingual error (#177).
259
+ log_error_pair(
260
+ f"Failed to save configuration to {self.config_file_path}: {e}",
261
+ f"无法将配置保存到 {self.config_file_path}:{e}",
262
+ )
263
+ return False
264
+
265
+
266
+ class ConfigManager:
267
+ """Manage application configuration with auto-reload support.
268
+
269
+ This is a facade that delegates to smaller, single-responsibility components:
270
+ - ConfigPathResolver: Resolves configuration file paths
271
+ - ConfigLoader: Loads configuration from multiple sources
272
+ - ConfigSaver: Saves configuration changes smartly
273
+ """
274
+
275
+ def __init__(self, config_file: str | None = None) -> None:
276
+ """
277
+ Initialize configuration manager with support for configuration layers.
278
+
279
+ Args:
280
+ config_file (str, optional): Path to project configuration file.
281
+ """
282
+ self._path_resolver = ConfigPathResolver()
283
+ self.config_file = config_file or self._path_resolver.get_default_config_path()
284
+ self._loader = ConfigLoader(self.config_file, self._path_resolver)
285
+ self._env_exporter = EnvExporter(self._loader)
286
+ self._config: dict[str, Any] = {}
287
+ self._last_modified_time: float = 0.0
288
+ self._original_project_config: dict[str, Any] = {}
289
+ # Top-level keys this process set explicitly. A runtime-derived key is
290
+ # only written to disk when it is in here (#208).
291
+ self._explicit_keys: set[str] = set()
292
+ self._load_config()
293
+
294
+ def _get_default_config(self) -> dict[str, Any]:
295
+ """Get default configuration dictionary.
296
+
297
+ This is kept for backward compatibility with tests.
298
+ """
299
+ from .defaults import get_default_config
300
+
301
+ return get_default_config()
302
+
303
+ def _get_environment(self) -> str:
304
+ """Get current environment mode.
305
+
306
+ This is kept for backward compatibility with tests.
307
+ """
308
+ return self._loader.get_environment()
309
+
310
+ def _load_config(self) -> None:
311
+ """Load configuration from all sources using ConfigLoader."""
312
+ # Load all configuration layers
313
+ merged_config = self._loader.load_all()
314
+
315
+ # Save original project config for later comparison when saving
316
+ project_config = self._loader.load_config_file(self.config_file)
317
+ self._original_project_config = project_config.copy()
318
+
319
+ # Validate configuration
320
+ self._config = validate_config(merged_config)
321
+
322
+ # Loading is a **read** and must never materialise the project config.
323
+ # Writing here meant that merely importing wl_cli (module-level logger →
324
+ # LoggingConfig → get_config) dropped a `.wl/` into whatever directory
325
+ # happened to be the CWD — see the lazy logger in utils/logging.py.
326
+ #
327
+ # Creating the file is an explicit act now: `wl init` calls
328
+ # ensure_project_config(), and `set()` still persists on a write.
329
+ #
330
+ # Gating the write on "does the project dir exist?" was tried and is NOT
331
+ # enough: get_config_manager() is a process-cached singleton, so the
332
+ # decision falls to whichever call happens to run first — and `wl init`
333
+ # logs before it ever reaches generate_configs(), so the manager gets
334
+ # built while `.wl/` does not exist yet and never re-checks.
335
+ self._last_modified_time = time.time()
336
+
337
+ def ensure_project_config(self) -> bool:
338
+ """Materialise the project config file, writing defaults when absent.
339
+
340
+ The one explicit way to create `.wl/config.json`. `wl init` calls this;
341
+ nothing else should need to, because every other path either only reads
342
+ or writes through `set()`.
343
+
344
+ This is a *create* contract, so it passes ``force`` down: under pytest
345
+ the incidental-save guard would otherwise answer "written" while leaving
346
+ no file behind, and the .wl self-sync then half-materialised a checkout
347
+ -- templates deployed, no `config.json`, a failed version stamp and the
348
+ one-hour `.update_failed` cooldown on a brand-new worktree (#202).
349
+
350
+ It also writes the *creation* defaults rather than ``self._config``.
351
+ ``self._config`` is the merged runtime view, so it carries values derived
352
+ from the process that happens to be creating the file -- under pytest
353
+ ``environment=testing``, in CI ``production``. Since ``load_all`` prefers
354
+ the file's ``environment`` over detection, persisting that value turned
355
+ a derivation into a permanent override, and the version stamp then
356
+ certified it as current (#208). See
357
+ :func:`~wl_cli.utils.config.defaults.get_creation_defaults`.
358
+
359
+ Returns:
360
+ True when the file exists afterwards; False when the defaults could
361
+ not be written (#202).
362
+ """
363
+ if os.path.exists(self.config_file):
364
+ return True
365
+ ok = self._save_config(force=True, creation=True)
366
+ return ok and os.path.exists(self.config_file)
367
+
368
+ def _persistable_config(self) -> dict[str, Any]:
369
+ """The merged config minus runtime-derived values nobody set (#208).
370
+
371
+ ``self._config`` is the merged runtime view, and ``ConfigLoader.load_all``
372
+ always fills in ``environment`` -- derived from this process or read back
373
+ from the file. Writing that value on an ordinary save would recreate the
374
+ permanent override that :meth:`ensure_project_config` deliberately keeps
375
+ out of a new file, so it reaches disk only when ``set()`` recorded it as
376
+ explicit user intent.
377
+
378
+ Returns:
379
+ A shallow copy of the merged config safe to persist.
380
+ """
381
+ return {
382
+ key: value
383
+ for key, value in self._config.items()
384
+ if key not in RUNTIME_DERIVED_KEYS or key in self._explicit_keys
385
+ }
386
+
387
+ def _save_config(self, *, force: bool = False, creation: bool = False) -> bool:
388
+ """Save configuration to file using ConfigSaver.
389
+
390
+ Args:
391
+ force: Write even in a test environment (see
392
+ :meth:`ensure_project_config`).
393
+ creation: Write the environment-independent creation payload instead
394
+ of the merged runtime config (see :meth:`ensure_project_config`).
395
+ Only the create path passes this; ``set()`` keeps persisting the
396
+ merged config, so an explicit ``wl config set environment ...``
397
+ is still written as the user's own setting (#208).
398
+
399
+ The runtime-derived keys are filtered out of the merged view unless
400
+ ``set()`` recorded them as explicit user intent. Doing this only on the
401
+ create path would leave the invariant one ``wl config set`` away from
402
+ being undone: the merged view always carries ``environment``, so the
403
+ first ordinary save in an empty repo (or any save in CI) wrote the
404
+ derivation back into the neutral file (#208).
405
+
406
+ Returns:
407
+ True when the write succeeded; False when it failed (#177).
408
+ """
409
+ if creation:
410
+ from .defaults import get_creation_defaults
411
+
412
+ full_config, original_config = get_creation_defaults(), {}
413
+ else:
414
+ full_config = self._persistable_config()
415
+ original_config = self._original_project_config
416
+
417
+ saver = ConfigSaver(self.config_file)
418
+ ok = saver.save(
419
+ full_config=full_config,
420
+ original_project_config=original_config,
421
+ force=force,
422
+ )
423
+ if ok:
424
+ # Only a real (or intentionally skipped) save resets the reload
425
+ # baseline; a failed write leaves the on-disk state behind.
426
+ self._last_modified_time = time.time()
427
+ return ok
428
+
429
+ def _should_reload(self) -> bool:
430
+ """Check if configuration should be reloaded based on file modification time."""
431
+ # In test environment, never reload configuration to prevent losing test data
432
+ if os.environ.get(TEST_ENV_VAR):
433
+ return False
434
+ try:
435
+ config_path = Path(self.config_file)
436
+ if config_path.exists():
437
+ current_mtime = config_path.stat().st_mtime
438
+ return current_mtime > self._last_modified_time
439
+ except OSError:
440
+ pass
441
+ return False
442
+
443
+ def get(self, key: str, default: Any = None, auto_reload: bool = True) -> Any:
444
+ """
445
+ Get configuration value with support for nested keys.
446
+
447
+ Args:
448
+ key (str): Configuration key (supports dot notation for nested keys).
449
+ default (Any, optional): Default value if key not found.
450
+ auto_reload (bool, optional): Whether to automatically reload if config file changed.
451
+
452
+ Returns:
453
+ Any: Configuration value.
454
+ """
455
+ if auto_reload and self._should_reload():
456
+ self._load_config()
457
+
458
+ # Support dot notation for nested keys
459
+ value = self._config
460
+ for part in key.split("."):
461
+ if isinstance(value, dict) and part in value:
462
+ value = value[part]
463
+ else:
464
+ return default
465
+
466
+ if key == "log_file" and value and isinstance(value, str):
467
+ log_path = Path(value)
468
+ if log_path.is_absolute():
469
+ return value
470
+ # Check if value is the default log file path pattern
471
+ default_log_path = Path(DEFAULT_LOG_FILE)
472
+ if log_path == default_log_path:
473
+ return value
474
+ config_dir = Path(self.config_file).parent
475
+ return str(config_dir / value)
476
+
477
+ return value
478
+
479
+ def set(self, key: str, value: Any) -> bool:
480
+ """Set configuration value with support for nested keys.
481
+
482
+ Returns:
483
+ True when the change was persisted (or the write was intentionally
484
+ skipped in a test environment); False when the write failed (#177).
485
+ """
486
+ # Support dot notation for nested keys
487
+ parts = key.split(".")
488
+ config = self._config
489
+
490
+ for part in parts[:-1]:
491
+ if part not in config or not isinstance(config[part], dict):
492
+ config[part] = {}
493
+ config = config[part]
494
+
495
+ config[parts[-1]] = value
496
+ # A runtime-derived key only becomes writable when the user names it:
497
+ # an explicit ``wl config set environment ...`` is intent, a derivation
498
+ # is not (#208).
499
+ self._explicit_keys.add(parts[0])
500
+ # Only save if not in test environment
501
+ if not os.environ.get(TEST_ENV_VAR):
502
+ return self._save_config()
503
+ # In test environment, update last modified time to prevent unnecessary reload
504
+ self._last_modified_time = time.time()
505
+ return True
506
+
507
+ def unset(self, key: str) -> bool:
508
+ """Remove a top-level ``key`` from the project config file.
509
+
510
+ ``wl config reset`` uses this for runtime-derived keys: their default is
511
+ a *derivation* (``development``), not a project setting, so writing it
512
+ back would freeze exactly the value #208 keeps out of the file. Deleting
513
+ the key restores derivation on the next load, and also clears the
514
+ in-memory merged value so a later get does not read the stale one.
515
+
516
+ Returns:
517
+ True when the removal was persisted (or the write was intentionally
518
+ skipped in a test environment); False when the write failed (#177).
519
+ """
520
+ self._explicit_keys.discard(key)
521
+ self._config.pop(key, None)
522
+ self._original_project_config.pop(key, None)
523
+ if key in RUNTIME_DERIVED_KEYS:
524
+ # Keep the merged view complete for the rest of the process: readers
525
+ # still need a value, and the re-derived one is filtered back out of
526
+ # any later write (see ``_persistable_config``).
527
+ self._config[key] = self._loader.get_environment()
528
+ if not os.environ.get(TEST_ENV_VAR):
529
+ return self._save_config()
530
+ self._last_modified_time = time.time()
531
+ return True
532
+
533
+ def get_all(self, auto_reload: bool = True) -> dict[str, Any]:
534
+ """Get all configuration."""
535
+ if auto_reload and self._should_reload():
536
+ self._load_config()
537
+ return self._config.copy()
538
+
539
+ def reload(self) -> None:
540
+ """Reload configuration from all sources."""
541
+ self._load_config()
542
+
543
+ def hot_reload(self) -> bool:
544
+ """
545
+ Hot reload configuration from file.
546
+
547
+ Returns:
548
+ bool: True if configuration was reloaded, False otherwise.
549
+ """
550
+ # Save current config
551
+ current_config = self._config.copy()
552
+
553
+ # In test environment, always reload to ensure test data is loaded
554
+ if os.environ.get(TEST_ENV_VAR):
555
+ self._load_config()
556
+ # Restore current config in test environment
557
+ for key, value in current_config.items():
558
+ self._config[key] = value
559
+ return True
560
+ if self._should_reload():
561
+ self._load_config()
562
+ # Restore existing keys that weren't reloaded to maintain current state
563
+ for key, value in current_config.items():
564
+ if key not in self._config:
565
+ self._config[key] = value
566
+ return True
567
+ return False
568
+
569
+ def export_environment_variables(
570
+ self, environment: str | None = None
571
+ ) -> dict[str, str]:
572
+ """Export current configuration as environment variables."""
573
+ config = self.get_all()
574
+ return self._env_exporter.export_environment_variables(config, environment)
575
+
576
+ def get_environment_template(self, environment: str) -> dict[str, str]:
577
+ """Get environment variable template for specific environment."""
578
+ return self._env_exporter.get_environment_template(environment)
579
+
580
+
581
+ _config_manager: ConfigManager | None = None
582
+ _config_manager_lock = threading.RLock()
583
+
584
+
585
+ class ConfigContext:
586
+ """配置上下文管理器,用于管理配置的作用域"""
587
+
588
+ def __init__(self, config_manager: ConfigManager | None = None) -> None:
589
+ """初始化配置上下文
590
+
591
+ Args:
592
+ config_manager: 自定义配置管理器实例,如果为None则使用全局实例
593
+ """
594
+ self._custom_manager = config_manager or ConfigManager()
595
+ self._original_manager: ConfigManager | None = None
596
+
597
+ def __enter__(self) -> ConfigManager:
598
+ """进入上下文,返回配置管理器"""
599
+ # 保存原始管理器
600
+ global _config_manager
601
+ with _config_manager_lock:
602
+ self._original_manager = _config_manager
603
+ _config_manager = self._custom_manager
604
+ return self._custom_manager
605
+
606
+ def __exit__(self, *_) -> None:
607
+ """退出上下文,恢复原始管理器"""
608
+ global _config_manager
609
+ with _config_manager_lock:
610
+ # 强制恢复原始管理器,即使它是None
611
+ _config_manager = self._original_manager
612
+
613
+
614
+ class ConfigProvider:
615
+ """配置提供者,用于依赖注入
616
+
617
+ ConfigProvider provides a way to inject configuration management into other components
618
+ without relying on global state. It can either use a custom ConfigManager instance
619
+ or fall back to the global one.
620
+ """
621
+
622
+ def __init__(self, config_manager: ConfigManager | None = None):
623
+ """初始化配置提供者
624
+
625
+ Args:
626
+ config_manager: 配置管理器实例,如果为None则使用全局实例
627
+ """
628
+ self._config_manager = config_manager
629
+
630
+ def get_config(self, key: str, default: Any = None) -> Any:
631
+ """获取配置值
632
+
633
+ Args:
634
+ key: 配置键(支持点号表示嵌套键)
635
+ default: 默认值,如果键不存在则返回
636
+
637
+ Returns:
638
+ 配置值
639
+ """
640
+ from .core import get_config_manager
641
+
642
+ manager = self._config_manager or get_config_manager()
643
+ return manager.get(key, default)
644
+
645
+ def set_config(self, key: str, value: Any) -> None:
646
+ """设置配置值
647
+
648
+ Args:
649
+ key: 配置键(支持点号表示嵌套键)
650
+ value: 配置值
651
+ """
652
+ from .core import get_config_manager
653
+
654
+ manager = self._config_manager or get_config_manager()
655
+ manager.set(key, value)
656
+
657
+ def get_all(self) -> dict[str, Any]:
658
+ """获取所有配置
659
+
660
+ Returns:
661
+ 所有配置的字典副本
662
+ """
663
+ from .core import get_config_manager
664
+
665
+ manager = self._config_manager or get_config_manager()
666
+ return manager.get_all()
667
+
668
+ def reload(self) -> None:
669
+ """重新加载配置
670
+
671
+ Reloads configuration from all sources (file, environment variables, etc.)
672
+ """
673
+ from .core import get_config_manager
674
+
675
+ manager = self._config_manager or get_config_manager()
676
+ manager.reload()
677
+
678
+ @property
679
+ def config_manager(self) -> ConfigManager:
680
+ """获取配置管理器实例
681
+
682
+ Returns:
683
+ 配置管理器实例
684
+ """
685
+ from .core import get_config_manager
686
+
687
+ return self._config_manager or get_config_manager()
688
+
689
+
690
+ def get_config_manager() -> ConfigManager:
691
+ """Get global configuration manager instance.
692
+
693
+ Returns:
694
+ Global ConfigManager instance
695
+ """
696
+ import os
697
+ import tempfile
698
+
699
+ global _config_manager
700
+ with _config_manager_lock:
701
+ if _config_manager is None:
702
+ # In test environment, create a ConfigManager with an empty configuration
703
+ if os.environ.get(TEST_ENV_VAR):
704
+ with tempfile.NamedTemporaryFile(
705
+ mode="w", suffix=".json", delete=False
706
+ ) as f:
707
+ f.write("{}") # 写入空配置
708
+ temp_config_file = f.name
709
+ try:
710
+ _config_manager = ConfigManager(config_file=temp_config_file)
711
+ finally:
712
+ if os.path.exists(temp_config_file):
713
+ os.unlink(temp_config_file)
714
+ else:
715
+ _config_manager = ConfigManager()
716
+ return _config_manager
717
+
718
+
719
+ def get_config(key: str, default: Any = None) -> Any:
720
+ """Get configuration value from global manager with auto-reload.
721
+
722
+ Args:
723
+ key: 配置键(支持点号表示嵌套键)
724
+ default: 默认值,如果键不存在则返回
725
+
726
+ Returns:
727
+ 配置值
728
+ """
729
+ return get_config_manager().get(key, default)
730
+
731
+
732
+ def set_config(key: str, value: Any) -> None:
733
+ """Set configuration value using the global manager.
734
+
735
+ Args:
736
+ key: 配置键(支持点号表示嵌套键)
737
+ value: 配置值
738
+ """
739
+ get_config_manager().set(key, value)
740
+
741
+
742
+ def reload_config() -> bool:
743
+ """Reload global configuration.
744
+
745
+ Returns:
746
+ True if configuration was reloaded, False otherwise
747
+ """
748
+ return get_config_manager().hot_reload()
749
+
750
+
751
+ def reset_config_manager() -> None:
752
+ """重置全局配置管理器,主要用于测试
753
+
754
+ Resets the global configuration manager instance to None, forcing a new instance
755
+ to be created on the next call to get_config_manager().
756
+ """
757
+ global _config_manager
758
+ with _config_manager_lock:
759
+ _config_manager = None
760
+
761
+
762
+ # Multi-language message mappings
763
+ MESSAGES = {
764
+ "en": {
765
+ "config": {
766
+ "loaded": "Configuration loaded successfully",
767
+ "reloaded": "Configuration reloaded successfully",
768
+ "saved": "Configuration saved successfully",
769
+ "not_found": "Configuration file not found, using defaults",
770
+ "invalid": "Invalid configuration, using defaults",
771
+ },
772
+ "errors": {
773
+ "file_not_found": "File not found: {path}",
774
+ "permission_denied": "Permission denied: {path}",
775
+ "invalid_input": "Invalid input: {message}",
776
+ "command_failed": "Command failed: {command}",
777
+ "unknown_error": "Unknown error occurred",
778
+ },
779
+ "info": {
780
+ "starting": "Starting {command}",
781
+ "completed": "{command} completed successfully",
782
+ "skipped": "{command} skipped",
783
+ "warning": "Warning: {message}",
784
+ },
785
+ },
786
+ "zh": {
787
+ "config": {
788
+ "loaded": "配置加载成功",
789
+ "reloaded": "配置重新加载成功",
790
+ "saved": "配置保存成功",
791
+ "not_found": "未找到配置文件,使用默认值",
792
+ "invalid": "无效的配置,使用默认值",
793
+ },
794
+ "errors": {
795
+ "file_not_found": "文件未找到: {path}",
796
+ "permission_denied": "权限被拒绝: {path}",
797
+ "invalid_input": "无效的输入: {message}",
798
+ "command_failed": "命令执行失败: {command}",
799
+ "unknown_error": "发生未知错误",
800
+ },
801
+ "info": {
802
+ "starting": "正在启动 {command}",
803
+ "completed": "{command} 执行成功",
804
+ "skipped": "{command} 已跳过",
805
+ "warning": "警告: {message}",
806
+ },
807
+ },
808
+ }
809
+
810
+
811
+ def get_message(key: str, language: str = "auto", **kwargs) -> str:
812
+ """
813
+ Get message in specified language.
814
+
815
+ Args:
816
+ key: Message key in format "category.message"
817
+ language: Language code ("en", "zh", or "auto")
818
+ **kwargs: Parameters to format the message
819
+
820
+ Returns:
821
+ Formatted message string
822
+ """
823
+ from ..language_utils import _should_display_language
824
+
825
+ # Determine language if auto
826
+ if language == "auto":
827
+ if _should_display_language("zh"):
828
+ language = "zh"
829
+ else:
830
+ language = "en"
831
+
832
+ # Default to English if language not supported
833
+ if language not in MESSAGES:
834
+ language = "en"
835
+
836
+ # Split key into category and message
837
+ parts = key.split(".")
838
+ if len(parts) != 2:
839
+ return f"Message not found: {key}"
840
+
841
+ category, message_key = parts
842
+
843
+ # Get message from dictionary
844
+ try:
845
+ message = MESSAGES[language][category][message_key]
846
+ return message.format(**kwargs)
847
+ except KeyError:
848
+ return f"Message not found: {key}"
849
+
850
+
851
+ __all__ = [
852
+ "ConfigManager",
853
+ "ConfigContext",
854
+ "ConfigProvider",
855
+ "EnvExporter",
856
+ "get_config_manager",
857
+ "get_config",
858
+ "set_config",
859
+ "reload_config",
860
+ "reset_config_manager",
861
+ "get_message",
862
+ ]