refactor(pm): pm owns the dependency-environment layout and interpreter paths

hermes_cli.runtime_paths (venv generations, selection, activation) moves to
pm.environments, and gains venv_bin_dir / venv_python / project_python. Every
in-tree caller asks pm for an interpreter now; pm no longer reaches back into
hermes_cli for its own environment layout (pm.packages, pm.extras, pm.ensure,
pm.paths imported hermes_cli.runtime_paths). The three open-coded
"Scripts/python.exe or bin/python" ladders in pm collapse onto venv_python.

hermes_constants.venv_python_path / venv_bin_dir and hermes_cli.runtime_paths
stay as frozen-updater-surface shims only (tests/compat/old_updater_surface.json).

To keep the boot path light, pm/__init__ resolves its facade lazily (PEP 562)
and pm.registry loads the built-in package definitions on first read instead of
at import: `import hermes_bootstrap` now loads pm + pm.environments only (25ms,
was 37ms with the eager facade dragging in the downloader). The stripped-payload
fixtures that ship only pre-import files keep working for the same reason.

Also restores two frozen-surface re-exports the F401 sweep dropped
(banner._github_compare_behind, cua_backend.resolve_cua_driver_cmd).
This commit is contained in:
ethernet committed 2026-09-18 20:02:36 -04:00
1 parent ad53be7826
commit bbec973514
101 files changed
+525 -493

No files matched your search

