fix(pm): enforce install policy for explicit Python builds
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user