-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement explicit lockfiles integration (#23)
* add list extensions * add more details to the explicit list * implement install from '# pypi:' lockfiles * fix call * add test * order can vary across runs 🤷 * add disclaimer * add version to disclaimer * add md5 of the RECORD file * test with --md5 * pre-commit * Verify checksums of RECORD files upon install * fix test * fix test * fix windows entry points * fix test * fix `context` cmd_line value fetching * use 0.1.0 as t0 * refactor pypi_lines with a helper object and rely on 3rd party parsers when feasible * mroe refactors * fix test * more docs!
- Loading branch information
Showing
15 changed files
with
800 additions
and
99 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -130,3 +130,6 @@ dmypy.json | |
|
||
# pixi | ||
.pixi/ | ||
|
||
# Used in debugging | ||
explicit.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
""" | ||
conda-pypi | ||
""" | ||
|
||
__version__ = "0.1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from . import install, list, pip | ||
|
||
__all__ = ["install", "list", "pip"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
from __future__ import annotations | ||
|
||
import sys | ||
from logging import getLogger | ||
from pathlib import Path | ||
from typing import TYPE_CHECKING | ||
|
||
from conda.base.context import context | ||
from conda.common.io import Spinner | ||
from conda.exceptions import CondaVerificationError, CondaFileIOError | ||
|
||
from ..main import run_pip_install, compute_record_sum, PyPIDistribution | ||
from ..utils import get_env_site_packages | ||
|
||
if TYPE_CHECKING: | ||
from typing import Iterable, Literal | ||
|
||
log = getLogger(f"conda.{__name__}") | ||
|
||
|
||
def _prepare_pypi_transaction(lines: Iterable[str]) -> dict[str, dict[str, str]]: | ||
pkgs = {} | ||
for line in lines: | ||
dist = PyPIDistribution.from_lockfile_line(line) | ||
pkgs[(dist.name, dist.version)] = { | ||
"url": dist.find_wheel_url(), | ||
"hashes": dist.record_checksums, | ||
} | ||
return pkgs | ||
|
||
|
||
def _verify_pypi_transaction( | ||
prefix: str, | ||
pkgs: dict[str, dict[str, str]], | ||
on_error: Literal["ignore", "warn", "error"] = "warn", | ||
): | ||
site_packages = get_env_site_packages(prefix) | ||
errors = [] | ||
dist_infos = [path for path in site_packages.glob("*.dist-info") if path.is_dir()] | ||
for (name, version), pkg in pkgs.items(): | ||
norm_name = name.lower().replace("-", "_").replace(".", "_") | ||
dist_info = next( | ||
( | ||
d | ||
for d in dist_infos | ||
if d.stem.rsplit("-", 1) in ([name, version], [norm_name, version]) | ||
), | ||
None, | ||
) | ||
if not dist_info: | ||
errors.append(f"Could not find installation for {name}=={version}") | ||
continue | ||
|
||
expected_hashes = pkg.get("hashes") | ||
if expected_hashes: | ||
found_hashes = compute_record_sum(dist_info / "RECORD", expected_hashes.keys()) | ||
log.info("Verifying %s==%s with %s", name, version, ", ".join(expected_hashes)) | ||
for algo, expected_hash in expected_hashes.items(): | ||
found_hash = found_hashes.get(algo) | ||
if found_hash and expected_hash != found_hash: | ||
msg = ( | ||
"%s checksum for %s==%s didn't match! Expected=%s, found=%s", | ||
algo, | ||
name, | ||
version, | ||
expected_hash, | ||
found_hash, | ||
) | ||
if on_error == "warn": | ||
log.warning(*msg) | ||
elif on_error == "error": | ||
errors.append(msg[0] % msg[1:]) | ||
else: | ||
log.debug(*msg) | ||
if errors: | ||
errors = "\n- ".join(errors) | ||
raise CondaVerificationError(f"PyPI packages checksum verification failed:\n- {errors}") | ||
|
||
|
||
def post_command(command: str) -> int: | ||
if command not in ("install", "create"): | ||
return 0 | ||
|
||
pypi_lines = _pypi_lines_from_paths() | ||
if not pypi_lines: | ||
return 0 | ||
|
||
with Spinner("\nPreparing PyPI transaction", enabled=not context.quiet, json=context.json): | ||
pkgs = _prepare_pypi_transaction(pypi_lines) | ||
|
||
with Spinner("Executing PyPI transaction", enabled=not context.quiet, json=context.json): | ||
run_pip_install( | ||
context.target_prefix, | ||
args=[pkg["url"] for pkg in pkgs.values()], | ||
dry_run=context.dry_run, | ||
quiet=context.quiet, | ||
verbosity=context.verbosity, | ||
force_reinstall=context.force_reinstall, | ||
yes=context.always_yes, | ||
check=True, | ||
) | ||
|
||
with Spinner("Verifying PyPI transaction", enabled=not context.quiet, json=context.json): | ||
on_error_dict = {"disabled": "ignore", "warn": "warn", "enabled": "error"} | ||
on_error = on_error_dict.get(context.safety_checks, "warn") | ||
_verify_pypi_transaction(context.target_prefix, pkgs, on_error=on_error) | ||
|
||
return 0 | ||
|
||
|
||
def _pypi_lines_from_paths(paths: Iterable[str] | None = None) -> list[str]: | ||
if paths is None: | ||
file_arg = context.raw_data["cmd_line"].get("file") | ||
if file_arg is None: | ||
return [] | ||
paths = file_arg.value(None) | ||
lines = [] | ||
line_prefix = PyPIDistribution._line_prefix | ||
for path in paths: | ||
path = path.value(None) | ||
try: | ||
with open(path) as f: | ||
for line in f: | ||
if line.startswith(line_prefix): | ||
lines.append(line[len(line_prefix) :]) | ||
except OSError as exc: | ||
raise CondaFileIOError(f"Could not process {path}") from exc | ||
return lines |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import sys | ||
from conda.base.context import context | ||
|
||
from .. import __version__ | ||
from ..main import pypi_lines_for_explicit_lockfile | ||
|
||
|
||
def post_command(command: str): | ||
if command != "list": | ||
return | ||
cmd_line = context.raw_data.get("cmd_line", {}) | ||
if "--explicit" not in sys.argv and not cmd_line.get("explicit").value(None): | ||
return | ||
if "--no-pip" in sys.argv or not cmd_line.get("pip"): | ||
return | ||
checksums = ("md5",) if ("--md5" in sys.argv or cmd_line.get("md5").value(None)) else None | ||
to_print = pypi_lines_for_explicit_lockfile(context.target_prefix, checksums=checksums) | ||
if to_print: | ||
sys.stdout.flush() | ||
print(f"# The following lines were added by conda-pypi v{__version__}") | ||
print("# This is an experimental feature subject to change. Do not use in production.") | ||
print(*to_print, sep="\n") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.