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
wl_cli/utils/root.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""The single module that answers "which root does this starting point belong to".
|
|
2
|
+
|
|
3
|
+
This module is the single answer to "which root does this starting point belong
|
|
4
|
+
to" for the whole CLI. The six same-shaped resolvers it replaces -- a PROJECT
|
|
5
|
+
marker walk, the test subsystem's CWD-reading spelling, its anchored mirror, a
|
|
6
|
+
DEPLOY resolver, a git work-tree probe and the bounded LEGACY walk -- were
|
|
7
|
+
deleted outright in the hard cut (#123). No alias and no deprecated wrapper
|
|
8
|
+
survives: a search for any of those old spellings now finds only the decision
|
|
9
|
+
record and the changelog, never code.
|
|
10
|
+
|
|
11
|
+
Interface
|
|
12
|
+
---------
|
|
13
|
+
|
|
14
|
+
Exactly three public names:
|
|
15
|
+
|
|
16
|
+
* :class:`RootKind` -- *which* rule resolved the root, as a value;
|
|
17
|
+
* :class:`Root` -- the resolved root plus the derived paths below it;
|
|
18
|
+
* :func:`resolve` -- ``resolve(start, kind) -> Root | None``.
|
|
19
|
+
|
|
20
|
+
``start`` is **required**: the module never reads an implicit current working
|
|
21
|
+
directory. "Not found" is a *result* (``None``), not an exception and not a
|
|
22
|
+
silent fallback -- the module does no fallback of its own. Falling back to the
|
|
23
|
+
CWD when nothing is found is the *caller's* policy and belongs in exactly one
|
|
24
|
+
place at the CLI top level; see ``docs/adr/0006-root-resolution-layering.md``
|
|
25
|
+
for why that layering was chosen and which alternatives were rejected.
|
|
26
|
+
|
|
27
|
+
Deliberately not part of this module
|
|
28
|
+
------------------------------------
|
|
29
|
+
|
|
30
|
+
Two functions are sometimes grouped with the resolvers above by name only; both
|
|
31
|
+
answer a different question, and pulling either in would widen this interface
|
|
32
|
+
without deepening it:
|
|
33
|
+
|
|
34
|
+
* ``WorkspaceDetector._find_workspace_root`` answers "is this starting point a
|
|
35
|
+
member of a uv workspace" -- a *predicate*, not a locator. A predicate has no
|
|
36
|
+
business returning a :class:`Root`.
|
|
37
|
+
* ``VersionUpdater._resolve_project_root`` resolves **wl's own installation
|
|
38
|
+
location** (including a ``Path(__file__)`` fallback). That is a question about
|
|
39
|
+
the tool, not about the user's root, and it moves with the installation rather
|
|
40
|
+
than with the directory the user ran from.
|
|
41
|
+
|
|
42
|
+
Internals
|
|
43
|
+
---------
|
|
44
|
+
|
|
45
|
+
* **One cache.** Keyed by ``(starting directory, resolution rule)`` and holding
|
|
46
|
+
the negative ("not found") answer too, so a directory with no root is not
|
|
47
|
+
re-walked on every ``wl`` invocation. There is a single invalidation entry
|
|
48
|
+
point, :func:`_clear_root_cache`, and it has a real production caller
|
|
49
|
+
(``GitInitializer.initialize``, right after ``git init``); before this ticket
|
|
50
|
+
two per-resolver caches shared the job, one of them with a clearer no
|
|
51
|
+
production code ever called.
|
|
52
|
+
* **git is an internal seam.** :class:`_GitProbe` is a protocol and
|
|
53
|
+
:data:`_git_probe` is the default subprocess implementation. Tests inject a
|
|
54
|
+
stand-in instead of monkey-patching ``subprocess`` globally -- which is what
|
|
55
|
+
used to force the git probe to carry a defensive "the stubbed result has no
|
|
56
|
+
usable ``stdout``" shape guard (#105). That guard is gone with the old probe
|
|
57
|
+
(#123): now that the seam is what tests drive, a completed process really is
|
|
58
|
+
one.
|
|
59
|
+
* Imports are underscore-aliased so that ``dir(module)`` -- the module's
|
|
60
|
+
namespace -- really is the three public names and nothing else.
|
|
61
|
+
|
|
62
|
+
The per-kind rules were lifted from the implementations this module replaces,
|
|
63
|
+
including their asymmetries. The comments keep the *reasoning* behind each
|
|
64
|
+
asymmetry, because those implementations are no longer here to explain
|
|
65
|
+
themselves.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
import enum as _enum
|
|
69
|
+
import os as _os
|
|
70
|
+
import subprocess as _subprocess
|
|
71
|
+
from dataclasses import dataclass as _dataclass
|
|
72
|
+
from pathlib import Path as _Path
|
|
73
|
+
from typing import Protocol as _Protocol
|
|
74
|
+
|
|
75
|
+
__all__ = ["Root", "RootKind", "resolve"]
|
|
76
|
+
|
|
77
|
+
_WL_DIR_NAME = ".wl"
|
|
78
|
+
_CONFIG_FILE_NAME = "config.json"
|
|
79
|
+
_PYPROJECT_NAME = "pyproject.toml"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RootKind(_enum.Enum):
|
|
83
|
+
"""Which rule resolved a root -- the input selector *and* the output label.
|
|
84
|
+
|
|
85
|
+
Kind is both input and output because each kind is resolved by a different
|
|
86
|
+
rule, and a caller that must branch on "what kind of root is this" would
|
|
87
|
+
otherwise have to remember which function name it called.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
#: The nearest ancestor carrying a ``.wl`` directory or a ``pyproject.toml``
|
|
91
|
+
#: file -- the plain PROJECT rule, with no git involvement.
|
|
92
|
+
PROJECT = "project"
|
|
93
|
+
#: git's own work-tree top (``git rev-parse --show-toplevel``).
|
|
94
|
+
WORKTREE = "worktree"
|
|
95
|
+
#: The root the ``.wl`` and the hooks leg are actually deployed under: the
|
|
96
|
+
#: git work-tree top when there is one, else the project root.
|
|
97
|
+
DEPLOY = "deploy"
|
|
98
|
+
#: An ancestor ``.wl`` in the pre-#95/#96 format that still has to be
|
|
99
|
+
#: migrated to the deploy root. Its boundaries are **completely different**
|
|
100
|
+
#: from :attr:`DEPLOY` -- a volume-root-bounded walk, a named system-scope
|
|
101
|
+
#: blacklist, and an explicit refusal of ``<home>/.wl`` -- which is why it
|
|
102
|
+
#: is its own value rather than a branch of ``DEPLOY``.
|
|
103
|
+
LEGACY = "legacy"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@_dataclass(frozen=True)
|
|
107
|
+
class Root:
|
|
108
|
+
"""A resolved root and the paths derived below it.
|
|
109
|
+
|
|
110
|
+
``path`` is the root directory itself; ``kind`` records which rule produced
|
|
111
|
+
it (see :class:`RootKind`). The derived-path methods are pure -- they build
|
|
112
|
+
a path from ``path`` and touch nothing on disk -- so they are testable
|
|
113
|
+
against a bare :class:`Root` without constructing a directory tree.
|
|
114
|
+
|
|
115
|
+
The methods are the only place the ``.wl`` layout is spelled out for the
|
|
116
|
+
roots this module owns; migrating the ~40 ``.wl`` literals and the bare
|
|
117
|
+
``src/`` checks onto them is a separate ticket.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
path: _Path
|
|
121
|
+
kind: RootKind
|
|
122
|
+
|
|
123
|
+
def wl_dir(self) -> _Path:
|
|
124
|
+
"""The ``.wl`` control directory at this root."""
|
|
125
|
+
return self.path / _WL_DIR_NAME
|
|
126
|
+
|
|
127
|
+
def mypy_ini(self) -> _Path:
|
|
128
|
+
"""The mypy baseline (``.wl/mypy.ini``) at this root."""
|
|
129
|
+
return self.wl_dir() / "mypy.ini"
|
|
130
|
+
|
|
131
|
+
def hooks_dir(self) -> _Path:
|
|
132
|
+
"""The deployed git hooks directory (``.wl/hooks``) at this root."""
|
|
133
|
+
return self.wl_dir() / "hooks"
|
|
134
|
+
|
|
135
|
+
def src_dir(self) -> _Path:
|
|
136
|
+
"""The source package directory (``src/``) at this root."""
|
|
137
|
+
return self.path / "src"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class _GitProbe(_Protocol):
|
|
141
|
+
"""The git seam: answer "what is the work-tree top for *start*".
|
|
142
|
+
|
|
143
|
+
Implementations return ``None`` for every "there is no work tree here"
|
|
144
|
+
shape -- git missing, a failed call, a directory that is not in any work
|
|
145
|
+
tree -- so no caller has to distinguish them.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def work_tree_root(self, start: _Path) -> _Path | None: # pragma: no cover
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _SubprocessGitProbe:
|
|
153
|
+
"""Default :class:`_GitProbe`: ``git rev-parse --show-toplevel``.
|
|
154
|
+
|
|
155
|
+
Returns ``None`` for every "there is no work tree here" shape -- git
|
|
156
|
+
missing, a failed call, a directory that is not part of any work tree -- so
|
|
157
|
+
no caller has to distinguish them.
|
|
158
|
+
|
|
159
|
+
Tests drive :data:`_git_probe` directly, so this reads a real
|
|
160
|
+
``CompletedProcess``. The old probe also guarded the *shape* of the result
|
|
161
|
+
because tests patched ``subprocess`` globally and could hand back one whose
|
|
162
|
+
``stdout`` was ``None`` or bytes (#105); with no such caller left, that
|
|
163
|
+
guard was deleted along with the probe it belonged to (#123).
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def work_tree_root(self, start: _Path) -> _Path | None:
|
|
167
|
+
try:
|
|
168
|
+
result = _subprocess.run(
|
|
169
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
170
|
+
cwd=start,
|
|
171
|
+
capture_output=True,
|
|
172
|
+
text=True,
|
|
173
|
+
check=False,
|
|
174
|
+
)
|
|
175
|
+
except (OSError, _subprocess.SubprocessError):
|
|
176
|
+
return None
|
|
177
|
+
if result.returncode != 0:
|
|
178
|
+
return None
|
|
179
|
+
value = result.stdout.strip()
|
|
180
|
+
return _Path(value) if value else None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
#: The single git seam. Tests swap this for a stand-in rather than patching
|
|
184
|
+
#: ``subprocess`` module-wide; production never touches it.
|
|
185
|
+
_git_probe: _GitProbe = _SubprocessGitProbe()
|
|
186
|
+
|
|
187
|
+
#: The one cache: ``(resolved starting directory, kind) -> Root | None``.
|
|
188
|
+
#:
|
|
189
|
+
#: Keyed the way the old caches were individually keyed -- by the *resolved*
|
|
190
|
+
#: spelling of the directory the lookup ran from -- so that two spellings of the
|
|
191
|
+
#: same directory (a relative path, a symlinked ``/tmp``) still share an entry.
|
|
192
|
+
#: The negative answer is cached too: "no root from here" is precisely the case
|
|
193
|
+
#: that would otherwise re-walk the filesystem on every ``wl`` invocation.
|
|
194
|
+
_root_cache: dict[tuple[str, RootKind], Root | None] = {}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _clear_root_cache() -> None:
|
|
198
|
+
"""The one invalidation entry point for :data:`_root_cache`.
|
|
199
|
+
|
|
200
|
+
A layout change made *under an already-consulted directory* -- ``wl init``'s
|
|
201
|
+
``git init`` is the production example -- can move the answer, and the memo
|
|
202
|
+
is per-directory, so the change has to be announced. This replaces the two
|
|
203
|
+
per-resolver caches and their separate clearers; the previous generation had
|
|
204
|
+
a clearer that no production code ever called, i.e. a cache that never
|
|
205
|
+
expired.
|
|
206
|
+
|
|
207
|
+
The production caller is ``GitInitializer.initialize``, right after a
|
|
208
|
+
successful ``git init``: the ``wl init`` group callback resolves the deploy
|
|
209
|
+
root *before* the subcommand runs, so the pre-creation answer is already
|
|
210
|
+
memoised for that directory and creating the work tree moves it. The
|
|
211
|
+
test-side entry point is the single ``_cold_root_cache`` fixture in
|
|
212
|
+
``tests/conftest.py`` (#115).
|
|
213
|
+
|
|
214
|
+
It is underscore-prefixed on purpose: the public face is the three names
|
|
215
|
+
above, so this stays an internal seam.
|
|
216
|
+
"""
|
|
217
|
+
_root_cache.clear()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def resolve(start: _Path, kind: RootKind) -> Root | None:
|
|
221
|
+
"""Resolve the ``kind``-root that *start* belongs to, or ``None``.
|
|
222
|
+
|
|
223
|
+
``start`` is required and is used exactly as given: the module never reads
|
|
224
|
+
an implicit current working directory, so a caller's own directory can never
|
|
225
|
+
leak into the answer (the failure an anchored mirror of the CWD-reading
|
|
226
|
+
spelling was written to avoid, #110). ``None`` means "no such root from
|
|
227
|
+
here" -- a result,
|
|
228
|
+
not an error; this module performs **no** fallback.
|
|
229
|
+
|
|
230
|
+
The result is memoised per ``(start, kind)``; see :func:`_clear_root_cache`
|
|
231
|
+
for the one way to invalidate it.
|
|
232
|
+
"""
|
|
233
|
+
resolved = _safe_resolve(start)
|
|
234
|
+
key = (str(resolved), kind)
|
|
235
|
+
if key in _root_cache:
|
|
236
|
+
return _root_cache[key]
|
|
237
|
+
result = _compute(resolved, kind)
|
|
238
|
+
_root_cache[key] = result
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _compute(start: _Path, kind: RootKind) -> Root | None:
|
|
243
|
+
"""The uncached body of :func:`resolve`; *start* is already resolved."""
|
|
244
|
+
if kind is RootKind.PROJECT:
|
|
245
|
+
found = _project_marker_root(start)
|
|
246
|
+
return Root(found, kind) if found is not None else None
|
|
247
|
+
if kind is RootKind.WORKTREE:
|
|
248
|
+
top = _git_probe.work_tree_root(start)
|
|
249
|
+
return Root(top, kind) if top is not None else None
|
|
250
|
+
if kind is RootKind.DEPLOY:
|
|
251
|
+
return _deploy(start)
|
|
252
|
+
if kind is RootKind.LEGACY:
|
|
253
|
+
return _legacy(start)
|
|
254
|
+
raise ValueError(f"unknown RootKind: {kind!r}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _deploy(start: _Path) -> Root | None:
|
|
258
|
+
"""``DEPLOY``: the git work-tree top, else the project root.
|
|
259
|
+
|
|
260
|
+
The rule is the one the deploy resolver this module replaces used -- "the
|
|
261
|
+
work-tree top when there is one, otherwise the project root" -- with the one
|
|
262
|
+
difference that is the whole point of this module: when the git lookup
|
|
263
|
+
misses *and* no project marker exists, that resolver returned the starting
|
|
264
|
+
directory (its own CWD fallback); here that is ``None``, and the caller
|
|
265
|
+
decides whether to substitute anything.
|
|
266
|
+
"""
|
|
267
|
+
top = _git_probe.work_tree_root(start)
|
|
268
|
+
if top is not None:
|
|
269
|
+
return Root(top, RootKind.DEPLOY)
|
|
270
|
+
found = _project_marker_root(start)
|
|
271
|
+
return Root(found, RootKind.DEPLOY) if found is not None else None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _legacy(start: _Path) -> Root | None:
|
|
275
|
+
"""``LEGACY``: the nearest ``.wl`` still to be migrated, or ``None``.
|
|
276
|
+
|
|
277
|
+
``start`` is the *invocation* directory, matching the one place the legacy
|
|
278
|
+
production walk ran from (``check_and_update_wl_dir`` passes the invocation
|
|
279
|
+
CWD). The deploy anchor the walk goes above is derived
|
|
280
|
+
internally by :func:`_deploy_anchor`; deriving it here rather than making
|
|
281
|
+
``resolve`` take two arguments is what keeps the interface at one starting
|
|
282
|
+
point.
|
|
283
|
+
|
|
284
|
+
The returned :class:`Root` is the *parent* of the legacy ``.wl`` -- the root
|
|
285
|
+
whose ``.wl`` is the stale one -- so ``result.wl_dir()`` is the directory the
|
|
286
|
+
old walk returned.
|
|
287
|
+
|
|
288
|
+
Both shapes of that walk are reproduced:
|
|
289
|
+
|
|
290
|
+
* the invocation directory's project root, when it is not the deploy root
|
|
291
|
+
(a sub-package with its own ``pyproject.toml``, or a workspace whose
|
|
292
|
+
``.wl`` the walk finds above);
|
|
293
|
+
* an ancestor ``.wl`` above the deploy root, bounded by
|
|
294
|
+
:func:`_is_out_of_scope_ancestor` and by the explicit ``<home>/.wl``
|
|
295
|
+
refusal.
|
|
296
|
+
|
|
297
|
+
The three bounds are copied from ``wl_dir_updater`` verbatim. They are *not*
|
|
298
|
+
``DEPLOY``'s rules: the walk is bounded by the volume's filesystem root and
|
|
299
|
+
its direct children (plus the named :data:`_SYSTEM_WL_SCOPES`), and
|
|
300
|
+
``<home>/.wl`` -- the *user-level* config, a different thing -- is refused by
|
|
301
|
+
realpath wherever the deploy root happens to sit (#104 N1).
|
|
302
|
+
"""
|
|
303
|
+
deploy_anchor = _deploy_anchor(start)
|
|
304
|
+
deploy_wl = deploy_anchor / _WL_DIR_NAME
|
|
305
|
+
|
|
306
|
+
project = _project_marker_root(start)
|
|
307
|
+
# The old project walk never returned None, so the project anchor was the
|
|
308
|
+
# marker root or, when there was none, the starting directory itself.
|
|
309
|
+
project_wl = (project if project is not None else start) / _WL_DIR_NAME
|
|
310
|
+
if not _same_path(project_wl, deploy_wl) and _is_file(
|
|
311
|
+
project_wl / _CONFIG_FILE_NAME
|
|
312
|
+
):
|
|
313
|
+
return Root(project_wl.parent, RootKind.LEGACY)
|
|
314
|
+
|
|
315
|
+
user_level_wl = [home / _WL_DIR_NAME for home in _user_home_dirs()]
|
|
316
|
+
ceiling = _walk_ceiling(deploy_anchor)
|
|
317
|
+
|
|
318
|
+
for ancestor in deploy_anchor.parents:
|
|
319
|
+
if _is_out_of_scope_ancestor(ancestor, ceiling):
|
|
320
|
+
# Ceiling / named system scope: stop, adopt nothing, and do not go
|
|
321
|
+
# higher -- nothing above them belongs to this project either.
|
|
322
|
+
break
|
|
323
|
+
candidate = ancestor / _WL_DIR_NAME
|
|
324
|
+
if any(_same_path(candidate, user_wl) for user_wl in user_level_wl):
|
|
325
|
+
break
|
|
326
|
+
if _same_path(candidate, deploy_wl):
|
|
327
|
+
continue
|
|
328
|
+
if _is_file(candidate / _CONFIG_FILE_NAME):
|
|
329
|
+
return Root(candidate.parent, RootKind.LEGACY)
|
|
330
|
+
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _deploy_anchor(start: _Path) -> _Path:
|
|
335
|
+
"""The deploy point, never ``None`` -- ``LEGACY``'s internal anchor.
|
|
336
|
+
|
|
337
|
+
``LEGACY`` walks *above* the deploy point, so it needs the anchor to exist
|
|
338
|
+
even when there is no git work tree and no project marker; the old deploy
|
|
339
|
+
resolver got that from its starting-directory fallback. Spelled out here as
|
|
340
|
+
an internal step so that :func:`resolve` can still answer ``None`` for the
|
|
341
|
+
``DEPLOY`` kind itself.
|
|
342
|
+
"""
|
|
343
|
+
top = _git_probe.work_tree_root(start)
|
|
344
|
+
if top is not None:
|
|
345
|
+
return top
|
|
346
|
+
found = _project_marker_root(start)
|
|
347
|
+
return found if found is not None else start
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _project_marker_root(start: _Path) -> _Path | None:
|
|
351
|
+
"""The ``PROJECT`` rule, step for step the one the old walk used.
|
|
352
|
+
|
|
353
|
+
That walk defined the markers but then papered over "not found" by returning
|
|
354
|
+
the starting path; here that outcome is ``None``.
|
|
355
|
+
|
|
356
|
+
A directory is a project root when it holds a ``.wl`` *directory* or a
|
|
357
|
+
``pyproject.toml`` *file* -- the shapes matter, so a stray ``.wl`` file or a
|
|
358
|
+
``pyproject.toml`` directory is not a marker. The nearest such ancestor wins,
|
|
359
|
+
and the walk stops before the filesystem root, which is never itself scanned.
|
|
360
|
+
|
|
361
|
+
The leading ``.wl`` check mirrors the original's ordering exactly, where the
|
|
362
|
+
starting directory was tested for ``.wl`` before the walk began. Since both
|
|
363
|
+
markers resolve to the *same* directory, that pre-check is redundant with the
|
|
364
|
+
walk's first iteration; it is kept only so the two rules stay line-by-line
|
|
365
|
+
comparable.
|
|
366
|
+
"""
|
|
367
|
+
if _is_dir(start / _WL_DIR_NAME):
|
|
368
|
+
return start
|
|
369
|
+
check = start
|
|
370
|
+
while check.parent != check:
|
|
371
|
+
if _is_file(check / _PYPROJECT_NAME):
|
|
372
|
+
return check
|
|
373
|
+
if _is_dir(check / _WL_DIR_NAME):
|
|
374
|
+
return check
|
|
375
|
+
check = check.parent
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _safe_resolve(path: _Path) -> _Path:
|
|
380
|
+
"""``Path.resolve()`` is not total -- a symlink loop raises ``RuntimeError``.
|
|
381
|
+
|
|
382
|
+
The un-resolved spelling is the fallback: the subsequent marker walk returns
|
|
383
|
+
the same answer either way, and the cache key stays usable.
|
|
384
|
+
"""
|
|
385
|
+
try:
|
|
386
|
+
return path.resolve()
|
|
387
|
+
except Exception:
|
|
388
|
+
return path
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _is_dir(path: _Path) -> bool:
|
|
392
|
+
"""``is_dir`` without letting an unreadable component raise."""
|
|
393
|
+
try:
|
|
394
|
+
return path.is_dir()
|
|
395
|
+
except OSError: # pragma: no cover - platform dependent
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _is_file(path: _Path) -> bool:
|
|
400
|
+
"""``is_file`` without letting an unreadable component raise."""
|
|
401
|
+
try:
|
|
402
|
+
return path.is_file()
|
|
403
|
+
except OSError: # pragma: no cover - platform dependent
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _same_path(a: _Path, b: _Path) -> bool:
|
|
408
|
+
"""Whether two paths point at the same directory (tolerating symlinks)."""
|
|
409
|
+
try:
|
|
410
|
+
return a.resolve() == b.resolve()
|
|
411
|
+
except Exception:
|
|
412
|
+
return a == b
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# --- LEGACY's bounds ---------------------------------------------------------
|
|
416
|
+
#
|
|
417
|
+
# These used to be duplicated: ``wl_dir_updater`` carried its own copy of the
|
|
418
|
+
# legacy walk and of these three bounds while this module was being expanded
|
|
419
|
+
# ahead of its call sites. The hard cut (#123) deleted that copy, so this is now
|
|
420
|
+
# the only one -- the bounds live with the rule that uses them.
|
|
421
|
+
|
|
422
|
+
#: Shared, machine-level scopes: a ``.wl`` sitting directly in one of these
|
|
423
|
+
#: belongs to the OS or to another user -- never to *this* checkout. Matched on
|
|
424
|
+
#: the realpath, because on macOS ``/tmp``, ``/var`` and ``/etc`` are symlinks
|
|
425
|
+
#: into ``/private`` while the deploy anchor is handed back resolved.
|
|
426
|
+
_SYSTEM_WL_SCOPES = frozenset(
|
|
427
|
+
{
|
|
428
|
+
"/tmp",
|
|
429
|
+
"/var",
|
|
430
|
+
"/etc",
|
|
431
|
+
"/usr",
|
|
432
|
+
"/opt",
|
|
433
|
+
"/srv",
|
|
434
|
+
"/bin",
|
|
435
|
+
"/sbin",
|
|
436
|
+
"/lib",
|
|
437
|
+
"/lib64",
|
|
438
|
+
"/boot",
|
|
439
|
+
"/dev",
|
|
440
|
+
"/proc",
|
|
441
|
+
"/sys",
|
|
442
|
+
"/run",
|
|
443
|
+
"/root",
|
|
444
|
+
"/home",
|
|
445
|
+
"/mnt",
|
|
446
|
+
"/media",
|
|
447
|
+
"/lost+found",
|
|
448
|
+
"/private",
|
|
449
|
+
"/System",
|
|
450
|
+
"/Library",
|
|
451
|
+
"/Volumes",
|
|
452
|
+
"/Applications",
|
|
453
|
+
"/app",
|
|
454
|
+
"/builds",
|
|
455
|
+
"/workspace",
|
|
456
|
+
"/workspaces",
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
#: :data:`_SYSTEM_WL_SCOPES` pre-resolved once, so the per-walk test is a set
|
|
461
|
+
#: lookup. Resolving a path that does not exist is harmless.
|
|
462
|
+
_RESOLVED_SYSTEM_WL_SCOPES = frozenset(_os.path.realpath(p) for p in _SYSTEM_WL_SCOPES)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _user_home_dirs() -> list[_Path]:
|
|
466
|
+
"""Every home directory the environment can vouch for (possibly none).
|
|
467
|
+
|
|
468
|
+
Deliberately *not* ``ConfigPathResolver.get_user_home_dir()``: when neither
|
|
469
|
+
``$HOME`` nor the account database yields a home that method falls back to
|
|
470
|
+
``os.getcwd()``, which would turn the caller's own directory into a bogus
|
|
471
|
+
"home" and silently disable every guard derived from it. Here an unknown
|
|
472
|
+
home is simply absent, and the walk ceiling still bounds the walk.
|
|
473
|
+
|
|
474
|
+
This distrust is warranted, not vestigial: the 2026-10-08 ruling kept that
|
|
475
|
+
caller-side CWD fallback in the config layer, and #126 ruled it stays there
|
|
476
|
+
(it answers the user's **home**, a different question from a project root, so
|
|
477
|
+
it is deliberately *not* folded into the single root-fallback policy) -- so
|
|
478
|
+
this module must go on deriving home from the environment alone.
|
|
479
|
+
"""
|
|
480
|
+
candidates: list[_Path] = []
|
|
481
|
+
for var in ("HOME", "USERPROFILE"):
|
|
482
|
+
raw = _os.environ.get(var)
|
|
483
|
+
if raw:
|
|
484
|
+
candidates.append(_Path(raw))
|
|
485
|
+
try:
|
|
486
|
+
candidates.append(_Path.home())
|
|
487
|
+
except (RuntimeError, OSError): # pragma: no cover - platform dependent
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
# A relative path here is not a home: ``Path.home()`` degrades to ``~`` when
|
|
491
|
+
# it cannot resolve, and an inherited ``HOME=~`` is nonsense. Sources
|
|
492
|
+
# routinely agree (``$HOME`` *is* what ``Path.home()`` reads), so dedupe.
|
|
493
|
+
return list(dict.fromkeys(home for home in candidates if home.is_absolute()))
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _walk_ceiling(deploy_root: _Path) -> _Path:
|
|
497
|
+
"""Absolute, HOME-independent ceiling of the legacy ancestor walk.
|
|
498
|
+
|
|
499
|
+
The filesystem root of ``deploy_root``'s volume -- the bound that does not
|
|
500
|
+
care where the checkout lives or whether ``$HOME`` is usable at all.
|
|
501
|
+
"""
|
|
502
|
+
anchor = _Path(_os.path.realpath(deploy_root)).anchor
|
|
503
|
+
return _Path(anchor) if anchor else _Path("/")
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _is_out_of_scope_ancestor(ancestor: _Path, ceiling: _Path) -> bool:
|
|
507
|
+
"""Whether ``ancestor`` may not be a legacy deploy point -- nor walked above.
|
|
508
|
+
|
|
509
|
+
Two absolute, HOME-independent rules:
|
|
510
|
+
|
|
511
|
+
* **position** -- the walk never inspects ``ceiling`` (the volume's
|
|
512
|
+
filesystem root) or any of its direct children;
|
|
513
|
+
* **the named** :data:`_SYSTEM_WL_SCOPES` -- matched on the realpath so the
|
|
514
|
+
symlinked spellings line up (macOS ``/tmp`` *is* ``/private/tmp``).
|
|
515
|
+
"""
|
|
516
|
+
if ancestor == ceiling or ancestor.parent == ceiling:
|
|
517
|
+
return True
|
|
518
|
+
resolved = _Path(_os.path.realpath(ancestor))
|
|
519
|
+
if resolved == ceiling or resolved.parent == ceiling:
|
|
520
|
+
return True
|
|
521
|
+
return str(resolved) in _RESOLVED_SYSTEM_WL_SCOPES
|