Repair missing packages during environment sync
This commit is contained in:
@@ -113,6 +113,60 @@ class PythonEnvironmentSyncTests(unittest.TestCase):
|
|||||||
self.assertEqual(plan.mode, "Selective Python environment repair")
|
self.assertEqual(plan.mode, "Selective Python environment repair")
|
||||||
self.assertEqual(plan.commands[0][-2:], ("-e", str(project_root)))
|
self.assertEqual(plan.commands[0][-2:], ("-e", str(project_root)))
|
||||||
|
|
||||||
|
def test_metadata_sync_also_repairs_unrelated_missing_distribution(self) -> None:
|
||||||
|
sync = load_sync_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
requirements = root / "requirements-dev.txt"
|
||||||
|
requirements.write_text("-e ./govoplan-changed\n-e ./govoplan-missing\n", encoding="utf-8")
|
||||||
|
for project in ("govoplan-changed", "govoplan-missing"):
|
||||||
|
project_root = root / project
|
||||||
|
project_root.mkdir()
|
||||||
|
(project_root / "pyproject.toml").write_text(
|
||||||
|
f'[project]\nname = "{project}"\nversion = "0.1.10"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
entries = sync.local_requirement_entries(requirements)
|
||||||
|
fingerprint = sync.build_fingerprint(
|
||||||
|
requirements=requirements,
|
||||||
|
python="/test/venv/bin/python",
|
||||||
|
local_requirements=entries,
|
||||||
|
)
|
||||||
|
requirements_digest = hashlib.sha256(requirements.read_bytes()).hexdigest()
|
||||||
|
previous = {
|
||||||
|
"version": sync.STAMP_VERSION,
|
||||||
|
"python": "/test/venv/bin/python",
|
||||||
|
"inputs": [
|
||||||
|
{"path": str(requirements), "sha256": requirements_digest},
|
||||||
|
{"path": entries[0].pyproject, "sha256": "stale"},
|
||||||
|
{
|
||||||
|
"path": entries[1].pyproject,
|
||||||
|
"sha256": hashlib.sha256(Path(entries[1].pyproject).read_bytes()).hexdigest(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"requirements_entries": [
|
||||||
|
entry.as_dict() for entry in sync.parse_requirement_entries(requirements)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
plan = sync.build_install_plan(
|
||||||
|
previous=previous,
|
||||||
|
fingerprint=fingerprint,
|
||||||
|
requirements=requirements,
|
||||||
|
python="/test/venv/bin/python",
|
||||||
|
local_requirements=entries,
|
||||||
|
repair_requirements=(entries[1],),
|
||||||
|
force=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(plan.mode, "Selective Python environment sync")
|
||||||
|
self.assertEqual(len(plan.commands), 1)
|
||||||
|
command = plan.commands[0]
|
||||||
|
self.assertEqual(command.count("-e"), 2)
|
||||||
|
self.assertIn(str(root / "govoplan-changed"), command)
|
||||||
|
self.assertIn(str(root / "govoplan-missing"), command)
|
||||||
|
|
||||||
def test_declared_module_entry_points_are_part_of_environment_validation(self) -> None:
|
def test_declared_module_entry_points_are_part_of_environment_validation(self) -> None:
|
||||||
sync = load_sync_module()
|
sync = load_sync_module()
|
||||||
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
|
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ def main() -> int:
|
|||||||
requirements=requirements,
|
requirements=requirements,
|
||||||
python=python,
|
python=python,
|
||||||
local_requirements=local_requirements,
|
local_requirements=local_requirements,
|
||||||
|
repair_requirements=environment.stale_requirements,
|
||||||
force=args.force,
|
force=args.force,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -250,6 +251,7 @@ def build_install_plan(
|
|||||||
python: str,
|
python: str,
|
||||||
local_requirements: tuple[RequirementEntry, ...],
|
local_requirements: tuple[RequirementEntry, ...],
|
||||||
force: bool,
|
force: bool,
|
||||||
|
repair_requirements: tuple[RequirementEntry, ...] = (),
|
||||||
) -> InstallPlan:
|
) -> InstallPlan:
|
||||||
full_command = (python, "-m", "pip", "install", "-r", str(requirements))
|
full_command = (python, "-m", "pip", "install", "-r", str(requirements))
|
||||||
if force:
|
if force:
|
||||||
@@ -273,7 +275,12 @@ def build_install_plan(
|
|||||||
removed_paths = set(previous_inputs) - set(current_inputs)
|
removed_paths = set(previous_inputs) - set(current_inputs)
|
||||||
stale_requirements = requirements_key in changed_paths or requirements_key in removed_paths
|
stale_requirements = requirements_key in changed_paths or requirements_key in removed_paths
|
||||||
|
|
||||||
installs: list[tuple[str, ...]] = []
|
# Metadata changes and installation drift can happen together. Keep both
|
||||||
|
# sets in one resolver transaction so a selective metadata sync also
|
||||||
|
# repairs local distributions omitted from the current environment.
|
||||||
|
installs: list[tuple[str, ...]] = [
|
||||||
|
requirement.install_args for requirement in repair_requirements
|
||||||
|
]
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
|
|
||||||
if stale_requirements:
|
if stale_requirements:
|
||||||
@@ -322,7 +329,7 @@ def build_install_plan(
|
|||||||
)
|
)
|
||||||
return InstallPlan(
|
return InstallPlan(
|
||||||
"Selective Python environment sync",
|
"Selective Python environment sync",
|
||||||
f"installing {len(deduped_installs)} stale local requirement(s) in one resolver transaction.",
|
f"installing {len(deduped_installs)} stale or missing local requirement(s) in one resolver transaction.",
|
||||||
(command,),
|
(command,),
|
||||||
tuple(warnings),
|
tuple(warnings),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user