fix(pm): enforce install policy for explicit Python builds

This commit is contained in:
ethernet
2026-09-11 18:13:56 -04:00
parent fc4086c4c5
commit cdd76e03ec
2 changed files with 14 additions and 5 deletions

View File

@@ -18,6 +18,13 @@ import uuid
from pm.package import InstallError
def _require_install_allowed(explicit: bool) -> None:
from pm.ensure import _refuse_lazy, lazy_installs_allowed
if not explicit and not lazy_installs_allowed():
raise _refuse_lazy("venv", "Python dependency operation requires an explicit request")
def build_environment(
*, source: Path, out: Path, python: Path | None = None,
cache: Path | None = None, env: Mapping[str, str] | None = None,
@@ -41,6 +48,7 @@ def build_environment(
raise InstallError("venv", f"frozen build requires a lock: {source / 'uv.lock'}")
if out.exists() or out.is_symlink():
raise FileExistsError(f"environment destination already exists: {out}")
_require_install_allowed(explicit)
environment = managed_environment(
out, python=Path(python) if python is not None else None,
cache=Path(cache) if cache is not None else None, env=env,
@@ -71,6 +79,7 @@ def lock_project(
source = Path(source).absolute()
if not (source / "pyproject.toml").is_file():
raise InstallError("venv", f"project manifest is missing: {source}")
_require_install_allowed(explicit)
environment = managed_environment(
source / ".venv", python=Path(python) if python is not None else None,
cache=Path(cache) if cache is not None else None, env=env,

View File

@@ -152,7 +152,7 @@ def test_public_build_installs_all_extras_at_explicit_destination(installable_pr
monkeypatch.setattr(pm.workspace, "enabled_member_dirs", lambda: pytest.fail("user plugins"))
before = dict(os.environ)
locked = (source / "uv.lock").read_bytes()
executable = build_environment(
executable = build_environment(explicit=True,
source=source, python=Path(sys.executable), out=tmp_path / "native environment",
cache=tmp_path / "cache", env=env, all_extras=True, offline=True,
sealed=sealed,
@@ -183,7 +183,7 @@ def test_public_dependency_only_build_needs_no_application_source(installable_pr
locked = (source / "uv.lock").read_bytes()
from pm import build_environment
executable = build_environment(source=source, python=Path(sys.executable),
executable = build_environment(explicit=True, source=source, python=Path(sys.executable),
out=tmp_path / "docker env", cache=tmp_path / "cache", env=env,
no_install_project=True, offline=True, **selection)
assert executable.is_file()
@@ -323,14 +323,14 @@ def test_failed_build_removes_only_its_candidate(installable_project, tmp_path,
source, uv, env = installable_project
previous = tmp_path / "previous"
executable = build_environment(source=source, python=Path(sys.executable),
executable = build_environment(explicit=True, source=source, python=Path(sys.executable),
out=previous, env=env, cache=tmp_path / "cache", offline=True)
cfg = (previous / "pyvenv.cfg").read_bytes()
source_lock = (source / "uv.lock").read_bytes()
# Check destination refusal with valid inputs. The contract does not specify
# which error comes first when the source is also damaged.
with pytest.raises(FileExistsError):
build_environment(source=source, python=Path(sys.executable),
build_environment(explicit=True, source=source, python=Path(sys.executable),
out=previous, env=env, cache=tmp_path / "cache", offline=True)
if damage == "source":
(source / "root_app.py").unlink()
@@ -343,7 +343,7 @@ def test_failed_build_removes_only_its_candidate(installable_project, tmp_path,
(source / "uv.lock").unlink()
candidate = tmp_path / "candidate"
with pytest.raises(InstallError, match="dependency validation" if damage == "check" else "uv sync|frozen build requires a lock"):
build_environment(source=source, python=Path(sys.executable),
build_environment(explicit=True, source=source, python=Path(sys.executable),
out=candidate, env=env, cache=tmp_path / "cold-cache", offline=True)
assert not candidate.exists()
assert (previous / "pyvenv.cfg").read_bytes() == cfg