@@ -38,7 +38,7 @@ import json, os, re, shutil, subprocess, sys, tomllib, venv
from pathlib import Path
root, temp = map(Path, sys.argv[1:])
sys.path.insert(0, str(root))
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
from pm.environments import install_state_dir, runtime_facts_path, site_packages
seed = temp / 'seed'
venv.EnvBuilder(with_pip=False).create(seed)
dependencies = temp / 'dependencies'
@@ -57,7 +57,7 @@ def main() -> None:
from pm import paths
from pm.lock import Facts
from pm.store import current_target, tree_digest
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
from hermes_cli._launchers import ensure_install_launchers
worker = root / "pm" / "worker.py"
+1 -1
View File
@@ -131,7 +131,7 @@ def _windows_cron_python_invocation(python_exe: str) -> tuple[str, dict[str, str
interpreter = sibling
from hermes_cli._launchers import resolve_store_python
from hermes_cli.runtime_paths import selected_venv, site_packages as dependency_site
from pm.environments import selected_venv, site_packages as dependency_site
repo = Path(__file__).resolve().parents[1]
managed_python = resolve_store_python(repo)
+1 -1
View File
@@ -311,7 +311,7 @@ suppress_platform_ver_console()
# Every entry point imports this module before its dependency graph.
from pathlib import Path
from hermes_cli.runtime_paths import activate_dependencies
from pm.environments import activate_dependencies
from hermes_cli._early_recovery import recover_if_needed
from hermes_cli._parser import command_argv
+2 -2
View File
@@ -181,7 +181,7 @@ def recover_if_needed(project_root: Path | None = None, argv: list[str] | None =
args = command_argv(sys.argv[1:] if argv is None else argv)
if not explicit and args[:1] == ["pm"]:
return False # PM's command boundary owns the explicit repair.
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, selected_venv, site_packages
from pm.environments import install_state_dir, runtime_facts_path, selected_venv, site_packages
missing_marker = install_state_dir(root) / ".repair-incomplete"
marker_paths = (root / ".update-incomplete", root / ".lazy-refresh-incomplete", missing_marker)
@@ -249,7 +249,7 @@ _EARLY_CORE_INSTALL_MAX_ATTEMPTS = 3
def _claim_recovery_lock(root: Path) -> int | None:
"""Hold a kernel lock in writable state; process exit releases it."""
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from hermes_cli.runtime_state import _lock
state = install_state_dir(root)
+2 -1
View File
@@ -281,7 +281,8 @@ def migrate_windows_bin_path(
root = Path(root)
# Same per-machine anchor as ensure_windows_bin_launchers (see there).
from hermes_constants import get_default_hermes_root, venv_bin_dir
from hermes_constants import get_default_hermes_root
from pm.environments import venv_bin_dir
try:
home = Path(get_default_hermes_root())
+1 -1
View File
@@ -21,7 +21,7 @@ if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from hermes_constants import get_hermes_home
from hermes_cli.runtime_paths import dependency_home_root, store_root
from pm.environments import dependency_home_root, store_root
def runtime_command(repo_root: Path, args=(), *, module: str = "hermes_cli.main",
+2 -2
View File
@@ -21,7 +21,7 @@ def prepare(request: dict) -> tuple[Path, dict[str, str]]:
from pm.client import ensure, sync_venv, venv_is_current
from pm.lock import Lockfile
from pm.registry import source_install_packages
from hermes_cli.runtime_paths import activation_environment, install_state_dir, runtime_facts_path
from pm.environments import activation_environment, install_state_dir, runtime_facts_path
from hermes_cli._launchers import resolve_store_python
from hermes_cli.venv_sync import publish_launchers
@@ -68,7 +68,7 @@ def main() -> int:
# Historical atexit cleanup may run after the update's result is fixed.
# It must reuse that installation, never start a second update/repair.
from hermes_cli._launchers import resolve_store_python
from hermes_cli.runtime_paths import activation_environment
from pm.environments import activation_environment
root = Path(request["root"])
python = resolve_store_python(root)
+1
View File
@@ -9,6 +9,7 @@ import threading
from pathlib import Path
from hermes_cli import source_check
# Historical updater import (tests/compat/old_updater_surface.json). In-tree callers use the owner.
from hermes_cli.source_check import _github_compare_behind # noqa: F401
from hermes_constants import get_hermes_home
from typing import TYPE_CHECKING, Any, Dict, List, Optional
+1 -1
View File
@@ -8,7 +8,7 @@ an explicit post-update run). PM owns runtime diagnosis, not this record.
"""
from __future__ import annotations
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
import json
import logging
import os
+1 -1
View File
@@ -17,7 +17,7 @@ class DataRemovalPlan:
def plan_data_removal(home: Path, project: Path, userdata: Path | None = None) -> DataRemovalPlan:
from hermes_constants import get_default_hermes_root
from hermes_cli.runtime_paths import base_venv, installs_root, store_root
from pm.environments import base_venv, installs_root, store_root
from hermes_cli.steward import is_bundled_payload
from tools.checkpoint_pruning import store_lock_path
+2 -2
View File
@@ -464,7 +464,7 @@ def _check_python_environment(should_fix: bool, f: Finding) -> None:
# sys.path. Neither sys.prefix nor a stale PYTHONPATH proves activation.
staged = _staged_venv_dir()
if staged is not None:
from hermes_cli.runtime_paths import site_packages
from pm.environments import site_packages
selected_site = site_packages(staged).resolve()
running_here = selected_site.is_dir() and any(Path(entry).resolve() == selected_site for entry in sys.path)
@@ -537,7 +537,7 @@ def _check_command_installation(should_fix: bool, f: Finding) -> None:
f.manual_issues.append(_python_repair_hint())
return
from hermes_cli._launchers import resolve_store_python
from hermes_cli.runtime_paths import base_venv, selected_venv
from pm.environments import base_venv, selected_venv
try:
selected = selected_venv(PROJECT_ROOT)
+1 -1
View File
@@ -6811,7 +6811,7 @@ def _pm_runtime_venv_dir(project_root: Path | None = None) -> Path | None:
selection raises — fail closed, never a silently wrong venv.
"""
root = Path(project_root) if project_root is not None else PROJECT_ROOT
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
venv = selected_venv(root) # a malformed committed selection raises: fail closed
return venv if venv.is_dir() else None
+5 -5
View File
@@ -23,7 +23,7 @@ import sys
import tempfile
from pathlib import Path
from hermes_constants import venv_python_path
from pm.environments import venv_python
from utils import atomic_write_text
logger = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ def _is_uv_macos_store(path: str) -> bool:
def _venv_dir(project_root: Path | None = None) -> Path | None:
root = Path(project_root) if project_root is not None else Path(__file__).resolve().parents[1]
return next((root / n for n in ("venv", ".venv") if _present(venv_python_path(root / n))), None)
return next((root / n for n in ("venv", ".venv") if _present(venv_python(root / n))), None)
def _present(path: Path) -> bool:
@@ -85,7 +85,7 @@ def _interpreter_file(src: str | Path) -> Path | None:
def _interpreter_source(venv_dir: Path) -> str | None:
"""Return the interpreter file the venv currently resolves to (symlink target or pyvenv.cfg home)."""
venv_py = venv_python_path(venv_dir)
venv_py = venv_python(venv_dir)
if venv_py.is_symlink():
try:
return str(venv_py.resolve(strict=False))
@@ -113,7 +113,7 @@ def _managed_venv(project_root: Path | None) -> tuple[Path, Path, str] | str:
source = _interpreter_source(venv_dir)
if source is None or not _is_uv_macos_store(source):
return "interpreter not uv-managed (stable path)"
return venv_dir, venv_python_path(venv_dir), source
return venv_dir, venv_python(venv_dir), source
def _anchor_marker(venv_bin: Path) -> Path:
@@ -283,7 +283,7 @@ def _install_anchor(venv_dir: Path, source_file: Path) -> None:
"""Replace ``bin/python`` with a signed copy, gated on a real boot."""
from hermes_cli.macos_signing import sign_managed_python
venv_py = venv_python_path(venv_dir)
venv_py = venv_python(venv_dir)
venv_bin = venv_py.parent
venv_bin.mkdir(parents=True, exist_ok=True)
+2 -1
View File
@@ -93,7 +93,8 @@ def _is_windows() -> bool:
def _venv_scripts_dir() -> Path | None:
"""Return the venv Scripts directory if we're running inside the project venv."""
from hermes_cli.main import PROJECT_ROOT
from hermes_constants import project_venv_dir, venv_bin_dir
from hermes_constants import project_venv_dir
from pm.environments import venv_bin_dir
venv_dir = project_venv_dir(PROJECT_ROOT)
if venv_dir is None:
return None
+1 -1
View File
@@ -85,7 +85,7 @@ def prepare_memory_provider_dependencies(provider_name: str) -> tuple[dict, str
if not inputs:
return meta, None
pm.sync_venv(explicit=True, **inputs)
from hermes_cli.runtime_paths import selected_venv, site_packages
from pm.environments import selected_venv, site_packages
from pm.paths import repo_root
selected = site_packages(selected_venv(repo_root())).resolve()
+11 -218
View File
@@ -1,220 +1,13 @@
"""Dependency-environment paths shared by PM and pre-import launchers.
"""Frozen updater surface: pre-PM updaters import these names from here.
Only stdlib and hermes_constants: environment selection must work before
any dependency from that environment has been imported.
The implementation moved to ``pm.environments``; in-tree code imports it from there.
"""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
from hermes_constants import get_default_hermes_root, project_venv_dir
def install_key(project_root: Path) -> str:
canonical = str(Path(project_root).resolve())
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
def dependency_home_root() -> Path:
"""Scope dependency state like a process launched in the active home."""
from hermes_constants import get_default_hermes_root, get_hermes_home_override
override = get_hermes_home_override()
return get_default_hermes_root(home=override) if override else get_default_hermes_root()
def installs_root() -> Path:
return dependency_home_root() / "installs"
def install_state_dir(project_root: Path) -> Path:
return installs_root() / install_key(project_root)
def runtime_facts_path(project_root: Path) -> Path:
return install_state_dir(project_root) / "facts.json"
def base_venv(project_root: Path) -> Path:
root = Path(project_root).resolve()
manifest_path = root.parent / "manifest.json"
if manifest_path.is_file():
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
if (root.parent / manifest.get("repo", "")).resolve() == root:
venv = (root.parent / manifest["venv"]).resolve()
if not venv.is_relative_to(root.parent):
raise RuntimeError("payload environment escapes its root")
return venv
return project_venv_dir(root) or root / "venv"
def store_root(project_root: Path) -> Path:
"""Resolve a payload-relative or stamped store before PM imports."""
override = os.environ.get("HERMES_RUNTIME_DIR")
if override:
return Path(override).resolve()
root = Path(project_root).resolve()
manifest_path = root.parent / "manifest.json"
if manifest_path.is_file():
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
if (root.parent / manifest.get("repo", "")).resolve() == root:
store = (root.parent / manifest["store"]).resolve()
if not store.is_relative_to(root.parent):
raise RuntimeError("payload store escapes its root")
return store
for directory in (root, *root.parents):
stamp = directory / "install-stamp.json"
if stamp.is_file():
try:
data = json.loads(stamp.read_text(encoding="utf-8-sig"))
except (OSError, ValueError):
return get_default_hermes_root() / "tools"
value = data.get("runtimeDir") if isinstance(data, dict) else None
return Path(value).resolve() if value else get_default_hermes_root() / "tools"
return get_default_hermes_root() / "tools"
def selected_venv(project_root: Path) -> Path:
"""Use the committed environment, or the original install before first sync.
A broken committed selection is an error, not permission to load an older
dependency set silently. Reading this function never creates user state.
"""
path = runtime_facts_path(project_root)
try:
data = json.loads(path.read_text(encoding="utf-8-sig"))
except FileNotFoundError:
return base_venv(project_root)
except (OSError, ValueError) as exc:
raise RuntimeError(f"cannot read dependency environment: {path}") from exc
try:
fact = data.get("packages", {}).get("venv", {})
value = fact.get("environment")
except AttributeError as exc:
raise RuntimeError(f"invalid dependency environment record: {path}") from exc
if value is None:
return base_venv(project_root)
if not isinstance(value, str):
raise RuntimeError("invalid dependency environment path")
environment = Path(value).resolve()
generations = install_state_dir(project_root) / "environments"
if not environment.is_relative_to(generations.resolve()) or not (environment / "pyvenv.cfg").is_file():
raise RuntimeError(f"dependency environment is missing or outside this install: {environment}")
return environment
def venv_python_version(venv: Path) -> tuple[int, int] | None:
"""The interpreter version a POSIX venv actually holds, or ``None``.
``site_packages`` must not date the tree from the CALLER's ``sys.version_info``:
an update can rebuild the dependency environment with a different Python than
the launcher that later imports it. Observed on an app-driven upgrade -- PM
built the environment with CPython 3.14 while the PATH shim ran 3.11, so the
shim composed ``lib/python3.11/site-packages`` inside a 3.14 venv, found no
tree, and failed *after* a successful update.
"""
try:
for line in (venv / "pyvenv.cfg").read_text(encoding="utf-8").splitlines():
key, _, value = line.partition("=")
if key.strip() != "version":
continue
major, _, rest = value.strip().partition(".")
minor, _, _ = rest.partition(".")
if major.isdigit() and minor.isdigit():
return int(major), int(minor)
except OSError:
pass
try:
candidates = sorted((venv / "lib").glob("python3*"))
except OSError:
return None
for candidate in candidates:
major, _, rest = candidate.name.removeprefix("python").partition(".")
minor, _, _ = rest.partition(".")
if major.isdigit() and minor.isdigit():
return int(major), int(minor)
return None
def site_packages(venv: Path) -> Path:
import sys
if os.name == "nt":
return venv / "Lib/site-packages"
version = venv_python_version(venv) or (sys.version_info.major, sys.version_info.minor)
return venv / f"lib/python{version[0]}.{version[1]}/site-packages"
def activate_dependencies(project_root: Path) -> None:
"""Select the committed tree at process boot, before third-party imports.
A process with no extension selection keeps its original launch contract.
Already-running processes are never switched after a dependency install.
"""
import sys
state = install_state_dir(project_root)
if state.is_dir():
from hermes_cli.runtime_state import runtime_lock, recover_publication, lease_generation
# The lock's holder may be another profile's backend running a full dependency rebuild;
# this process only reads the committed selection, so it proceeds without waiting rather
# than leaving the backend unbound (see runtime_lock).
with runtime_lock(project_root) as held:
if held:
recover_publication(project_root)
environment = selected_venv(project_root)
lease_generation(environment)
selected = site_packages(environment)
if not selected.is_dir() and not runtime_facts_path(project_root).is_file():
return
else:
# Older installs and sealed payloads still select once, before imports.
# Never consult VIRTUAL_ENV: it can describe the invoking shell's Python.
environment = base_venv(project_root)
selected = site_packages(environment)
if not selected.is_dir():
return # External/Nix interpreter owns its original sys.path.
if not selected.is_dir():
raise RuntimeError(f"dependency environment has no site-packages: {selected}")
import site
sys.path[:] = [entry for entry in sys.path
if Path(entry).name not in ("site-packages", "dist-packages")
and Path(entry).resolve() != project_root.resolve()]
# uv editable members are activated by .pth files, not by sys.path alone.
site.addsitedir(str(selected))
sys.path[:] = [str(project_root.resolve()), str(selected),
*[entry for entry in sys.path if Path(entry).resolve() != selected.resolve()]]
os.environ["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
os.environ.pop("VIRTUAL_ENV", None)
executable_dir = environment / ("Scripts" if os.name == "nt" else "bin")
if executable_dir.is_dir():
os.environ["PATH"] = os.pathsep.join([str(executable_dir), os.environ.get("PATH", "")])
def activation_environment(project_root: Path) -> dict[str, str]:
"""Read the installed PM environment; do not provision or switch imports."""
from pm.ensure import env_for
from pm.registry import all_packages
env = env_for(*all_packages())
selected = site_packages(selected_venv(project_root))
env.pop("PYTHONHOME", None)
env.pop("VIRTUAL_ENV", None)
env["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
# The child-process sentinel. Its VALUE is the installed-state file this
# environment was composed against, so a consumer gets three things for
# free: that it inherited an activated shell, which checkout/profile that
# shell came from, and a staleness stamp — uv.lock / pyproject.toml /
# pm/lock.json newer than this file means the shell's environment predates
# its inputs. pm rewrites it on every real sync and no-ops otherwise, so a
# `-nt` comparison settles back to "current" after one re-activation.
env["__HERMES_ACTIVATED"] = str(runtime_facts_path(project_root))
return env
if __name__ == "__main__":
print(json.dumps(activation_environment(Path(__file__).resolve().parents[1])))
from pm.environments import ( # noqa: F401
activation_environment,
dependency_home_root,
install_state_dir,
runtime_facts_path,
selected_venv,
site_packages,
store_root,
)
+2 -2
View File
@@ -15,7 +15,7 @@ import tempfile
import time
import uuid
from hermes_cli.runtime_paths import dependency_home_root, install_state_dir, runtime_facts_path
from pm.environments import dependency_home_root, install_state_dir, runtime_facts_path
LOG = logging.getLogger(__name__)
@@ -205,7 +205,7 @@ def lease_generation(environment: Path) -> None:
def collect_generations(project: Path, *, min_age_seconds: float = 86400) -> list[Path]:
"""Remove unselected lease-managed generations after their readers exit."""
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
removed = []
root = install_state_dir(project)
if not root.exists():
+4 -5
View File
@@ -63,10 +63,9 @@ def complete_source_checkout(
def _bootstrap_command(root: Path, argv: list[str]) -> list[str]:
"""Re-enter the checkout on PM's selected interpreter with its environment."""
from hermes_cli.runtime_paths import selected_venv
from hermes_constants import venv_python_path
from pm.environments import project_python
return [str(venv_python_path(selected_venv(root))), "-I", "-B", "-u",
return [str(project_python(root)), "-I", "-B", "-u",
str(Path(__file__).resolve()), "--source", str(root), *argv, _PREPARED]
@@ -91,7 +90,7 @@ def main(argv: list[str] | None = None) -> int:
if prepared:
# Dependencies are selected before any application import: this is the
# same ordering the update completion guarantees.
from hermes_cli.runtime_paths import activate_dependencies
from pm.environments import activate_dependencies
activate_dependencies(root)
if args.finish_update:
@@ -111,7 +110,7 @@ def main(argv: list[str] | None = None) -> int:
)
return 0 if ok else 1
from hermes_cli.runtime_paths import activation_environment
from pm.environments import activation_environment
# This interpreter is a bootstrap one: the work happens in the re-exec, so
# every flag that decides WHICH tail runs has to survive into it. Losing
+1 -1
View File
@@ -31,7 +31,7 @@ installers and boot paths read it before the full config machinery loads.
from __future__ import annotations
from hermes_cli.runtime_paths import install_key, installs_root
from pm.environments import install_key, installs_root
from hermes_cli.release_channels import validate_name
from contextlib import contextmanager
import logging
+3 -4
View File
@@ -135,8 +135,7 @@ def _read_terminal_receipt(request: dict) -> dict | None:
def _prepare(request: dict, request_path: Path, result_path: Path) -> int:
import pm
from pm import receipt
from hermes_cli.runtime_paths import activation_environment, selected_venv
from hermes_constants import venv_python_path
from pm.environments import activation_environment, project_python
root = Path(request["source"])
update_id = request["receipt"]["update_id"]
@@ -146,7 +145,7 @@ def _prepare(request: dict, request_path: Path, result_path: Path) -> int:
finally:
request["pm_receipt"] = receipt.last_for_update(update_id)
_write_json(request_path, request)
command = [str(venv_python_path(selected_venv(root))),
command = [str(project_python(root)),
"-I", "-S", "-u", "-X", f"pycache_prefix={request['bytecode_cache']}",
str(root / "hermes_cli/update_completion.py"),
str(request_path), str(result_path), "--prepared"]
@@ -262,7 +261,7 @@ def main() -> int:
if "--prepared" in sys.argv[3:]:
# Claim the selected generation's lease and process its .pth files only
# after PM selection, before importing any application dependencies.
from hermes_cli.runtime_paths import activate_dependencies
from pm.environments import activate_dependencies
activate_dependencies(root)
return _finish(request, result_path)
try:
+3 -2
View File
@@ -54,11 +54,12 @@ def post_swap_python() -> Path:
if sys.platform != "win32":
return Path(sys.executable)
from hermes_cli._launchers import _is_windows
from hermes_constants import project_venv_dir, venv_python_path
from hermes_constants import project_venv_dir
from pm.environments import venv_python
venv_dir = project_venv_dir(Path(__file__).resolve().parents[1])
if venv_dir is not None and _is_windows():
candidate = venv_python_path(venv_dir, windows=True)
candidate = venv_python(venv_dir, windows=True)
if candidate.is_file():
return candidate
return Path(sys.executable)
+1 -1
View File
@@ -16,7 +16,7 @@ def main(context: Path, result: Path) -> int:
handled = True
return 0
sys.path.insert(0, str(root))
from hermes_cli.runtime_paths import activate_dependencies
from pm.environments import activate_dependencies
activate_dependencies(root)
from hermes_cli.dashboard_procs import _filter_dashboard_respawn_candidates
from hermes_cli.main_dashboard import _respawn_dashboard_processes
+1 -1
View File
@@ -156,7 +156,7 @@ def prepare_launch(project_root: Path, argv: list[str]) -> Path | None:
import pm
from hermes_cli._launchers import resolve_store_python
from hermes_cli.runtime_paths import activation_environment, runtime_facts_path
from pm.environments import activation_environment, runtime_facts_path
current = pm.venv_is_current(project_root=root)
if not current:
+7 -15
View File
@@ -973,19 +973,10 @@ AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1"
def venv_bin_dir(venv_dir, *, windows: bool | None = None) -> Path:
"""Venv executable dir (``Scripts``/``bin``); *windows* lets tests exercise Windows paths on Linux.
"""Frozen updater surface: pre-PM updaters import this name; pm.environments owns it."""
from pm.environments import venv_bin_dir as resolve
Returned unconditionally — callers differ on whether a missing venv is an error.
Canonical helper for venv layout. This was open-coded in seven places across four ``hermes_cli`` modules
using three different Windows predicates (``platform.system()``, ``is_windows()``, ``_is_windows()``);
each new call site had to re-derive it, and #76091 shipped an eighth copy because the correct behaviour
lived 2400 lines away in another function. A few sites outside ``hermes_cli``
(``tools/code_execution_tool.py``, ``agent/lsp/install.py``, ``agent/lsp/servers.py``) still hand-roll
it — convert them as they are touched.
"""
windows = sys.platform == "win32" if windows is None else windows
return Path(venv_dir) / ("Scripts" if windows else "bin")
return resolve(venv_dir, windows=windows)
def project_venv_dir(project_root) -> Path | None:
@@ -1001,9 +992,10 @@ def project_venv_dir(project_root) -> Path | None:
def venv_python_path(venv_dir, *, windows: bool | None = None) -> Path:
"""Path to the Python interpreter inside *venv_dir* (may not exist)."""
bin_dir = venv_bin_dir(venv_dir, windows=windows)
return bin_dir / ("python.exe" if bin_dir.name == "Scripts" else "python")
"""Frozen updater surface: pre-PM updaters import this name; pm.environments owns it."""
from pm.environments import venv_python
return venv_python(venv_dir, windows=windows)
# First-party roots: an ImportError naming one means our own tree is inconsistent. The
+41 -63
View File
@@ -9,70 +9,48 @@ file (facts.json, per install) says what is actually on this machine.
ensure(name) makes the installed state match the lockfile and returns a
Runner with the composed environment. env_for(*names) composes already-installed
packages' env without installing anything.
The facade resolves lazily (PEP 562): ``pm.environments`` runs at process boot,
before any dependency is importable, and must not drag the downloader/ensure
machinery in with it.
"""
from pm.ensure import (
activate,
check,
drift,
enabled_extras,
env_for,
is_installed,
installed_package,
lazy_installs_allowed,
)
from pm.client import (
ensure, sync_venv, build_environment, lock_project, stage_manager_runtime,
ensure_environment, ensure_python_tool, venv_is_current,
check_project_lock, export_requirements, build_requirements_environment, prune_cache, stage_tools, prepare_tools,
)
from pm.operations import environment_python, python_tool
from pm.extras import available, ensure_import
from pm.lock import Facts, Lockfile
from pm.package import InstallError, Package, Runner, compose_env
from pm.registry import all_packages, get_package, register, walk
from pm.store import Store, current_target
from __future__ import annotations
__all__ = [
"ensure",
"env_for",
"is_installed",
"installed_package",
"check",
"drift",
"activate",
"sync_venv",
"available",
"ensure_import",
"enabled_extras",
"lazy_installs_allowed",
"build_environment",
"lock_project",
"stage_manager_runtime",
"stage_tools",
"prepare_tools",
"ensure_environment",
"environment_python",
"ensure_python_tool",
"python_tool",
"venv_is_current",
"check_project_lock",
"export_requirements",
"build_requirements_environment",
"prune_cache",
"Facts",
"Lockfile",
"InstallError",
"Package",
"Runner",
"compose_env",
"all_packages",
"get_package",
"register",
"walk",
"Store",
"current_target",
]
import importlib
_EXPORTS = {
"pm.ensure": (
"activate", "check", "drift", "enabled_extras", "env_for", "is_installed",
"installed_package", "lazy_installs_allowed",
),
"pm.client": (
"ensure", "sync_venv", "build_environment", "lock_project", "stage_manager_runtime",
"ensure_environment", "ensure_python_tool", "venv_is_current", "check_project_lock",
"export_requirements", "build_requirements_environment", "prune_cache", "stage_tools",
"prepare_tools",
),
"pm.operations": ("environment_python", "python_tool"),
"pm.extras": ("available", "ensure_import"),
"pm.lock": ("Facts", "Lockfile"),
"pm.package": ("InstallError", "Package", "Runner", "compose_env"),
"pm.registry": ("all_packages", "get_package", "register", "walk"),
"pm.store": ("Store", "current_target"),
}
_HOME = {name: module for module, names in _EXPORTS.items() for name in names}
__all__ = list(_HOME)
def __getattr__(name: str):
module = _HOME.get(name)
if module is None:
raise AttributeError(f"module 'pm' has no attribute {name!r}")
value = getattr(importlib.import_module(module), name)
globals()[name] = value
return value
def __dir__():
return sorted(set(globals()) | set(_HOME))
import pm.packages # noqa: E402,F401 (registers the built-in definitions)
import pm.security_packages # noqa: E402,F401
+2 -2
View File
@@ -496,7 +496,7 @@ def env_for(*names: str, base_env: Optional[dict] = None) -> dict[str, str]:
def _runtime_state_matches(fact: dict, stamp: str, *, project_root: Path | None = None) -> bool:
if not isinstance(fact, dict) or fact.get("stamp") != stamp:
return False
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
try:
environment = selected_venv(paths.repo_root() if project_root is None else project_root)
@@ -511,7 +511,7 @@ def _runtime_state_matches(fact: dict, stamp: str, *, project_root: Path | None
def venv_is_current(*, extras: list[str] | None = None, plugin_dirs=None, extra_plugin_dirs=(),
project_root: Path | None = None) -> bool:
"""Probe the requested union without changing recorded dependency state."""
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
from pm.packages import Venv
root = paths.repo_root() if project_root is None else Path(project_root).absolute()
+3 -1
View File
@@ -172,7 +172,9 @@ class PythonEnvironment:
@property
def executable(self) -> Path:
return self.destination / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
from pm.environments import venv_python
return venv_python(self.destination)
def _run(self, args: list[str], *, cwd: Path, timeout: int) -> subprocess.CompletedProcess:
# Explicit index credentials survive, but cannot redirect the project,
+241
View File
@@ -0,0 +1,241 @@
"""Dependency-environment layout: where a project's venv generations live, which one
is selected, and the interpreter inside any venv. Shared by PM and pre-import launchers.
Only stdlib and hermes_constants: environment selection must work before
any dependency from that environment has been imported.
"""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
from hermes_constants import get_default_hermes_root, project_venv_dir
def install_key(project_root: Path) -> str:
canonical = str(Path(project_root).resolve())
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
def dependency_home_root() -> Path:
"""Scope dependency state like a process launched in the active home."""
from hermes_constants import get_default_hermes_root, get_hermes_home_override
override = get_hermes_home_override()
return get_default_hermes_root(home=override) if override else get_default_hermes_root()
def installs_root() -> Path:
return dependency_home_root() / "installs"
def install_state_dir(project_root: Path) -> Path:
return installs_root() / install_key(project_root)
def runtime_facts_path(project_root: Path) -> Path:
return install_state_dir(project_root) / "facts.json"
def base_venv(project_root: Path) -> Path:
root = Path(project_root).resolve()
manifest_path = root.parent / "manifest.json"
if manifest_path.is_file():
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
if (root.parent / manifest.get("repo", "")).resolve() == root:
venv = (root.parent / manifest["venv"]).resolve()
if not venv.is_relative_to(root.parent):
raise RuntimeError("payload environment escapes its root")
return venv
return project_venv_dir(root) or root / "venv"
def store_root(project_root: Path) -> Path:
"""Resolve a payload-relative or stamped store before PM imports."""
override = os.environ.get("HERMES_RUNTIME_DIR")
if override:
return Path(override).resolve()
root = Path(project_root).resolve()
manifest_path = root.parent / "manifest.json"
if manifest_path.is_file():
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
if (root.parent / manifest.get("repo", "")).resolve() == root:
store = (root.parent / manifest["store"]).resolve()
if not store.is_relative_to(root.parent):
raise RuntimeError("payload store escapes its root")
return store
for directory in (root, *root.parents):
stamp = directory / "install-stamp.json"
if stamp.is_file():
try:
data = json.loads(stamp.read_text(encoding="utf-8-sig"))
except (OSError, ValueError):
return get_default_hermes_root() / "tools"
value = data.get("runtimeDir") if isinstance(data, dict) else None
return Path(value).resolve() if value else get_default_hermes_root() / "tools"
return get_default_hermes_root() / "tools"
def selected_venv(project_root: Path) -> Path:
"""Use the committed environment, or the original install before first sync.
A broken committed selection is an error, not permission to load an older
dependency set silently. Reading this function never creates user state.
"""
path = runtime_facts_path(project_root)
try:
data = json.loads(path.read_text(encoding="utf-8-sig"))
except FileNotFoundError:
return base_venv(project_root)
except (OSError, ValueError) as exc:
raise RuntimeError(f"cannot read dependency environment: {path}") from exc
try:
fact = data.get("packages", {}).get("venv", {})
value = fact.get("environment")
except AttributeError as exc:
raise RuntimeError(f"invalid dependency environment record: {path}") from exc
if value is None:
return base_venv(project_root)
if not isinstance(value, str):
raise RuntimeError("invalid dependency environment path")
environment = Path(value).resolve()
generations = install_state_dir(project_root) / "environments"
if not environment.is_relative_to(generations.resolve()) or not (environment / "pyvenv.cfg").is_file():
raise RuntimeError(f"dependency environment is missing or outside this install: {environment}")
return environment
def venv_bin_dir(venv: Path, *, windows: bool | None = None) -> Path:
"""``Scripts`` on Windows, ``bin`` elsewhere. Returned unconditionally — callers
differ on whether a missing venv is an error. *windows* lets a POSIX process
reason about a Windows layout (update hand-off, launcher repair)."""
if windows is None:
windows = os.name == "nt"
return Path(venv) / ("Scripts" if windows else "bin")
def venv_python(venv: Path, *, windows: bool | None = None) -> Path:
"""The interpreter inside *venv* (may not exist)."""
bin_dir = venv_bin_dir(venv, windows=windows)
return bin_dir / ("python.exe" if bin_dir.name == "Scripts" else "python")
def project_python(project_root: Path) -> Path:
"""The interpreter of the committed dependency environment for *project_root*."""
return venv_python(selected_venv(project_root))
def venv_python_version(venv: Path) -> tuple[int, int] | None:
"""The interpreter version a POSIX venv actually holds, or ``None``.
``site_packages`` must not date the tree from the CALLER's ``sys.version_info``:
an update can rebuild the dependency environment with a different Python than
the launcher that later imports it. Observed on an app-driven upgrade -- PM
built the environment with CPython 3.14 while the PATH shim ran 3.11, so the
shim composed ``lib/python3.11/site-packages`` inside a 3.14 venv, found no
tree, and failed *after* a successful update.
"""
try:
for line in (venv / "pyvenv.cfg").read_text(encoding="utf-8").splitlines():
key, _, value = line.partition("=")
if key.strip() != "version":
continue
major, _, rest = value.strip().partition(".")
minor, _, _ = rest.partition(".")
if major.isdigit() and minor.isdigit():
return int(major), int(minor)
except OSError:
pass
try:
candidates = sorted((venv / "lib").glob("python3*"))
except OSError:
return None
for candidate in candidates:
major, _, rest = candidate.name.removeprefix("python").partition(".")
minor, _, _ = rest.partition(".")
if major.isdigit() and minor.isdigit():
return int(major), int(minor)
return None
def site_packages(venv: Path) -> Path:
import sys
if os.name == "nt":
return venv / "Lib/site-packages"
version = venv_python_version(venv) or (sys.version_info.major, sys.version_info.minor)
return venv / f"lib/python{version[0]}.{version[1]}/site-packages"
def activate_dependencies(project_root: Path) -> None:
"""Select the committed tree at process boot, before third-party imports.
A process with no extension selection keeps its original launch contract.
Already-running processes are never switched after a dependency install.
"""
import sys
state = install_state_dir(project_root)
if state.is_dir():
from hermes_cli.runtime_state import runtime_lock, recover_publication, lease_generation
# The lock's holder may be another profile's backend running a full dependency rebuild;
# this process only reads the committed selection, so it proceeds without waiting rather
# than leaving the backend unbound (see runtime_lock).
with runtime_lock(project_root) as held:
if held:
recover_publication(project_root)
environment = selected_venv(project_root)
lease_generation(environment)
selected = site_packages(environment)
if not selected.is_dir() and not runtime_facts_path(project_root).is_file():
return
else:
# Older installs and sealed payloads still select once, before imports.
# Never consult VIRTUAL_ENV: it can describe the invoking shell's Python.
environment = base_venv(project_root)
selected = site_packages(environment)
if not selected.is_dir():
return # External/Nix interpreter owns its original sys.path.
if not selected.is_dir():
raise RuntimeError(f"dependency environment has no site-packages: {selected}")
import site
sys.path[:] = [entry for entry in sys.path
if Path(entry).name not in ("site-packages", "dist-packages")
and Path(entry).resolve() != project_root.resolve()]
# uv editable members are activated by .pth files, not by sys.path alone.
site.addsitedir(str(selected))
sys.path[:] = [str(project_root.resolve()), str(selected),
*[entry for entry in sys.path if Path(entry).resolve() != selected.resolve()]]
os.environ["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
os.environ.pop("VIRTUAL_ENV", None)
executable_dir = venv_bin_dir(environment)
if executable_dir.is_dir():
os.environ["PATH"] = os.pathsep.join([str(executable_dir), os.environ.get("PATH", "")])
def activation_environment(project_root: Path) -> dict[str, str]:
"""Read the installed PM environment; do not provision or switch imports."""
from pm.ensure import env_for
from pm.registry import all_packages
env = env_for(*all_packages())
selected = site_packages(selected_venv(project_root))
env.pop("PYTHONHOME", None)
env.pop("VIRTUAL_ENV", None)
env["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
# The child-process sentinel. Its VALUE is the installed-state file this
# environment was composed against, so a consumer gets three things for
# free: that it inherited an activated shell, which checkout/profile that
# shell came from, and a staleness stamp — uv.lock / pyproject.toml /
# pm/lock.json newer than this file means the shell's environment predates
# its inputs. pm rewrites it on every real sync and no-ops otherwise, so a
# `-nt` comparison settles back to "current" after one re-activation.
env["__HERMES_ACTIVATED"] = str(runtime_facts_path(project_root))
return env
if __name__ == "__main__":
print(json.dumps(activation_environment(Path(__file__).resolve().parents[1])))
+1 -1
View File
@@ -178,7 +178,7 @@ def ensure_import(extra: str) -> None:
sync_venv([extra])
# Activation is a process-boot operation. Never mix a newly resolved
# dependency tree with libraries already imported by this process.
from hermes_cli.runtime_paths import selected_venv, site_packages
from pm.environments import selected_venv, site_packages
from pm.paths import repo_root, runtime_facts_path
import sys
from pathlib import Path
+3 -1
View File
@@ -120,7 +120,9 @@ def _environment_root(name: str, root: Path | None) -> Path:
def _python(environment: Path) -> Path:
return environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
from pm.environments import venv_python
return venv_python(environment)
def _selection(root: Path) -> dict:
+2 -2
View File
@@ -358,7 +358,7 @@ class Venv(StatePackage):
return repo_root() if self._project_root is None else self._project_root
def venv_dir(self) -> Path:
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
return selected_venv(self.project_root())
@@ -387,7 +387,7 @@ class Venv(StatePackage):
def apply(self, extras: list[str], *, plugin_dirs=None, repair: bool = False, explicit: bool = False) -> dict:
"""Prepare one complete environment; the caller commits its selection."""
import uuid
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path
from pm.environments import install_state_dir, runtime_facts_path
from pm.environment import managed_environment
from pm.lock import Facts
from pm.workspace import enabled_member_dirs, lock_and_sync
+2 -2
View File
@@ -14,7 +14,7 @@ def lockfile_path() -> Path:
def store_root() -> Path:
from hermes_cli.runtime_paths import store_root as resolve
from pm.environments import store_root as resolve
return resolve(repo_root())
@@ -47,6 +47,6 @@ def writable_store_root() -> Path:
def runtime_facts_path() -> Path:
from hermes_cli.runtime_paths import runtime_facts_path as resolve
from pm.environments import runtime_facts_path as resolve
return resolve(repo_root())
+2 -2
View File
@@ -11,7 +11,7 @@ from typing import Any, Optional
def _profiles_root() -> Path:
# Plugin discovery and dependency publication must use the same home root.
from hermes_cli.runtime_paths import dependency_home_root
from pm.environments import dependency_home_root
return dependency_home_root() / "profiles"
@@ -88,7 +88,7 @@ def _is_directory(path: Path) -> bool:
def _all_homes() -> list[Path]:
"""Enumerate the complete union or refuse; a partial scan cannot remove members."""
from hermes_cli.runtime_paths import dependency_home_root
from pm.environments import dependency_home_root
homes = [dependency_home_root()]
root = _profiles_root()
+1 -1
View File
@@ -11,7 +11,7 @@ import io
import json
from pathlib import Path
from hermes_cli.runtime_paths import dependency_home_root, install_state_dir, runtime_facts_path
from pm.environments import dependency_home_root, install_state_dir, runtime_facts_path
from hermes_cli.runtime_state import _atomic_bytes, _bytes, _digest
from pm.workspace import enabled_plugin_dirs, _is_member_candidate
+22 -5
View File
@@ -14,6 +14,20 @@ from pm.package import InstallError, Package
_packages: dict[str, Package] = {}
def _builtins_loaded() -> dict[str, Package]:
"""Register the built-in definitions on first read, not on ``import pm``.
Process boot imports ``pm.environments`` before any dependency is importable;
pulling the whole package catalogue (downloader, network) in with it would
cost every launch ~40ms and break the stripped payloads that ship only the
pre-import files.
"""
if "pm.packages" not in sys.modules:
import pm.packages # noqa: F401 (registers the built-in definitions)
import pm.security_packages # noqa: F401
return _packages
def register(cls):
instance = cls()
if not instance.name:
@@ -23,13 +37,14 @@ def register(cls):
def get_package(name: str) -> Package:
if name not in _packages:
packages = _builtins_loaded()
if name not in packages:
raise KeyError(f"unknown package: {name}")
return _packages[name]
return packages[name]
def all_packages() -> list[str]:
return sorted(_packages)
return sorted(_builtins_loaded())
def source_install_packages(names: list[str]) -> list[str]:
@@ -45,7 +60,7 @@ def package_definitions(names: list[str] | None = None) -> list[dict[str, Any]]:
source bodies, or the application's import path. ``names`` selects a closure.
"""
definitions = []
for package in walk(names) if names is not None else list(_packages.values()):
for package in walk(names) if names is not None else list(_builtins_loaded().values()):
cls = type(package)
if cls.__module__ == "pm.packages":
continue
@@ -122,7 +137,9 @@ def load_package_definitions(definitions: list[dict[str, Any]]) -> None:
definition["name"], f"cannot load package definition {module_name}.{definition['qualname']}: {exc}",
"keep the package definition and its dependencies importable in the isolated PM runtime",
) from exc
# Import-time decorators must not override the caller's final selection.
# Import-time decorators must not override the caller's final selection:
# load the built-ins first so a restored definition wins.
_builtins_loaded()
_packages.update(restored)
+4 -2
View File
@@ -31,7 +31,9 @@ def runtime_environment() -> dict[str, str]:
def _python(environment: Path) -> Path:
return environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
from pm.environments import venv_python
return venv_python(environment)
def _inputs(project: Path, python: Path) -> str:
@@ -147,7 +149,7 @@ def runtime_python(*, bootstrap: bool = True, cache: Path | None = None) -> Path
"""Resolve PM without selecting, repairing, or importing the app environment."""
if is_runtime():
return Path(sys.executable)
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from pm._uv import _toolchain
from pm.paths import repo_root
+2 -6
View File
@@ -105,13 +105,9 @@ def current_target() -> str:
def sha256_file(path: Path) -> str:
import hashlib
from pm.downloader import _sha256_file
digest = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
return _sha256_file(path)
def hash_url(url: str) -> str:
+1 -1
View File
@@ -35,7 +35,7 @@ free of both the exec bit and a ``PATH`` lookup.
Activation is not re-run when the inherited environment is still current: the
sentinel's value is the installed-state file the environment was composed
against (see ``hermes_cli.runtime_paths.activation_environment``), so the
against (see ``pm.environments.activation_environment``), so the
prologue compares it against the inputs that decide the dependency set. Any of
``uv.lock``, ``pyproject.toml`` or ``pm/lock.json`` being newer than that file
means the inherited environment predates its inputs, and the prologue
+1 -1
View File
@@ -182,7 +182,7 @@ def dependencies(args) -> None:
return
import tomllib
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
from pm import build_environment, check_project_lock, sync_venv
from pm.paths import repo_root
+1 -1
View File
@@ -182,7 +182,7 @@ class TestRunJobScript:
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(store))
# The supplied payload launcher is older than the committed extension
# generation. It must not override the installation's current selection.
from hermes_cli.runtime_paths import install_state_dir, site_packages as dependency_site
from pm.environments import install_state_dir, site_packages as dependency_site
repo = Path(sched_script.__file__).resolve().parents[1]
state = install_state_dir(repo)
selected = state / "environments" / "selected" / "venv"
+1 -1
View File
@@ -92,7 +92,7 @@ class PluginWorld:
plugins_command(parser.parse_args(argv))
def selected(self) -> Path:
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
return selected_venv(self.core)
def enabled(self) -> list[str]:
+1 -1
View File
@@ -41,7 +41,7 @@ def layout(tmp_path, monkeypatch):
@pytest.mark.parametrize("mode", ["confirmed", "cancel", "dry-run"])
def test_data_only_preserves_runtime_and_sibling_homes(layout, monkeypatch, mode):
import json
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, selected_venv
from pm.environments import install_state_dir, runtime_facts_path, selected_venv
monkeypatch.delattr(Path, "is_junction", raising=False)
home, witnesses, data = layout
@@ -10,7 +10,7 @@ import sys
import pytest
from hermes_cli import _launchers, doctor, doctor_platform
from hermes_cli.runtime_paths import install_state_dir, site_packages
from pm.environments import install_state_dir, site_packages
def _tree(tmp_path, monkeypatch):
@@ -46,7 +46,7 @@ def _pm_source(project, home):
root = Path(__file__).resolve().parents[2]
for relative in (
"hermes", "hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py",
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py",
"pm/environments.py", "hermes_cli/runtime_state.py",
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py",
"hermes_cli/venv_sync.py", "hermes_cli/steward.py", "hermes_cli/stderr_timestamp.py",
):
+1 -1
View File
@@ -90,7 +90,7 @@ def test_recovery_obeys_live_owner_and_single_flight(tmp_path, monkeypatch):
def test_missing_environment_cannot_write_a_retry_marker_without_lock(tmp_path, monkeypatch):
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path
from pm.environments import install_state_dir, runtime_facts_path
from pm.lock import Facts
root = _project(tmp_path)
+1 -1
View File
@@ -1286,7 +1286,7 @@ def _seed_pm_environment(tmp_path, monkeypatch, with_venv_fact=True):
facts.json whose venv fact names a committed environment generation under
the install state (pyvenv.cfg included). Returns
``(project_root, environment_dir)``."""
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
monkeypatch.setenv("HERMES_HOME", str(tmp_path)) # installs_root() under the test root
project_root = tmp_path / "payload" / "hermes-agent"
@@ -14,7 +14,7 @@ import sys
import pytest
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
from pm.environments import install_state_dir, runtime_facts_path, site_packages
_HOLDER = """
import sys
@@ -69,7 +69,7 @@ def test_runtime_lock_reports_a_lost_race(locked_install):
def test_boot_activation_proceeds_while_the_install_is_locked(locked_install, monkeypatch):
"""The issue's symptom, inverted: the backend reaches its dependency environment and can bind
while a sibling holds the lock. Recovery belongs to whoever holds it, so it is skipped."""
import hermes_cli.runtime_paths as runtime_paths
import pm.environments as runtime_paths
import hermes_cli.runtime_state as runtime_state
repo, site = locked_install
@@ -8,24 +8,27 @@ import sys
import pytest
from hermes_cli import _launchers
from hermes_cli.runtime_paths import install_state_dir, site_packages
from pm.environments import install_state_dir, site_packages
@pytest.mark.platforms("windows")
def test_minted_launcher_reads_current_selection_and_editable_members(tmp_path, monkeypatch):
from hermes_cli import runtime_paths, runtime_state
from pm import environments as runtime_paths, runtime_state
import hermes_constants
root = tmp_path / "repo"
package = root / "hermes_cli"
package.mkdir(parents=True)
(package / "__init__.py").write_text("")
pm_package = root / "pm"
pm_package.mkdir()
(pm_package / "__init__.py").write_text("")
# Real selection code, with a fixture entry point rather than a live CLI.
(package / "runtime_paths.py").write_bytes(Path(runtime_paths.__file__).read_bytes())
(pm_package / "environments.py").write_bytes(Path(runtime_paths.__file__).read_bytes())
(package / "runtime_state.py").write_bytes(Path(runtime_state.__file__).read_bytes())
(root / "hermes_constants.py").write_bytes(Path(hermes_constants.__file__).read_bytes())
(root / "hermes_bootstrap.py").write_text(
"from pathlib import Path\nfrom hermes_cli.runtime_paths import activate_dependencies\n"
"from pathlib import Path\nfrom pm.environments import activate_dependencies\n"
"activate_dependencies(Path(__file__).resolve().parent)\n"
)
(package / "main.py").write_text(
+11 -11
View File
@@ -26,7 +26,7 @@ import pytest
import hermes_cli.doctor as doctor
import hermes_cli.macos_tcc_anchor as tcc
from hermes_constants import venv_python_path
from pm.environments import venv_python
from hermes_cli import doctor_platform
@@ -128,7 +128,7 @@ class TestEnsureTccAnchorNonMacos:
# The host check gates before any filesystem access, so a plain
# regular-file interpreter fixture is enough: ensure must return None
# and leave the file byte-identical. Assert the fixture's actual path
# (venv_python_path is host-shaped: Scripts/python.exe on Windows).
# (venv_python is host-shaped: Scripts/python.exe on Windows).
venv_py = tmp_path / "checkout" / ".venv" / "bin" / "python"
venv_py.parent.mkdir(parents=True)
venv_py.write_bytes(b"#!fake interpreter")
@@ -170,7 +170,7 @@ class TestEnsureTccAnchor:
store_py.write_bytes(b"#!fake generation interpreter")
store_py.chmod(0o755)
root = _build_checkout(tmp_path, store_bin=store_bin)
venv_py = venv_python_path(root / ".venv")
venv_py = venv_python(root / ".venv")
assert venv_py.is_symlink()
anchored = tcc.ensure_tcc_anchor(root)
@@ -182,7 +182,7 @@ class TestEnsureTccAnchor:
def test_anchors_uv_managed_interpreter(self, tmp_path):
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
venv_py = venv_python_path(root / ".venv")
venv_py = venv_python(root / ".venv")
assert venv_py.is_symlink()
anchored = tcc.ensure_tcc_anchor(root)
@@ -202,7 +202,7 @@ class TestEnsureTccAnchor:
def test_idempotent(self, tmp_path):
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin, anchored=True)
venv_py = venv_python_path(root / ".venv")
venv_py = venv_python(root / ".venv")
marker = venv_py.parent / ".tcc-anchor-source"
before = marker.read_text(encoding="utf-8")
@@ -229,7 +229,7 @@ class TestEnsureTccAnchor:
def test_reanchors_after_patch_bump(self, tmp_path):
old_bin = _build_store(tmp_path, version="3.11.15")
root = _build_checkout(tmp_path, store_bin=old_bin, anchored=True)
venv_py = venv_python_path(root / ".venv")
venv_py = venv_python(root / ".venv")
new_bin = _build_store(tmp_path, version="3.11.16")
new_py = new_bin / "python3.11"
@@ -250,7 +250,7 @@ class TestEnsureTccAnchor:
def test_skips_homebrew_interpreter(self, tmp_path):
root = _build_checkout(tmp_path, homebrew=True)
venv_py = venv_python_path(root / ".venv")
venv_py = venv_python(root / ".venv")
assert tcc.ensure_tcc_anchor(root) is None
assert venv_py.is_symlink()
@@ -282,7 +282,7 @@ class TestEnsureTccAnchor:
def test_boot_gate_refusal_leaves_venv_untouched(self, tmp_path, monkeypatch):
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
venv_py = venv_python_path(root / ".venv")
venv_py = venv_python(root / ".venv")
monkeypatch.setattr(tcc, "_passes_boot_gate", lambda *a, **k: False)
assert tcc.ensure_tcc_anchor(root) is None
@@ -296,7 +296,7 @@ class TestEnsureTccAnchor:
# The next ensure retries the whole install.
store_bin = _build_store(tmp_path)
root = _build_checkout(tmp_path, store_bin=store_bin)
venv_py = venv_python_path(root / ".venv")
venv_py = venv_python(root / ".venv")
monkeypatch.setattr(tcc, "_copy_alias", lambda *a, **k: False)
import logging
@@ -477,7 +477,7 @@ class TestTccAnchorState:
status, detail = tcc.tcc_anchor_state(root)
assert status == "missing"
assert str(venv_python_path(root / ".venv")) in detail
assert str(venv_python(root / ".venv")) in detail
tcc.ensure_tcc_anchor(root)
@@ -498,7 +498,7 @@ class TestTccAnchorState:
status, _ = tcc.tcc_anchor_state(root)
assert status == "stale"
anchored = tcc.ensure_tcc_anchor(root)
assert anchored == venv_python_path(root / ".venv")
assert anchored == venv_python(root / ".venv")
assert (root / ".venv" / "bin" / "python").read_bytes() == (
new_bin / "python3.11"
).read_bytes()
@@ -12,7 +12,7 @@ import pytest
import hermes_yaml as yaml
from hermes_cli import memory_setup
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
from pm import paths
from tests.pm._fixtures import _wheel
@@ -317,7 +317,10 @@ def test_completed_serve_token_is_acknowledged_without_preparation(tmp_path, enc
# request without importing PM or launching any completed backend twice.
(package / "_launchers.py").write_text(
"import sys\nresolve_store_python = lambda root: sys.executable\n", encoding="utf-8")
(package / "runtime_paths.py").write_text(
pm_package = root / "pm"
pm_package.mkdir()
(pm_package / "__init__.py").write_text("", encoding="utf-8")
(pm_package / "environments.py").write_text(
"import os\nactivation_environment = lambda root: dict(os.environ)\n", encoding="utf-8")
context, result_path = tmp_path / "request.json", tmp_path / "result.json"
context.write_text(json.dumps({"root": str(root), "stopped_serves": {"pending": False}},
@@ -30,7 +30,7 @@ from pathlib import Path
import os,sys
from pm.publication import StagedPlugin
from pm.store import tree_digest
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
from pm.lock import Facts
project,staged,target = map(Path,sys.argv[1:4])
StagedPlugin({"staged": str(staged), "target": str(target), "target_digest": tree_digest(target),
@@ -76,7 +76,7 @@ def installed(admission_env, monkeypatch, request):
@pytest.mark.parametrize("failure", ["dependencies", "version", "publication", "manifest"])
def test_failed_update_keeps_code_metadata_config_and_environment(installed, monkeypatch, failure):
from hermes_cli import plugins_cmd
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
from pm import paths
from pm.lock import Facts
@@ -108,7 +108,7 @@ def test_failed_update_keeps_code_metadata_config_and_environment(installed, mon
def test_successful_update_publishes_matching_code_and_durable_workspace(installed):
import tomllib
from hermes_cli import plugins_cmd
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
from pm import paths
from pm.lock import Facts
from pm.packages import Venv
+1 -1
View File
@@ -5,7 +5,7 @@ import os
import pytest
from hermes_cli.runtime_paths import site_packages, store_root, venv_python_version
from pm.environments import site_packages, store_root, venv_python_version
from hermes_constants import get_default_hermes_root
@@ -10,12 +10,12 @@ import sys
import pytest
from hermes_cli import _launchers
from hermes_cli.runtime_paths import install_state_dir, site_packages
from pm.environments import install_state_dir, site_packages
ROOT = Path(__file__).resolve().parents[2]
BOOT_FILES = (
"hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py", "hermes_cli/_launchers.py",
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py",
"pm/environments.py", "hermes_cli/runtime_state.py",
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py",
"hermes_cli/venv_sync.py", "hermes_cli/steward.py",
"hermes_cli/stderr_timestamp.py",
@@ -20,8 +20,9 @@ def transition(tmp_path):
package = root / "hermes_cli"
package.mkdir()
(package / "__init__.py").write_text("")
(root / "pm").mkdir()
(root / "pm/__init__.py").write_text("OLD_API = True\n")
pm_package = root / "pm"
pm_package.mkdir()
(pm_package / "__init__.py").write_text("OLD_API = True\n")
def git(*args):
return subprocess.run(["git", *args], cwd=root, text=True, capture_output=True, check=True).stdout.strip()
@@ -52,21 +53,20 @@ def transition(tmp_path):
" with pathlib.Path('events.jsonl').open('a') as f:\n"
" f.write(json.dumps(dict(name=name, pid=os.getpid(), python=sys.executable, **values)) + '\\n')\n"
)
(package / "runtime_paths.py").write_text(
selected = tmp_path / "selected-python"
venv.EnvBuilder(with_pip=False).create(selected)
selected_python = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
(pm_package / "environments.py").write_text(
"import os, sys\n"
"from pathlib import Path\n"
"selected_venv = lambda root: Path(sys.executable).parent.parent\n"
f"project_python = lambda root: Path({str(selected_python)!r})\n"
"activation_environment = lambda root: {**os.environ, 'PYTHONPATH': str(root)}\n"
"def activate_dependencies(root):\n"
" from hermes_cli.probe import event\n"
" event('activate')\n"
)
selected = tmp_path / "selected-python"
venv.EnvBuilder(with_pip=False).create(selected)
selected_python = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
(root / "hermes_constants.py").write_text(
f"venv_python_path = lambda root: {str(selected_python)!r}\n"
)
(root / "hermes_constants.py").write_text("")
(package / "venv_sync.py").write_text(
"from hermes_cli.probe import event\n"
"publish_launchers = lambda root: event('launchers')\n"
+2 -2
View File
@@ -31,7 +31,7 @@ def _put(root, name, content):
@pytest.fixture
def completion(tmp_path, monkeypatch):
from hermes_cli.config_defaults import DEFAULT_CONFIG
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
from pm.environments import install_state_dir, runtime_facts_path, site_packages
home = tmp_path / "home"
home.mkdir()
@@ -215,7 +215,7 @@ def test_failure_preserves_original_receipt_before_build(completion, fault):
context.write_text(json.dumps(request), encoding="utf-8")
message = "application import failed before build"
if fault == "activation":
from hermes_cli.runtime_paths import selected_venv, site_packages
from pm.environments import selected_venv, site_packages
shutil.rmtree(site_packages(selected_venv(source)))
message = "dependency environment has no site-packages"
@@ -10,7 +10,7 @@ import sys
import pytest
from hermes_cli import venv_sync
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
def test_first_launch_syncs_without_marker_then_uses_completion_fact(tmp_path, monkeypatch):
@@ -15,8 +15,8 @@ import subprocess
import pytest
import pm
from hermes_cli.runtime_paths import activation_environment, runtime_facts_path, selected_venv, site_packages
from hermes_constants import venv_python_path
from pm.environments import activation_environment, runtime_facts_path, selected_venv, site_packages
from pm.environments import venv_python
from tests.pm._fixtures import isolated_python # noqa: F401
from tests.pm.test_source_update_launch import source_launch # noqa: F401
@@ -52,7 +52,7 @@ def configured_update(source_launch, tmp_path, monkeypatch):
f"source_build.build_update_products(Path({str(root)!r}), desktop=False)\n"
"print('TARGET=' + json.dumps({'prefix': sys.prefix, 'python': sys.executable}))\n"
)
subprocess.run([str(venv_python_path(selected)), "-c", script],
subprocess.run([str(venv_python(selected)), "-c", script],
cwd=root, env=activation_environment(root), check=True)
home = tmp_path / "home"
config = home / "config.yaml"
@@ -308,9 +308,9 @@ def test_zip_refuses_non_main_before_transport(zip_update, monkeypatch, capsys):
@pytest.mark.parametrize("windows,folder,executable", [(True, "Scripts", "python.exe"), (False, "bin", "python")])
def test_venv_layout_explicit_and_native(tmp_path, windows, folder, executable):
import os
from hermes_constants import venv_bin_dir, venv_python_path
from pm.environments import venv_bin_dir, venv_python
assert venv_bin_dir(tmp_path, windows=windows) == tmp_path / folder
assert venv_python_path(str(tmp_path), windows=windows) == tmp_path / folder / executable
assert venv_python(str(tmp_path), windows=windows) == tmp_path / folder / executable
if windows == (os.name == "nt"):
assert venv_python_path(tmp_path) == tmp_path / folder / executable
assert venv_python(tmp_path) == tmp_path / folder / executable
+1 -1
View File
@@ -8,7 +8,7 @@ import subprocess
import hermes_yaml as yaml
from hermes_cli import venv_sync
from hermes_cli.runtime_paths import install_state_dir, selected_venv
from pm.environments import install_state_dir, selected_venv
from pm import paths
from pm.lock import Lockfile
from tests.pm.test_plugin_survival_contract import admission_env # noqa: F401
@@ -15,8 +15,8 @@ from hermes_cli.web_routers import memory_providers as mp
@pytest.mark.parametrize("declaration", ["pyproject", "python_dependencies", "pip_dependencies"])
def test_setup_admits_real_provider_union_and_keeps_selection_on_failure(tmp_path, monkeypatch, surface, declaration):
import pm
from hermes_constants import venv_python_path
from hermes_cli.runtime_paths import selected_venv
from pm.environments import venv_python
from pm.environments import selected_venv
from tests.pm._fixtures import _wheel
from hermes_cli import memory_setup
from hermes_cli.web_server_memory import _memory_provider_setup_info
@@ -89,7 +89,7 @@ def test_setup_admits_real_provider_union_and_keeps_selection_on_failure(tmp_pat
success = prepare()
if surface == "dashboard":
assert success[0]["status"] == "restart_required", success
python = venv_python_path(selected_venv(core))
python = venv_python(selected_venv(core))
result = subprocess.run([str(python), "-I", "-c", "import existing_dep, provider_dep; print('both')"],
check=True, capture_output=True, text=True, timeout=30)
assert result.stdout.strip() == "both"
+1 -1
View File
@@ -57,7 +57,7 @@ def probe_pm(root: Path, desktop: str, command: list[str]) -> None:
# bootstrap: that would complete dependencies or recover markers for it.
sys.path.insert(0, str(root))
from hermes_cli._launchers import runtime_command
from hermes_cli.runtime_paths import selected_venv, site_packages
from pm.environments import selected_venv, site_packages
if command != runtime_command(root):
raise RuntimeError("published launcher belongs to another installation or Python")
+1 -1
View File
@@ -17,7 +17,7 @@ def publish_fixture_launcher(root: Path, main_source: str) -> Path:
# are real. The interpreter is external to the checkout, like PM's store.
(root / "hermes_bootstrap.py").write_text("", encoding="utf-8")
(root / "pm").mkdir(exist_ok=True)
for relative in ("hermes_constants.py", "hermes_cli/_launchers.py", "hermes_cli/runtime_paths.py"):
for relative in ("hermes_constants.py", "hermes_cli/_launchers.py", "pm/environments.py"):
shutil.copyfile(repository / relative, root / relative)
out = root / ".hermes" / "bin"
out.mkdir(parents=True)
@@ -24,9 +24,9 @@ def side_root(tmp_path, monkeypatch):
def test_ensure_sideenv_uses_pm_selected_python(side_root, monkeypatch):
import pm
from hermes_constants import venv_python_path
from pm.environments import venv_python
python = venv_python_path(side_root / "selected")
python = venv_python(side_root / "selected")
seen = {}
def ensure(name, requirements, **kwargs):
@@ -44,9 +44,9 @@ def test_ensure_sideenv_uses_pm_selected_python(side_root, monkeypatch):
def test_sideenv_python_is_pm_passive_selection(side_root, monkeypatch):
import pm
from hermes_constants import venv_python_path
from pm.environments import venv_python
python = venv_python_path(side_root / "selected")
python = venv_python(side_root / "selected")
seen = []
monkeypatch.setattr(pm, "environment_python",
lambda name, **kw: seen.append((name, kw)) or python)
@@ -61,8 +61,8 @@ def side_python(side_root, monkeypatch, tmp_path):
subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(side_root)],
check=True, capture_output=True, timeout=30)
from hermes_constants import venv_python_path
python = venv_python_path(side_root)
from pm.environments import venv_python
python = venv_python(side_root)
site = Path(subprocess.check_output(
[str(python), "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"],
text=True, timeout=10,
+1 -1
View File
@@ -156,7 +156,7 @@ def _isolated_checkout(tmp_path: Path) -> Path:
shutil.copytree(REPO_ROOT / "pm", root / "pm", ignore=shutil.ignore_patterns("__pycache__"))
(root / "hermes_cli").mkdir()
for relative in ("activate", "activate.ps1", "hermes_constants.py", "hermes_cli/__init__.py",
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py"):
"pm/environments.py", "hermes_cli/runtime_state.py"):
shutil.copy2(REPO_ROOT / relative, root / relative)
# Environment-only tests do not exercise provisioning; the runtime tests
# replace these stubs with a publisher that records and applies each sync.
+1 -1
View File
@@ -22,7 +22,7 @@ def _sync_checkout(tmp_path: Path):
# no-op and activation must still call it, rather than cache its own answer.
(root / "sync.py").write_text(textwrap.dedent('''\
import json, os, pathlib, shutil, sys
from hermes_cli.runtime_paths import runtime_facts_path, site_packages
from pm.environments import runtime_facts_path, site_packages
root = pathlib.Path(__file__).parent
record = {"argv": sys.argv[1:], "python_env": {
key: os.environ.get(key) for key in ("PYTHONHOME", "PYTHONPATH", "VIRTUAL_ENV")}}
+1 -1
View File
@@ -13,7 +13,7 @@ def test_minimal_bootstrap_closure_reaches_pm_paths_and_locks(tmp_path):
shutil.copytree(repo / "pm", stage / "pm", ignore=shutil.ignore_patterns("__pycache__"))
shutil.copy2(repo / "hermes_constants.py", stage / "hermes_constants.py")
(stage / "hermes_cli").mkdir()
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py"):
for name in ("__init__.py", "runtime_state.py"):
shutil.copy2(repo / "hermes_cli" / name, stage / "hermes_cli" / name)
store = stage / "tools"
env = dict(os.environ, HERMES_HOME=str(tmp_path / "home"),
+2 -2
View File
@@ -187,7 +187,7 @@ assert importlib.util.find_spec('idna') is None
report_code = """
import json
from pathlib import Path
from hermes_cli.runtime_paths import install_state_dir, selected_venv, runtime_facts_path
from pm.environments import install_state_dir, selected_venv, runtime_facts_path
root = Path(sys.path[0])
state = install_state_dir(root)
print(json.dumps({'state': str(state), 'app': str(selected_venv(root)),
@@ -225,7 +225,7 @@ print(json.dumps({{'yaml': ruamel.yaml.__file__, 'prefix': sys.prefix}}))
app_code = """
import json
from pathlib import Path
from hermes_cli.runtime_paths import activate_dependencies, selected_venv
from pm.environments import activate_dependencies, selected_venv
root = Path(sys.path[0])
activate_dependencies(root)
import ruamel.yaml, idna
+5 -5
View File
@@ -146,7 +146,7 @@ def test_public_build_installs_all_extras_at_explicit_destination(installable_pr
assert executable.parent.parent == tmp_path / "native environment"
assert not (tmp_path / "wrong-environment").exists()
assert "root_app" not in sys.modules
from hermes_cli.runtime_paths import site_packages
from pm.environments import site_packages
site = site_packages(executable.parent.parent)
assert (site / "_virtualenv.pth").exists() is not sealed
@@ -203,7 +203,7 @@ def test_group_only_build_excludes_application_dependencies(locked_project, tmp_
@pytest.mark.parametrize("lazy", [False, True])
def test_first_bundle_extension_preserves_shipped_extras(locked_project, build_worker, tmp_path, monkeypatch, lazy):
import pm
from hermes_cli.runtime_paths import selected_venv, runtime_facts_path
from pm.environments import selected_venv, runtime_facts_path
from pm import paths
from pm.features import write_features
from pm.lock import Facts
@@ -258,7 +258,7 @@ def test_first_bundle_extension_preserves_shipped_extras(locked_project, build_w
def test_worker_sync_reuses_unions_and_reports_real_lock_drift(locked_project, build_worker, tmp_path, monkeypatch):
import pm
from hermes_cli.runtime_paths import selected_venv, runtime_facts_path
from pm.environments import selected_venv, runtime_facts_path
from pm.lock import Facts, Lockfile
from pm import paths
@@ -689,7 +689,7 @@ def test_real_sync_retains_selection_until_commit(locked_project, tmp_path, monk
import pm.extras as extras
from pm import paths
from pm.lock import Facts
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
source, uv, env = locked_project
monkeypatch.setattr(paths, "repo_root", lambda: source)
@@ -729,7 +729,7 @@ def test_real_sync_retains_selection_until_commit(locked_project, tmp_path, monk
def test_live_apply_keeps_selection_on_failed_union(locked_project, tmp_path, monkeypatch):
from hermes_cli.runtime_paths import runtime_facts_path, selected_venv
from pm.environments import runtime_facts_path, selected_venv
from pm.lock import Facts
from pm.packages import Venv
import pm.paths
+1 -1
View File
@@ -10,7 +10,7 @@ import pytest
import pm.extras as extras
import pm.features as features
from hermes_cli.runtime_paths import site_packages
from pm.environments import site_packages
def test_inventory_uses_the_target_and_requires_every_anchor(tmp_path, monkeypatch):
+1 -1
View File
@@ -66,7 +66,7 @@ def test_sync_venv_allows_frozen_extras_when_lazy_off(rooted, monkeypatch):
import sys
from pm import paths
from pm.lock import Facts
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path
from pm.environments import install_state_dir, runtime_facts_path
ensure_mod = sys.modules["pm.ensure"]
+1 -1
View File
@@ -35,7 +35,7 @@ def test_state_fact_in_shared_facts_file_is_not_a_store_install(pm_env):
"""``venv`` is recorded via ``record_state`` (stamp + extras, no entry) into
the same facts.json as tool facts. Reading the composed env over every
registered package must skip it, not KeyError on ``fact["entry"]`` —
that crashed ``activate`` (``hermes_cli.runtime_paths``) after a sync."""
that crashed ``activate`` (``pm.environments``) after a sync."""
from pm.ensure import env_for
from pm.lock import Facts
from pm.registry import all_packages
+2 -2
View File
@@ -189,7 +189,7 @@ def test_conflicting_candidate_refused_unenabled_and_unimported(admission_env):
plug_a, plug_b, *_ = _local_conflict_members(home)
_write_enabled(home, [], provider="plug-a")
admission.admit_plugin_set_change(set(), set(), active_plugins_dir=home / "plugins")
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
working = selected_venv(tmp_path / "core")
config_before = (home / "config.yaml").read_bytes()
tree_before = {p: sorted(str(f) for f in p.rglob("*")) for p in (plug_a, plug_b)}
@@ -250,7 +250,7 @@ def test_conflicting_candidate_refused_unenabled_and_unimported(admission_env):
assert marker.read_bytes() == before
child = subprocess.run([str(sidecar_python), "-c", "import sys; print(sys.prefix)"], check=True, capture_output=True, text=True, timeout=30)
assert Path(child.stdout.strip()) == sidecar
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
selected = selected_venv(tmp_path / "core")
assert selected.is_dir() and selected != sidecar
# A declared version range remains a member across the next managed rebuild.
+3 -3
View File
@@ -29,7 +29,7 @@ def test_startup_validation_checks_real_ruamel_dependency(tmp_path, failure):
import ruamel.yaml
from hermes_cli.runtime_paths import site_packages
from pm.environments import site_packages
from pm.package import InstallError
from pm.recovery import validate_environment
@@ -86,7 +86,7 @@ def recovery_graph(tmp_path):
def test_repair_restores_recorded_plugin_dependencies_without_config(tmp_path, monkeypatch, recovery_graph, failure):
import pm.paths as paths
import pm.workspace as workspace
from hermes_cli.runtime_paths import selected_venv, site_packages
from pm.environments import selected_venv, site_packages
engine = importlib.import_module("pm.ensure")
uv = shutil.which("uv")
@@ -166,7 +166,7 @@ def test_repair_restores_recorded_plugin_dependencies_without_config(tmp_path, m
def test_uncertain_profile_selection_refuses_sync_but_not_recorded_repair(tmp_path, monkeypatch, recovery_graph):
import pm.paths as paths
from hermes_cli.plugins_admission import AdmissionRefused, admit_plugin_set_change
from hermes_cli.runtime_paths import install_state_dir, selected_venv, site_packages
from pm.environments import install_state_dir, selected_venv, site_packages
engine = importlib.import_module("pm.ensure")
# Use the same engine for admission and repair with the offline uv fixture.
+2 -2
View File
@@ -6,7 +6,7 @@ import sys
def test_bootstrap_lease_survives_selection_change(tmp_path, monkeypatch):
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
from pm.environments import install_state_dir, runtime_facts_path, site_packages
from hermes_cli.runtime_state import collect_generations
repo = tmp_path / "repo"
@@ -30,7 +30,7 @@ def test_bootstrap_lease_survives_selection_change(tmp_path, monkeypatch):
code = '''
import sys
from pathlib import Path
from hermes_cli.runtime_paths import activate_dependencies
from pm.environments import activate_dependencies
activate_dependencies(Path(sys.argv[1]))
print("ready", flush=True)
sys.stdin.readline()
+1 -1
View File
@@ -1,6 +1,6 @@
"""Context-only homes use the same dependency state as their own process."""
from hermes_cli import runtime_paths
from pm import environments as runtime_paths
from pm.publication import PluginSelection
from hermes_cli.runtime_state import recover_publication, runtime_lock
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
@@ -6,7 +6,7 @@ def test_recovery_refuses_to_replace_newer_config(tmp_path, monkeypatch):
from hermes_cli.runtime_state import recover_publication, runtime_lock
from pm.publication import PluginSelection
import pm.paths as paths
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
repo = tmp_path / "repo"
+1 -1
View File
@@ -3,7 +3,7 @@ import json
import pytest
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from hermes_cli.runtime_state import recover_publication, runtime_lock
+7 -7
View File
@@ -6,7 +6,7 @@ import pytest
def test_install_runtime_selection_is_scoped_and_read_only(tmp_path, monkeypatch):
from hermes_cli import runtime_paths
from pm import environments as runtime_paths
home = tmp_path / "home"
monkeypatch.setenv("HERMES_HOME", str(home))
@@ -33,7 +33,7 @@ def test_boot_uses_one_selected_dependency_tree_in_fresh_process(tmp_path, monke
import os
import subprocess
import sys
from hermes_cli import runtime_paths
from pm import environments as runtime_paths
root = tmp_path / "repo"
base = root / "venv"
@@ -53,7 +53,7 @@ def test_boot_uses_one_selected_dependency_tree_in_fresh_process(tmp_path, monke
"venv": {"environment": str(selected)}
}}))
code = (
"import sys; from pathlib import Path; from hermes_cli.runtime_paths import activate_dependencies; "
"import sys; from pathlib import Path; from pm.environments import activate_dependencies; "
"sys.path.insert(0, sys.argv[2]); activate_dependencies(Path(sys.argv[1])); "
"import probe_package, importlib.util; print(probe_package.version); "
"print(importlib.util.find_spec('base_only') is None)"
@@ -70,7 +70,7 @@ def test_broken_environment_keeps_explicit_repair_entry_reachable(tmp_path, monk
import os
import subprocess
import sys
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
repo = Path(__file__).resolve().parents[2]
@@ -90,7 +90,7 @@ def test_manual_repair_bypasses_damaged_generation_activation(tmp_path, monkeypa
import os
import subprocess
import sys
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
from pm.environments import install_state_dir, runtime_facts_path, site_packages
repo = Path(__file__).resolve().parents[2]
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
@@ -112,7 +112,7 @@ def test_manual_repair_bypasses_damaged_generation_activation(tmp_path, monkeypa
@pytest.mark.parametrize("data", [[], {"packages": []}, {"packages": {"venv": []}}])
def test_malformed_selection_has_actionable_error(tmp_path, monkeypatch, data):
from hermes_cli import runtime_paths
from pm import environments as runtime_paths
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
record = runtime_paths.runtime_facts_path(tmp_path / "repo")
record.parent.mkdir(parents=True)
@@ -123,7 +123,7 @@ def test_malformed_selection_has_actionable_error(tmp_path, monkeypatch, data):
@pytest.mark.parametrize("bad_path", ["outside", "missing"])
def test_invalid_selected_environment_never_silently_falls_back(tmp_path, monkeypatch, bad_path):
from hermes_cli import runtime_paths
from pm import environments as runtime_paths
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
root = tmp_path / "repo"
+3 -3
View File
@@ -19,7 +19,7 @@ import pytest
import pm
from hermes_cli import venv_sync
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, selected_venv, site_packages
from pm.environments import install_state_dir, runtime_facts_path, selected_venv, site_packages
from pm import paths
from pm.lock import Facts
from pm.package import InstallError
@@ -313,7 +313,7 @@ def test_real_bootstrap_reexecs_before_app_imports(source_launch, tmp_path, isol
"import hermes_bootstrap\n"
"import json, sys\n"
"from pathlib import Path\n"
"from hermes_cli.runtime_paths import selected_venv, site_packages\n"
"from pm.environments import selected_venv, site_packages\n"
"from hermes_cli.venv_sync import prepare_launch\n"
"root = Path(__file__).parent\n"
"selected = selected_venv(root)\n"
@@ -342,7 +342,7 @@ def test_real_bootstrap_reexecs_before_app_imports(source_launch, tmp_path, isol
activation_probe = subprocess.run(
[str(isolated_python), "-I", "-c",
f"import sys; sys.path.insert(0, {str(repository)!r}); "
"from pathlib import Path; from hermes_cli.runtime_paths import activate_dependencies; "
"from pathlib import Path; from pm.environments import activate_dependencies; "
f"activate_dependencies(Path({str(root)!r}))"],
capture_output=True, text=True, timeout=30,
)
+2 -2
View File
@@ -34,7 +34,7 @@ def isolated_machine_home(tmp_path, monkeypatch):
@pytest.mark.parametrize("marker_name", [".update-incomplete", ".lazy-refresh-incomplete", None, "manual", "baseline"])
def test_bootstrap_repairs_before_dependency_activation(tmp_path, monkeypatch, marker_name):
import pm.paths as paths
from hermes_cli.runtime_paths import selected_venv, site_packages
from pm.environments import selected_venv, site_packages
engine = importlib.import_module("pm.ensure")
repo = Path(__file__).resolve().parents[2]
@@ -49,7 +49,7 @@ def test_bootstrap_repairs_before_dependency_activation(tmp_path, monkeypatch, m
cli.mkdir()
# Include the real preimport protocol, including its ownership check. Do
# not stub prepare_launch: the same files are also saved in PM's workspace.
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py", "_early_recovery.py",
for name in ("__init__.py", "runtime_state.py", "_early_recovery.py",
"_parser.py", "venv_sync.py", "steward.py"):
shutil.copy2(repo / "hermes_cli" / name, cli / name)
wheels = tmp_path / "wheels"
+1 -1
View File
@@ -66,7 +66,7 @@ def test_fresh_takeover_prepares_generation_and_runs_selected_python(tmp_path):
"import json, sys\nfrom pathlib import Path\n"
"request=json.loads(Path(sys.argv[1]).read_text())\n"
"sys.path.insert(0,request['root'])\n"
"from hermes_cli.runtime_paths import activate_dependencies\n"
"from pm.environments import activate_dependencies\n"
"activate_dependencies(Path(request['root']))\nimport takeover_dep\n"
"assert request['pm_receipt']['update_id']==request['update_id']\n"
"Path(sys.argv[2]).write_text(json.dumps({'python':sys.executable,'dep':takeover_dep.__file__}))\n",
+6 -6
View File
@@ -49,7 +49,7 @@ def test_refused_or_already_paused_install_does_not_acquire_runtime(client, monk
def _current_environment(tmp_path, monkeypatch, members):
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from pm.lock import Facts
from pm.packages import Venv
@@ -71,7 +71,7 @@ def _current_environment(tmp_path, monkeypatch, members):
def test_currency_probe_preserves_union_and_candidate_inputs(client, tmp_path, monkeypatch, isolated_python,
member_shape, route):
import json
from hermes_cli.runtime_paths import runtime_facts_path, selected_venv
from pm.environments import runtime_facts_path, selected_venv
from pm.lock import Facts
from pm.packages import Venv
@@ -158,7 +158,7 @@ def test_currency_probe_preserves_union_and_candidate_inputs(client, tmp_path, m
def _assert_worker_holds_lock(repo):
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from hermes_cli.runtime_state import _lock
with (install_state_dir(repo) / ".install.lock").open("a+b") as lock:
@@ -237,7 +237,7 @@ def test_lazy_disabled_sync_does_not_bootstrap_tools(client, tmp_path, monkeypat
def test_invalid_selection_waits_for_failed_receipt_and_lock_release(client, tmp_path, monkeypatch):
import json
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from hermes_cli.runtime_state import _lock
repo = _current_environment(tmp_path, monkeypatch, [])
@@ -463,7 +463,7 @@ def test_resolution_conflict_survives_worker_and_receipt(client, tmp_path, monke
def test_failed_facts_write_restores_exact_config_before_reporting(client, tmp_path, monkeypatch, isolated_python):
from tests.pm._fixtures import worker_toolchain
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
repo = _current_environment(tmp_path, monkeypatch, [])
home = tmp_path / "home"
@@ -502,7 +502,7 @@ def test_worker_death_reports_transport_failure(client, monkeypatch, isolated_py
def test_foreign_checkout_sync_uses_its_own_pm_generation(client, tmp_path, monkeypatch, isolated_python):
from pm import venv_is_current
from hermes_cli.runtime_paths import selected_venv, runtime_facts_path
from pm.environments import selected_venv, runtime_facts_path
from tests.pm._fixtures import worker_toolchain
worker_toolchain(client, monkeypatch, isolated_python)
+7 -7
View File
@@ -15,7 +15,7 @@ from tests.pm._fixtures import worker_toolchain
def test_worker_publishes_selection_even_when_dependencies_are_current(client, tmp_path, monkeypatch):
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from pm import receipt
home = tmp_path / "home"
@@ -47,7 +47,7 @@ def test_worker_publishes_selection_even_when_dependencies_are_current(client, t
def test_staged_plugin_publication_uses_installed_identity_and_local_dependencies(
client, tmp_path, monkeypatch, isolated_python, active, missing,
):
from hermes_cli.runtime_paths import install_state_dir, selected_venv
from pm.environments import install_state_dir, selected_venv
from pm.store import tree_digest
from tests.pm.test_environment_build import _wheel
from pm import paths
@@ -105,7 +105,7 @@ def test_staged_plugin_publication_uses_installed_identity_and_local_dependencie
@pytest.mark.parametrize("mutation", ["sibling", "active"])
def test_selection_refuses_config_edits_during_preparation(client, tmp_path, monkeypatch, isolated_python, mutation):
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
home = tmp_path / "home"
home.mkdir()
config = home / "config.yaml"
@@ -178,7 +178,7 @@ def test_memory_setup_sends_candidate_paths_instead_of_discovery_callbacks(tmp_p
def test_worker_death_recovers_at_each_durable_publication_boundary(
client, tmp_path, monkeypatch, isolated_python, kind, rebuild, phase,
):
from hermes_cli.runtime_paths import install_state_dir, selected_venv
from pm.environments import install_state_dir, selected_venv
from pm import paths
from pm.package import InstallError
from pm.store import tree_digest
@@ -243,7 +243,7 @@ def test_worker_death_recovers_at_each_durable_publication_boundary(
assert (state / "publication.json").exists()
source = Path(client.__file__).resolve().parent.parent
program = (f"import sys; sys.path.insert(0, {str(source)!r}); from pathlib import Path; "
"from hermes_cli.runtime_paths import activate_dependencies; "
"from pm.environments import activate_dependencies; "
f"project = Path({str(project)!r})\n"
"activate_dependencies(project)\nactivate_dependencies(project)\n")
recovery = subprocess.run([sys.executable, "-I", "-S", "-c", program], capture_output=True, text=True,
@@ -266,7 +266,7 @@ def test_worker_death_recovers_at_each_durable_publication_boundary(
@pytest.mark.parametrize("mutation", ["metadata", "target", "staged", "sibling-manifest"])
def test_staged_publication_refuses_concurrent_input_edits(client, tmp_path, monkeypatch, isolated_python, mutation):
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
from pm.store import tree_digest
repo = _current_environment(tmp_path, monkeypatch, [])
home = tmp_path / "home"
@@ -342,7 +342,7 @@ def test_selection_preserves_yaml11_values_and_quotes_plugin_names(client, tmp_p
def test_explicit_publication_keeps_its_intent_through_tool_acquisition(client, tmp_path, monkeypatch, isolated_python):
from pm import paths
from hermes_cli.runtime_paths import selected_venv
from pm.environments import selected_venv
project = tmp_path / "project"
project.mkdir()
monkeypatch.setattr(paths, "repo_root", lambda: project)
+2 -2
View File
@@ -23,7 +23,7 @@ def test_bundle_stages_git_tree_and_runs_native_children_before_manifest(tmp_pat
import importlib
import inspect
from hermes_cli.runtime_paths import site_packages
from pm.environments import site_packages
from pm.lock import Facts
from pm.registry import get_package
from pm.store import tree_digest
@@ -55,7 +55,7 @@ def test_bundle_stages_git_tree_and_runs_native_children_before_manifest(tmp_pat
source = Path(__file__).resolve().parents[2]
shutil.copytree(source / "pm", repo / "pm", ignore=shutil.ignore_patterns("__pycache__"))
(repo / "hermes_cli").mkdir()
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py"):
for name in ("__init__.py", "runtime_state.py"):
shutil.copy2(source / "hermes_cli" / name, repo / "hermes_cli" / name)
shutil.copy2(source / "hermes_constants.py", repo / "hermes_constants.py")
wheels = repo / "wheels"
+1 -1
View File
@@ -15,7 +15,7 @@ def test_stamp_uses_built_commit_even_with_dispatch_sha_and_refuses_mismatch(tmp
repo.mkdir()
for relative in ('scripts/write_install_stamp.py', 'scripts/releases/commit_build.py',
'hermes_cli/__init__.py', 'hermes_cli/update_channel.py', 'hermes_cli/release_channels.py',
'hermes_cli/runtime_paths.py', 'hermes_cli/steward.py', 'hermes_constants.py'):
'pm/environments.py', 'hermes_cli/steward.py', 'hermes_constants.py'):
dest = repo / relative
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / relative, dest)
+1 -1
View File
@@ -139,7 +139,7 @@ def test_bootstrap_real_pm_resolves_only_build_owned_state(tmp_path):
probe = (
"import json, sys; from pathlib import Path; sys.path.insert(0, sys.argv[1]); "
"from pm import paths; from pm.packages import uv_cache_dir; "
"from hermes_cli.runtime_paths import install_state_dir; "
"from pm.environments import install_state_dir; "
"print(json.dumps([str(paths.store_root()), str(paths.partials_root()), "
"str(uv_cache_dir()), str(install_state_dir(Path(sys.argv[1])))]))"
)
+1 -1
View File
@@ -67,7 +67,7 @@ def payload_tree(tmp_path: Path):
# Exercise the real bootstrap before the fixture entry point.
for relative in ("hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py",
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py",
"pm/environments.py", "hermes_cli/runtime_state.py",
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py"):
shutil.copy2(_REPO / relative, repo / relative)
(repo / "hermes_cli" / "main.py").write_text(
+1 -1
View File
@@ -22,7 +22,7 @@ def _exercise_relocated_pm_runtime(tmp_path, monkeypatch):
source = Path(__file__).resolve().parents[2]
shutil.copytree(source / "pm", repo / "pm", ignore=shutil.ignore_patterns("__pycache__", ".hermes-tmp.*"))
(repo / "hermes_cli").mkdir()
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py"):
for name in ("__init__.py", "runtime_state.py"):
shutil.copy2(source / "hermes_cli" / name, repo / "hermes_cli" / name)
shutil.copy2(source / "hermes_constants.py", repo / "hermes_constants.py")
# Copy the base executable, not a venv's launcher. The test host provides
+1 -1
View File
@@ -35,7 +35,7 @@ def fixture_repo(tmp_path):
'scripts/releases/r2.py', 'scripts/releases/r2_scope.py', 'scripts/release-content-types.json',
'hermes_cli/__init__.py', 'hermes_cli/update_channel.py',
'hermes_cli/release_channels.py',
'hermes_cli/runtime_paths.py', 'hermes_constants.py'):
'pm/environments.py', 'hermes_constants.py'):
dest = repo / relative
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / relative, dest)
+1 -1
View File
@@ -56,7 +56,7 @@ def test_development_setup_keeps_test_groups_out_of_the_runtime(tmp_path, monkey
assert result.stdout.strip() == str("dev" in extras)
assert Path(outputs["venv"]).is_relative_to(home)
assert not (core / ".venv").exists()
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
assert runtime_facts_path(core).exists() == ("dev" not in extras)
+1 -1
View File
@@ -17,7 +17,7 @@ def _stamp_probe(tmp_path, shell):
repo = tmp_path / "installed source"
for relative in (
"scripts/write_install_stamp.py", "hermes_cli/__init__.py",
"hermes_cli/update_channel.py", "hermes_cli/runtime_paths.py",
"hermes_cli/update_channel.py", "pm/environments.py",
"hermes_cli/steward.py", "hermes_constants.py",
):
dest = repo / relative
+1 -1
View File
@@ -146,7 +146,7 @@ root, store, node, deps = map(Path, sys.argv[1:])
sys.path.insert(0, str(root))
from pm.lock import Facts, Lockfile
from pm.packages import Venv
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
from pm.environments import install_state_dir, runtime_facts_path, site_packages
from hermes_cli._launchers import ensure_install_launchers
lock = Lockfile(root / 'pm/lock.json')
lock.set_pin('node', 'fixture', {})
+2 -2
View File
@@ -64,7 +64,7 @@ def test_candidate_member_dirs_preserves_proposed_home_order_and_extras(isolated
@pytest.fixture
def publication(isolated_home, tmp_path):
from hermes_cli.runtime_paths import install_state_dir
from pm.environments import install_state_dir
project = tmp_path / "checkout"
project.mkdir()
@@ -115,7 +115,7 @@ plugins_transaction.recover_plugin_publication(
@pytest.mark.parametrize("commit", ["rollback", "explicit", "facts-changed"])
def test_old_publication_recovers_in_stdlib_using_supplied_row_and_journal(publication, commit):
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
project, row, journal, canonical = publication
target, backup, metadata = (Path(row[key]) for key in ("target", "backup", "metadata"))
+1 -1
View File
@@ -7,7 +7,7 @@ import subprocess
import pytest
from hermes_cli.runtime_paths import install_state_dir, site_packages
from pm.environments import install_state_dir, site_packages
from pm.lock import Lockfile
from tests.hermes_cli.test_source_launcher_publication import fixture_tree
+2 -2
View File
@@ -282,7 +282,7 @@ def test_launcher_alias_provenance(child_env, monkeypatch, link_at, profile):
@pytest.mark.parametrize("has_facts", [True, False])
def test_runtime_provenance_is_independent_of_aliases_and_virtual_env(child_env, monkeypatch, has_facts):
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
payload = child_env / "payload"
runtime = payload / "state/environments/candidate/venv"
site = runtime / ("Lib/site-packages" if os.name == "nt" else
@@ -291,7 +291,7 @@ def test_runtime_provenance_is_independent_of_aliases_and_virtual_env(child_env,
(runtime / "pyvenv.cfg").write_text("version = 3.14\n", encoding="utf-8")
(payload / "tools").mkdir()
(payload / "manifest.json").write_text("{}", encoding="utf-8")
monkeypatch.setattr("hermes_cli.runtime_paths.install_state_dir", lambda repo: payload / "state")
monkeypatch.setattr("pm.environments.install_state_dir", lambda repo: payload / "state")
if has_facts:
facts = runtime_facts_path(Path(__file__).resolve().parents[2])
facts.parent.mkdir(parents=True, exist_ok=True)
+1 -1
View File
@@ -123,7 +123,7 @@ def test_warm_sdk_uses_pm_availability_and_refuses_inactive_generation(tmp_path,
import pm.client
import pm.paths
import pm.extras
from hermes_cli.runtime_paths import runtime_facts_path
from pm.environments import runtime_facts_path
calls = []
root = tmp_path / "repo"
+3 -2
View File
@@ -21,8 +21,9 @@ from hermes_cli._subprocess_compat import windows_hide_flags
from tools.computer_use.backend import ActionResult, ComputerUseBackend
from tools.computer_use.cua_backend_capture import _CaptureMixin
from tools.computer_use.cua_backend_daemon import _EmbeddedCuaDaemon
from tools.computer_use.cua_backend_driver import (
_CUA_DRIVER_CMD_ENV, cua_driver_binary_available, cua_driver_runtime_contract_status)
from tools.computer_use.cua_backend_driver import ( # noqa: F401 — resolve_cua_driver_cmd: frozen updater surface
_CUA_DRIVER_CMD_ENV, cua_driver_binary_available, cua_driver_runtime_contract_status,
resolve_cua_driver_cmd)
from tools.computer_use.cua_backend_input import _InputMixin
from tools.computer_use.cua_backend_parse import _action_result_from
from tools.computer_use.cua_backend_session import _AsyncBridge, _CuaDriverSession
+2 -2
View File
@@ -74,7 +74,7 @@ def _validated_runtime_venv(env: dict) -> Path | None:
"""Producer-owned runtime venv identified by VIRTUAL_ENV, or None. The variable
alone is not provenance (users carry unrelated venvs): require the legacy Windows
base-Python producer's exact ``<repo>/venv`` layout AND a real ``pyvenv.cfg``."""
from hermes_cli.runtime_paths import runtime_facts_path, selected_venv
from pm.environments import runtime_facts_path, selected_venv
root = Path(__file__).resolve().parents[2]
if runtime_facts_path(root).is_file():
@@ -111,7 +111,7 @@ def _get_hermes_site_packages(env: dict) -> list[Path]:
runtime_venv = _validated_runtime_venv(env)
if runtime_venv is not None:
from hermes_cli.runtime_paths import site_packages
from pm.environments import site_packages
runtime_site_packages = site_packages(runtime_venv)
if not any(_same_path(runtime_site_packages, existing) for existing in result):
result.append(runtime_site_packages)
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more