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.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- wl_cli/vendors/sop_doc/rust-readme.md +219 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""The one place ADR 0006's caller-side CWD fallback policy is implemented.
|
|
2
|
+
|
|
3
|
+
``utils/root.py`` answers "which root does this starting point belong to" and
|
|
4
|
+
reports "no root found" as ``None`` -- it performs no fallback of its own. *What
|
|
5
|
+
to do* with that ``None`` is the caller's policy, and ADR 0006
|
|
6
|
+
(``docs/adr/0006-root-resolution-layering.md``) commits to implementing it in
|
|
7
|
+
exactly one place at the CLI top level. This module is that place.
|
|
8
|
+
|
|
9
|
+
The policy is one rule: a ``wl`` run from a directory that belongs to no root is
|
|
10
|
+
not an error, so the starting directory itself is used as the root, labelled
|
|
11
|
+
with the kind the caller asked for. Before #126 that rule was written out at
|
|
12
|
+
every call site -- sixteen markers across nine files, converging on nine real
|
|
13
|
+
landing points, several of them byte-identical copies of the same three lines --
|
|
14
|
+
and they now all route through :func:`resolve_root_or_start`.
|
|
15
|
+
|
|
16
|
+
Two properties are load-bearing and deliberate:
|
|
17
|
+
|
|
18
|
+
* **It takes the kind.** Both the ``PROJECT`` and ``DEPLOY`` roots fall back the
|
|
19
|
+
same way, so the rule is parameterised by kind rather than copied per kind.
|
|
20
|
+
* **It never touches *start*.** ``resolve`` answers for the *resolved* spelling
|
|
21
|
+
of its input, but every old resolver fell back to the spelling the caller
|
|
22
|
+
actually passed. So this function uses *start* verbatim -- as the ``resolve``
|
|
23
|
+
input and as the fallback answer's ``path`` -- and leaves canonicalisation to
|
|
24
|
+
the caller. The callers that resolved their start through a ``_start_directory``
|
|
25
|
+
helper still do (keeping their fallback byte-identical to the answer it
|
|
26
|
+
replaces), and ``ConfigPathResolver.get_default_config_path`` keeps passing its
|
|
27
|
+
*un-resolved* ``Path.cwd()`` exactly as before.
|
|
28
|
+
|
|
29
|
+
This module is deliberately **not** part of ``utils/root.py``: that module's
|
|
30
|
+
whole contract is that it performs no fallback, and folding this in would widen
|
|
31
|
+
its deliberate three-name interface (``Root`` / ``RootKind`` / ``resolve``) with
|
|
32
|
+
a fourth name that answers a different question.
|
|
33
|
+
|
|
34
|
+
#125 added :func:`current_root` -- the *other* caller-side policy over the root
|
|
35
|
+
module: which directory the process itself is anchored at. ``resolve`` requires
|
|
36
|
+
an explicit starting point and never reads an implicit CWD (ADR 0006), so
|
|
37
|
+
something has to supply the process CWD as that start, and this is the single
|
|
38
|
+
place that does. It exists because a bare ``Path(".wl")`` -- resolved against
|
|
39
|
+
the process CWD -- used to be scattered across ~20 sites, so a ``wl`` run from a
|
|
40
|
+
subdirectory read the subdirectory's ``.wl`` instead of the root's.
|
|
41
|
+
|
|
42
|
+
#160 added :func:`_start_directory` (the one CWD read both policies below share)
|
|
43
|
+
and :func:`current_context`, which resolves the three roots one invocation is
|
|
44
|
+
anchored on -- project, deploy and the legacy ``.wl`` to migrate -- *together*,
|
|
45
|
+
once, at a public entry. Callers that need only one root go through
|
|
46
|
+
:func:`current_root` instead of carrying a context they do not use.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
from .root import Root, RootKind, resolve
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"resolve_root_or_start",
|
|
58
|
+
"current_root",
|
|
59
|
+
"current_worktree",
|
|
60
|
+
"RootContext",
|
|
61
|
+
"current_context",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class RootContext:
|
|
67
|
+
"""The roots one ``wl`` invocation is anchored on, resolved once (#160).
|
|
68
|
+
|
|
69
|
+
Before #160 the self-sync chain re-derived its roots at every layer, each
|
|
70
|
+
layer spelling the derivation out as ``Root | None = None`` with an
|
|
71
|
+
``else _deploy_root()`` default. A caller that forgot to thread the value
|
|
72
|
+
down did not fail -- it silently fell back to the process CWD, which is the
|
|
73
|
+
bug class #140 removed. Resolving the three values once at the public entry
|
|
74
|
+
and handing this frozen value down makes a missing value a type error.
|
|
75
|
+
|
|
76
|
+
* ``project`` -- the ``PROJECT`` root (nearest ``.wl`` / ``pyproject.toml``);
|
|
77
|
+
* ``deploy`` -- the ``DEPLOY`` root (git work-tree top, else project root);
|
|
78
|
+
* ``legacy_wl_dir`` -- the pre-#95/#96 ``.wl`` still to be migrated, or
|
|
79
|
+
``None`` when there is none (see :func:`current_context`).
|
|
80
|
+
|
|
81
|
+
Every field is already a real root: the fallback policy (:func:`current_root`
|
|
82
|
+
/ :func:`resolve_root_or_start`) has been applied, so no consumer has to
|
|
83
|
+
re-answer "what if there is none".
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
project: Root
|
|
87
|
+
deploy: Root
|
|
88
|
+
legacy_wl_dir: Path | None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def resolve_root_or_start(start: Path, kind: RootKind) -> Root:
|
|
92
|
+
"""The ``kind``-root for *start*, falling back to *start* when there is none.
|
|
93
|
+
|
|
94
|
+
``resolve(start, kind)`` reports "no root found from here" as ``None`` (ADR
|
|
95
|
+
0006: a result, not an error). Commands whose contract is to return *some*
|
|
96
|
+
root -- ``wl lint``, ``wl gate``, ``wl clean``, the ``.wl`` self-sync, the
|
|
97
|
+
test subsystem -- substitute the starting directory instead, and this is the
|
|
98
|
+
single function that does so.
|
|
99
|
+
|
|
100
|
+
*start* is used exactly as given, both as the ``resolve`` input and as the
|
|
101
|
+
fallback answer's ``path``: no canonicalisation happens here, so a caller
|
|
102
|
+
that needs a resolved spelling resolves it before calling (see the module
|
|
103
|
+
docstring for why that preserves behaviour).
|
|
104
|
+
|
|
105
|
+
The returned value is always a :class:`~wl_cli.utils.root.Root`; a caller
|
|
106
|
+
that previously handed back a bare :class:`~pathlib.Path` (``lint`` /
|
|
107
|
+
``clean`` / ``TemplateManager``) or a ``str`` (``project_root_for``) keeps
|
|
108
|
+
that outward type by taking ``.path`` or ``str(...)`` of the result, so no
|
|
109
|
+
caller's signature changes.
|
|
110
|
+
"""
|
|
111
|
+
root = resolve(start, kind)
|
|
112
|
+
if root is not None:
|
|
113
|
+
return root
|
|
114
|
+
return Root(start, kind)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def current_root(kind: RootKind) -> Root:
|
|
118
|
+
"""The ``kind``-root the process's *current directory* belongs to (#125).
|
|
119
|
+
|
|
120
|
+
The root module requires an explicit starting point and never reads an
|
|
121
|
+
implicit CWD (ADR 0006). Callers that hold a start of their own pass it to
|
|
122
|
+
:func:`resolve_root_or_start`; callers that *are* the process -- a
|
|
123
|
+
:class:`~click.Command` with no start in hand -- need the CWD turned into a
|
|
124
|
+
start, and this is the single place that does so. Before #125 each such
|
|
125
|
+
caller spelled this out as a bare ``Path(".wl")``, which resolves against
|
|
126
|
+
the process CWD: a ``wl`` run from a subdirectory then read the
|
|
127
|
+
subdirectory's ``.wl`` instead of the root's. Routing every one of them
|
|
128
|
+
through here is what makes the anchor the *root* the CWD belongs to.
|
|
129
|
+
|
|
130
|
+
The read is :func:`_start_directory` -- a deleted working directory falls
|
|
131
|
+
back to a relative ``.`` (#95), and the result is canonicalised so the anchor
|
|
132
|
+
matches the resolved root -- and the "no root found" policy is
|
|
133
|
+
:func:`resolve_root_or_start`'s, not a private copy. The returned value is
|
|
134
|
+
always a :class:`~wl_cli.utils.root.Root`: callers wanting the root itself
|
|
135
|
+
take ``.path``, those wanting its control directory take ``.wl_dir()``.
|
|
136
|
+
"""
|
|
137
|
+
return resolve_root_or_start(_start_directory(), kind)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def current_worktree() -> Root | None:
|
|
141
|
+
"""The ``WORKTREE`` root the process's current directory belongs to, or ``None``.
|
|
142
|
+
|
|
143
|
+
The hooks leg is the one consumer (#160): it must tell "inside a git work
|
|
144
|
+
tree" from "not inside one", because outside one there is no
|
|
145
|
+
``core.hooksPath`` to read and nothing git would run. :func:`current_root`
|
|
146
|
+
cannot serve that question -- its whole point is to *never* answer ``None``,
|
|
147
|
+
so applying its fallback here would turn "not a work tree" into "the CWD",
|
|
148
|
+
and ``hooks_path_is_stale`` would then report a stale path forever.
|
|
149
|
+
|
|
150
|
+
So this is the same start read and the same ``resolve`` call the deleted
|
|
151
|
+
per-module copies spelled out, exposed once: a public seam rather than a
|
|
152
|
+
cross-module import of :func:`_start_directory`, which no caller outside
|
|
153
|
+
this module should reach for.
|
|
154
|
+
"""
|
|
155
|
+
return resolve(_start_directory(), RootKind.WORKTREE)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _start_directory() -> Path:
|
|
159
|
+
"""The directory ``wl`` was invoked from, spelled the way the callers expect.
|
|
160
|
+
|
|
161
|
+
The only CWD read **on the root-resolution path** (#160). It is not the only
|
|
162
|
+
CWD read in the tree: :func:`_legacy_wl_dir` below deliberately keeps its own
|
|
163
|
+
raw read (see its docstring), and unrelated subsystems read the CWD for their
|
|
164
|
+
own purposes. What matters here is that ``resolve`` requires an explicit
|
|
165
|
+
starting point and never reads an implicit CWD (ADR 0006), so every root
|
|
166
|
+
policy in this module needs this one value handed to it rather than each
|
|
167
|
+
caller reading it for itself -- the four byte-identical ``_start_directory``
|
|
168
|
+
copies #160 deleted.
|
|
169
|
+
|
|
170
|
+
Spelled *resolved*: the callers being migrated resolved their start before
|
|
171
|
+
passing it to the root module, which keeps their fallbacks byte-identical to
|
|
172
|
+
the answers the old resolvers gave. A deleted working directory falls back
|
|
173
|
+
to a relative ``.`` (the #95 shape); an unresolvable one keeps its verbatim
|
|
174
|
+
spelling.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
start = Path.cwd()
|
|
178
|
+
except OSError:
|
|
179
|
+
# A deleted working directory -- the #95 shape. The old resolvers
|
|
180
|
+
# bottomed out at a relative ``.``; keep that as the last resort.
|
|
181
|
+
return Path(".")
|
|
182
|
+
try:
|
|
183
|
+
return start.resolve()
|
|
184
|
+
except Exception:
|
|
185
|
+
return start
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def current_context() -> RootContext:
|
|
189
|
+
"""The three roots the process's current directory is anchored on (#160).
|
|
190
|
+
|
|
191
|
+
The public-entry half of :class:`RootContext`: a caller that needs more than
|
|
192
|
+
one root (the ``.wl`` self-sync) resolves this **once** and threads the
|
|
193
|
+
frozen value down, instead of each layer re-deriving a root from the process
|
|
194
|
+
CWD. Single-root callers use :func:`current_root` and never build one.
|
|
195
|
+
|
|
196
|
+
``project`` and ``deploy`` go through :func:`resolve_root_or_start`, i.e. the
|
|
197
|
+
same CLI-top-level fallback policy as :func:`current_root`; the values are
|
|
198
|
+
therefore exactly what those callers resolved before #160.
|
|
199
|
+
|
|
200
|
+
``legacy_wl_dir`` keeps the historical spelling on purpose: the pre-#95/#96
|
|
201
|
+
walk has always been handed the **un-resolved** ``Path.cwd()`` (the F2
|
|
202
|
+
residual in ``wl_dir_updater``). Unifying that spelling with the resolved
|
|
203
|
+
start above is a separate ticket; ``resolve`` canonicalises internally
|
|
204
|
+
either way, so the only observable difference is the deleted-CWD state,
|
|
205
|
+
where the raw read raises where the resolved read would fall back to a
|
|
206
|
+
relative ``.`` (:func:`_legacy_wl_dir` catches it and answers ``None``).
|
|
207
|
+
There is no invocation directory to walk from then, so the context carries
|
|
208
|
+
``None`` -- which is also what the old chain concluded: both ``.wl`` anchors
|
|
209
|
+
live under a CWD that is gone, so its anchor check returned before the
|
|
210
|
+
legacy walk was ever reached.
|
|
211
|
+
"""
|
|
212
|
+
start = _start_directory()
|
|
213
|
+
return RootContext(
|
|
214
|
+
project=resolve_root_or_start(start, RootKind.PROJECT),
|
|
215
|
+
deploy=resolve_root_or_start(start, RootKind.DEPLOY),
|
|
216
|
+
legacy_wl_dir=_legacy_wl_dir(),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _legacy_wl_dir() -> Path | None:
|
|
221
|
+
"""The pre-#95/#96 ``.wl`` at the *invocation* directory, or ``None``.
|
|
222
|
+
|
|
223
|
+
Read with the raw, un-resolved ``Path.cwd()`` -- the spelling
|
|
224
|
+
``wl_dir_updater.check_and_update_wl_dir`` has always used (the F2 residual:
|
|
225
|
+
it must be resolved *before* the deploy ``.wl`` exists, or the walk resolves
|
|
226
|
+
to the deploy root itself and the migration is silently skipped).
|
|
227
|
+
|
|
228
|
+
A deleted working directory raises ``OSError`` on that raw read, and the
|
|
229
|
+
exception **is caught here and turned into ``None``**: this state has no
|
|
230
|
+
invocation directory to walk from, so there is nothing a legacy walk could
|
|
231
|
+
find. Catching it is load-bearing for the component-level callers that
|
|
232
|
+
drive :func:`current_context` or :meth:`ProjectStructureSetup.setup`
|
|
233
|
+
*directly* (component use, tests, embedding), where an escaping ``OSError``
|
|
234
|
+
would abort the call. No shipped ``wl`` CLI entry reaches this leg in a
|
|
235
|
+
deleted-CWD shell: ``wl init`` reads ``Path.cwd()`` earlier
|
|
236
|
+
(``EnvironmentInitializer.__init__`` /
|
|
237
|
+
``ProjectStructureInitializer.initialize``), and the per-command hot path
|
|
238
|
+
returns at its anchor early-return (``check_and_update_wl_dir``) before this
|
|
239
|
+
function is reached. The #95 fallback in :func:`_start_directory` must not
|
|
240
|
+
be used for this leg (it would walk a relative ``.`` instead of the
|
|
241
|
+
invocation directory); ``None`` is the answer.
|
|
242
|
+
``tests/test_root_context_160.py::test_deleted_cwd`` pins this leg, and
|
|
243
|
+
driving ``ProjectStructureSetup.setup`` through it is pinned by
|
|
244
|
+
``tests/test_project_structure_modules.py::
|
|
245
|
+
test_setup_in_a_deleted_cwd_does_not_raise_oserror``.
|
|
246
|
+
|
|
247
|
+
The bounds of the walk (volume-root ceiling, system-scope blacklist, the
|
|
248
|
+
``<home>/.wl`` refusal) are the root module's ``LEGACY`` kind (#118); this
|
|
249
|
+
reads no legacy directory of its own.
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
invocation_dir = Path.cwd()
|
|
253
|
+
except OSError:
|
|
254
|
+
return None
|
|
255
|
+
legacy_root = resolve(invocation_dir, RootKind.LEGACY)
|
|
256
|
+
return legacy_root.wl_dir() if legacy_root is not None else None
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Structured evidence for one ``wl test`` run -- the cross-process seam (#212).
|
|
2
|
+
|
|
3
|
+
``wl gate`` used to decide "did this run exercise the tree?" by regex-matching
|
|
4
|
+
the human-readable ``Cache usage: N/M files`` line in ``wl test``'s stdout
|
|
5
|
+
(``_CACHE_USAGE_RE`` / ``cached_result_count``). That made a *presentation*
|
|
6
|
+
string a decision input, with two concrete failures (ADR-0010):
|
|
7
|
+
|
|
8
|
+
* The line is suppressed whenever ``concise`` renders the summary, so the fact
|
|
9
|
+
simply disappears and the gate reads "0 hits".
|
|
10
|
+
* The all-cached run short-circuits before pytest, so it writes no junit report
|
|
11
|
+
-- and a cached run is exactly the state the gate must judge red.
|
|
12
|
+
|
|
13
|
+
This module carries the fact across the process boundary as a versioned value
|
|
14
|
+
instead: which files ran, how many results came from the cache, whether
|
|
15
|
+
``--no-cache`` was requested, whether the run was cacheable at all, its exit
|
|
16
|
+
code, and when it started/finished. It carries **facts only**; the verdict
|
|
17
|
+
("a cache hit is red") belongs to :mod:`wl_cli.commands.gate.gate_command`.
|
|
18
|
+
|
|
19
|
+
Reading is deliberately fail-closed: a missing, unreadable, corrupt or
|
|
20
|
+
unknown-version file is ``None`` -- never a half-parsed value. The gate turns
|
|
21
|
+
``None`` into red and names the path.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import math
|
|
28
|
+
import os
|
|
29
|
+
import tempfile
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
#: Bumped whenever the payload gains, loses, or renames a fact. A reader that
|
|
37
|
+
#: does not know the version returns ``None`` rather than guessing -- a stale
|
|
38
|
+
#: schema can silently mean something else (ADR-0010: "绝不半信").
|
|
39
|
+
SCHEMA_VERSION = 1
|
|
40
|
+
|
|
41
|
+
#: Every fact a reader must find. ``cache_blocked_reason`` is nullable but not
|
|
42
|
+
#: optional: an absent key and an explicit ``null`` mean different things to a
|
|
43
|
+
#: producer, so the reader requires the key and rejects the missing one.
|
|
44
|
+
_REQUIRED_FIELDS: tuple[str, ...] = (
|
|
45
|
+
"schema_version",
|
|
46
|
+
"executed_files",
|
|
47
|
+
"cache_hits",
|
|
48
|
+
"cache_total",
|
|
49
|
+
"no_cache_requested",
|
|
50
|
+
"cacheable",
|
|
51
|
+
"cache_blocked_reason",
|
|
52
|
+
"exit_code",
|
|
53
|
+
"started_at",
|
|
54
|
+
"finished_at",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, kw_only=True)
|
|
59
|
+
class TestRunEvidence:
|
|
60
|
+
"""The facts one ``wl test`` invocation produced.
|
|
61
|
+
|
|
62
|
+
``executed_files`` are spellings relative to the project root, in the order
|
|
63
|
+
the run observed them. They are a *record*, not the verdict: the gate only
|
|
64
|
+
refuses runs whose ``cache_hits`` is non-zero.
|
|
65
|
+
|
|
66
|
+
``no_cache_requested`` states what the caller asked for, so
|
|
67
|
+
"``--no-cache`` was requested but the runner still hit the cache" becomes a
|
|
68
|
+
fact instead of something the gate has to guess.
|
|
69
|
+
|
|
70
|
+
``cacheable`` / ``cache_blocked_reason`` describe whether the run was
|
|
71
|
+
eligible to write cache at all (an unsafe passthrough switch or
|
|
72
|
+
``PYTEST_ADDOPTS`` closes it). They are independent of
|
|
73
|
+
``no_cache_requested``: a run may be cacheable and still asked to bypass
|
|
74
|
+
the cache.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
#: Keep pytest from collecting this value class as a test case: its name
|
|
78
|
+
#: starts with ``Test`` and it has an ``__init__``. Same guard the CLI's
|
|
79
|
+
#: ``TestCommand`` uses (``commands/testcommands/test_commands.py``).
|
|
80
|
+
__test__ = False
|
|
81
|
+
|
|
82
|
+
executed_files: tuple[str, ...]
|
|
83
|
+
cache_hits: int
|
|
84
|
+
cache_total: int
|
|
85
|
+
no_cache_requested: bool
|
|
86
|
+
cacheable: bool
|
|
87
|
+
cache_blocked_reason: str | None
|
|
88
|
+
exit_code: int
|
|
89
|
+
started_at: float
|
|
90
|
+
finished_at: float
|
|
91
|
+
schema_version: int = SCHEMA_VERSION
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
"""The JSON payload, with an explicit shape (and field order)."""
|
|
95
|
+
return {
|
|
96
|
+
"schema_version": self.schema_version,
|
|
97
|
+
"executed_files": list(self.executed_files),
|
|
98
|
+
"cache_hits": self.cache_hits,
|
|
99
|
+
"cache_total": self.cache_total,
|
|
100
|
+
"no_cache_requested": self.no_cache_requested,
|
|
101
|
+
"cacheable": self.cacheable,
|
|
102
|
+
"cache_blocked_reason": self.cache_blocked_reason,
|
|
103
|
+
"exit_code": self.exit_code,
|
|
104
|
+
"started_at": self.started_at,
|
|
105
|
+
"finished_at": self.finished_at,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_dict(cls, raw: Any) -> TestRunEvidence | None:
|
|
110
|
+
"""Rebuild from a decoded payload, or ``None`` when anything is off.
|
|
111
|
+
|
|
112
|
+
Every required field must be present *and* of the right type, and the
|
|
113
|
+
schema version must be exactly :data:`SCHEMA_VERSION`. A payload that
|
|
114
|
+
merely looks close is rejected whole -- a half-trusted record is worse
|
|
115
|
+
than no record, because the gate would act on a guess.
|
|
116
|
+
"""
|
|
117
|
+
if not isinstance(raw, dict):
|
|
118
|
+
return None
|
|
119
|
+
if any(field not in raw for field in _REQUIRED_FIELDS):
|
|
120
|
+
return None
|
|
121
|
+
version = raw.get("schema_version")
|
|
122
|
+
# ``bool`` is a subclass of ``int``, so ``isinstance(True, int)`` is
|
|
123
|
+
# true; the explicit bool exclusion keeps a flag from masquerading as
|
|
124
|
+
# a version.
|
|
125
|
+
if not isinstance(version, int) or isinstance(version, bool):
|
|
126
|
+
return None
|
|
127
|
+
if version != SCHEMA_VERSION:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
executed_files = raw.get("executed_files")
|
|
131
|
+
if not isinstance(executed_files, list) or not all(
|
|
132
|
+
isinstance(entry, str) for entry in executed_files
|
|
133
|
+
):
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
cache_hits = raw.get("cache_hits")
|
|
137
|
+
cache_total = raw.get("cache_total")
|
|
138
|
+
exit_code = raw.get("exit_code")
|
|
139
|
+
if (
|
|
140
|
+
not isinstance(cache_hits, int)
|
|
141
|
+
or isinstance(cache_hits, bool)
|
|
142
|
+
or not isinstance(cache_total, int)
|
|
143
|
+
or isinstance(cache_total, bool)
|
|
144
|
+
or not isinstance(exit_code, int)
|
|
145
|
+
or isinstance(exit_code, bool)
|
|
146
|
+
):
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
no_cache_requested = raw.get("no_cache_requested")
|
|
150
|
+
cacheable = raw.get("cacheable")
|
|
151
|
+
if not isinstance(no_cache_requested, bool) or not isinstance(cacheable, bool):
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
cache_blocked_reason = raw.get("cache_blocked_reason")
|
|
155
|
+
if cache_blocked_reason is not None and not isinstance(
|
|
156
|
+
cache_blocked_reason, str
|
|
157
|
+
):
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
started_at = raw.get("started_at")
|
|
161
|
+
finished_at = raw.get("finished_at")
|
|
162
|
+
if (
|
|
163
|
+
not isinstance(started_at, (int, float))
|
|
164
|
+
or isinstance(started_at, bool)
|
|
165
|
+
or not isinstance(finished_at, (int, float))
|
|
166
|
+
or isinstance(finished_at, bool)
|
|
167
|
+
):
|
|
168
|
+
return None
|
|
169
|
+
# ``json`` accepts bare ``NaN`` / ``Infinity`` literals by default, and
|
|
170
|
+
# both are ``float`` -- so the type check above lets them through.
|
|
171
|
+
# Neither is an epoch second, so a non-finite timestamp is a corrupt
|
|
172
|
+
# record, judged like any other corrupt payload: whole record rejected
|
|
173
|
+
# (review N1).
|
|
174
|
+
if not math.isfinite(started_at) or not math.isfinite(finished_at):
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
return cls(
|
|
178
|
+
executed_files=tuple(executed_files),
|
|
179
|
+
cache_hits=cache_hits,
|
|
180
|
+
cache_total=cache_total,
|
|
181
|
+
no_cache_requested=no_cache_requested,
|
|
182
|
+
cacheable=cacheable,
|
|
183
|
+
cache_blocked_reason=cache_blocked_reason,
|
|
184
|
+
exit_code=exit_code,
|
|
185
|
+
started_at=float(started_at),
|
|
186
|
+
finished_at=float(finished_at),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def write(self, path: Path) -> None:
|
|
190
|
+
"""Write this record to *path* atomically.
|
|
191
|
+
|
|
192
|
+
The payload lands in a sibling temporary file and is moved into place
|
|
193
|
+
with :func:`os.replace`, so a concurrent reader observes either the old
|
|
194
|
+
file or the complete new one -- never a truncated JSON document. Missing
|
|
195
|
+
parent directories are created (the gate hands us a path under the
|
|
196
|
+
system temp directory, which always exists).
|
|
197
|
+
"""
|
|
198
|
+
parent = path.parent
|
|
199
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
payload = json.dumps(self.to_dict(), ensure_ascii=False) + "\n"
|
|
201
|
+
|
|
202
|
+
descriptor, temporary = tempfile.mkstemp(
|
|
203
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=str(parent)
|
|
204
|
+
)
|
|
205
|
+
try:
|
|
206
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
207
|
+
handle.write(payload)
|
|
208
|
+
handle.flush()
|
|
209
|
+
os.fsync(handle.fileno())
|
|
210
|
+
os.replace(temporary, path)
|
|
211
|
+
except BaseException:
|
|
212
|
+
try:
|
|
213
|
+
os.unlink(temporary)
|
|
214
|
+
except OSError:
|
|
215
|
+
pass
|
|
216
|
+
raise
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def read(cls, path: Path | None) -> TestRunEvidence | None:
|
|
220
|
+
"""Read the evidence at *path*, or ``None`` when it cannot be trusted.
|
|
221
|
+
|
|
222
|
+
Missing, unreadable, non-JSON, wrong-shape, wrong-type and
|
|
223
|
+
unknown-version payloads all collapse to ``None``: the gate's
|
|
224
|
+
fail-closed red. There is no partial result and no fallback.
|
|
225
|
+
"""
|
|
226
|
+
if path is None:
|
|
227
|
+
return None
|
|
228
|
+
try:
|
|
229
|
+
text = path.read_text(encoding="utf-8")
|
|
230
|
+
except (OSError, UnicodeDecodeError):
|
|
231
|
+
return None
|
|
232
|
+
try:
|
|
233
|
+
raw = json.loads(text)
|
|
234
|
+
except (ValueError, RecursionError):
|
|
235
|
+
return None
|
|
236
|
+
return cls.from_dict(raw)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured logging implementation for WL Commands.
|
|
3
|
+
|
|
4
|
+
This file maintains backward compatibility while delegating to the modularized
|
|
5
|
+
structured_logging package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import the modularized StructuredLogger from the structured_logging package
|
|
9
|
+
# Import datetime for backward compatibility with tests
|
|
10
|
+
import datetime as _datetime
|
|
11
|
+
|
|
12
|
+
from .structured_logging.core import StructuredLogger
|
|
13
|
+
|
|
14
|
+
# Re-export for backward compatibility
|
|
15
|
+
__all__ = ["StructuredLogger"]
|
|
16
|
+
|
|
17
|
+
# For backward compatibility, expose datetime
|
|
18
|
+
datetime = _datetime
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Structured logging module for WL Commands."""
|
|
2
|
+
|
|
3
|
+
# Export main components from submodules
|
|
4
|
+
from .config import LoggingConfig
|
|
5
|
+
from .core import StructuredLogger, StructuredLoggerCore
|
|
6
|
+
from .filtering import LogLevelChecker
|
|
7
|
+
from .filters import LogFilter
|
|
8
|
+
from .formatters import LogFormatter
|
|
9
|
+
from .handlers import LogHandler
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"StructuredLogger",
|
|
13
|
+
"StructuredLoggerCore",
|
|
14
|
+
"LoggingConfig",
|
|
15
|
+
"LogLevelChecker",
|
|
16
|
+
"LogFormatter",
|
|
17
|
+
"LogHandler",
|
|
18
|
+
"LogFilter",
|
|
19
|
+
]
|