Modules resolve the manifest with uv pip compile before syncing, so listed packages bring their dependencies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
@@ -6,9 +6,12 @@ into a venv on the data volume rather than into the engine's environment: a
|
||||
pin here can never shadow — or be shadowed by — what the app itself runs on,
|
||||
and the worker processes that import them need nothing from the app.
|
||||
|
||||
``uv pip sync`` rather than install, so a line taken out of the manifest is
|
||||
uninstalled. The manifest is written only after a sync succeeds, which is all
|
||||
the rollback a failed resolve needs.
|
||||
The manifest is resolved with ``uv pip compile`` first, so a line naming a
|
||||
package brings its dependencies along, and a manifest that already pins
|
||||
everything resolves to itself. The result goes to ``uv pip sync`` rather than
|
||||
install, so a line taken out of the manifest is uninstalled. The manifest is
|
||||
written only after a sync succeeds, which is all the rollback a failed resolve
|
||||
needs.
|
||||
|
||||
None of that applies to an *adopted* venv. A data scientist makes a venv,
|
||||
installs what they work with, and then installs Fluksio into it too — at which
|
||||
@@ -179,35 +182,52 @@ def sync(requirements: str) -> tuple[bool, str]:
|
||||
|
||||
|
||||
def _sync(requirements: str) -> tuple[bool, str]:
|
||||
"""The install itself, under `_sync_lock`."""
|
||||
manifest = ""
|
||||
"""The resolve and install, under `_sync_lock`."""
|
||||
try:
|
||||
ensure_venv()
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as handle:
|
||||
handle.write(requirements)
|
||||
manifest = handle.name
|
||||
result = subprocess.run(
|
||||
# Empty means empty: without the flag uv refuses to clear a venv,
|
||||
# so deleting the last line would leave the package installed.
|
||||
[
|
||||
uv_bin(),
|
||||
"pip",
|
||||
"sync",
|
||||
"--allow-empty-requirements",
|
||||
"--python",
|
||||
venv_python(),
|
||||
manifest,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=SYNC_TIMEOUT,
|
||||
)
|
||||
python = venv_python()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
manifest = Path(directory, "requirements.in")
|
||||
lock = Path(directory, "requirements.txt")
|
||||
manifest.write_text(requirements)
|
||||
for command in (
|
||||
# The emit flags carry the manifest's index, find-links and
|
||||
# binary options into the lock, which is all sync reads.
|
||||
[
|
||||
uv_bin(),
|
||||
"pip",
|
||||
"compile",
|
||||
"--quiet",
|
||||
"--emit-index-url",
|
||||
"--emit-find-links",
|
||||
"--emit-build-options",
|
||||
"--python",
|
||||
python,
|
||||
str(manifest),
|
||||
"--output-file",
|
||||
str(lock),
|
||||
],
|
||||
# Empty means empty: without the flag uv refuses to clear a
|
||||
# venv, so deleting the last line would leave the package
|
||||
# installed.
|
||||
[
|
||||
uv_bin(),
|
||||
"pip",
|
||||
"sync",
|
||||
"--allow-empty-requirements",
|
||||
"--python",
|
||||
python,
|
||||
str(lock),
|
||||
],
|
||||
):
|
||||
result = subprocess.run(
|
||||
command, capture_output=True, text=True, timeout=SYNC_TIMEOUT
|
||||
)
|
||||
if result.returncode != 0:
|
||||
break
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
detail = str(getattr(exc, "stderr", None) or exc).strip()
|
||||
return False, f"uv could not run: {detail}"
|
||||
finally:
|
||||
if manifest:
|
||||
Path(manifest).unlink(missing_ok=True)
|
||||
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
if result.returncode == 0:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -77,6 +78,40 @@ def test_a_manifest_that_does_not_resolve_leaves_the_venv_alone(venv: Path):
|
||||
assert (venv / ".applied").read_text() == modules._digest("")
|
||||
|
||||
|
||||
def _wheel(directory: Path, name: str, requires: str = "") -> None:
|
||||
"""A wheel with nothing in it but a name and, optionally, one dependency."""
|
||||
info = f"{name}-1.0.dist-info"
|
||||
metadata = f"Metadata-Version: 2.1\nName: {name}\nVersion: 1.0\n"
|
||||
if requires:
|
||||
metadata += f"Requires-Dist: {requires}\n"
|
||||
with zipfile.ZipFile(directory / f"{name}-1.0-py3-none-any.whl", "w") as wheel:
|
||||
wheel.writestr(f"{name}/__init__.py", "")
|
||||
wheel.writestr(f"{info}/METADATA", metadata)
|
||||
wheel.writestr(
|
||||
f"{info}/WHEEL",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
)
|
||||
wheel.writestr(f"{info}/RECORD", "")
|
||||
|
||||
|
||||
def test_a_listed_package_brings_its_dependencies(
|
||||
venv: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
wheels = tmp_path / "wheels"
|
||||
wheels.mkdir()
|
||||
_wheel(wheels, "fluksio_top", requires="fluksio_dep")
|
||||
_wheel(wheels, "fluksio_dep")
|
||||
monkeypatch.setenv("UV_OFFLINE", "1")
|
||||
|
||||
ok, output = modules.sync(f"--find-links {wheels}\nfluksio-top\n")
|
||||
|
||||
assert ok, output
|
||||
imported = subprocess.run(
|
||||
[modules.venv_python(), "-c", "import fluksio_dep"], check=False
|
||||
)
|
||||
assert imported.returncode == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adopting the venv Fluksio was installed into
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user