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).
75 lines
3.1 KiB
Python
75 lines
3.1 KiB
Python
"""Feature inventory reads the selected dependency tree, not the builder."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
import packaging
|
|
import pytest
|
|
|
|
import pm.extras as extras
|
|
import pm.features as features
|
|
from pm.environments import site_packages
|
|
|
|
|
|
def test_inventory_uses_the_target_and_requires_every_anchor(tmp_path, monkeypatch):
|
|
target_root = tmp_path / "target"
|
|
subprocess.run(
|
|
[sys.executable, "-m", "venv", "--without-pip", str(target_root)],
|
|
capture_output=True, check=True, timeout=60,
|
|
)
|
|
target = target_root / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
|
dependencies = tmp_path / "dependencies"
|
|
site = site_packages(dependencies)
|
|
site.mkdir(parents=True)
|
|
witness = tmp_path / "child.json"
|
|
package = site / "inventory_anchor"
|
|
package.mkdir()
|
|
(package / "__init__.py").write_text(
|
|
"import json, pathlib, sys\n"
|
|
f"pathlib.Path({str(witness)!r}).write_text(json.dumps(sys.executable), encoding='utf-8')\n",
|
|
encoding="utf-8",
|
|
)
|
|
(package / "present.py").write_text("VALUE = 'target'\n", encoding="utf-8")
|
|
(site / "one_part.py").write_text("", encoding="utf-8")
|
|
editable = tmp_path / "editable"
|
|
editable.mkdir()
|
|
(editable / "editable_anchor.py").write_text("", encoding="utf-8")
|
|
(site / "editable.pth").write_text(str(editable) + "\n", encoding="utf-8")
|
|
launches = tmp_path / "launches.jsonl"
|
|
(site / "launches.pth").write_text(
|
|
f"import json, pathlib, sys; p = pathlib.Path({str(launches)!r}); "
|
|
"text = p.read_text(encoding='utf-8') if p.exists() else ''; "
|
|
"p.write_text(text + json.dumps(sys.executable) + '\\n', encoding='utf-8')\n",
|
|
encoding="utf-8",
|
|
)
|
|
mapping = {
|
|
"complete": "inventory_anchor.present",
|
|
"partial": ("one_part", "absent_part"),
|
|
"host-only": "packaging",
|
|
"editable": "editable_anchor",
|
|
}
|
|
repo = tmp_path / "source"
|
|
repo.mkdir()
|
|
(repo / "pyproject.toml").write_text(
|
|
"[project.optional-dependencies]\n" + "".join(f'"{name}"=[]\n' for name in mapping),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr(extras, "ANCHORS", mapping)
|
|
before_path = list(sys.path)
|
|
before_env = dict(os.environ)
|
|
assert packaging.__file__ and "packaging" in sys.modules
|
|
result = features.installed_extras(repo, dependencies, python_exe=target)
|
|
assert result == ["complete", "editable"]
|
|
assert Path(json.loads(witness.read_text(encoding="utf-8"))) == target
|
|
assert [Path(json.loads(line)) for line in launches.read_text(encoding="utf-8").splitlines()] == [target]
|
|
assert "inventory_anchor" not in sys.modules
|
|
assert sys.path == before_path
|
|
assert dict(os.environ) == before_env
|
|
|
|
(site / "absent_part.py").write_text("", encoding="utf-8")
|
|
assert features.installed_extras(repo, dependencies, python_exe=target) == ["complete", "editable", "partial"]
|
|
with pytest.raises(features.FeatureProbeError, match="feature inventory failed"):
|
|
features.installed_extras(repo, dependencies, python_exe=tmp_path / "missing-python")
|