106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import optparse
|
|
from contextlib import contextmanager
|
|
from typing import Iterator, Mapping, cast
|
|
|
|
from pip._internal.commands.install import InstallCommand
|
|
from pip._internal.index.package_finder import PackageFinder
|
|
from pip._internal.models.candidate import InstallationCandidate
|
|
from pip._internal.network.session import PipSession
|
|
from pip._internal.req import InstallRequirement
|
|
from pip._internal.utils.hashes import FAVORITE_HASH
|
|
|
|
from piptools.utils import as_tuple, key_from_ireq, make_install_requirement
|
|
|
|
from .base import BaseRepository
|
|
from .pypi import PyPIRepository
|
|
|
|
|
|
def ireq_satisfied_by_existing_pin(
|
|
ireq: InstallRequirement, existing_pin: InstallationCandidate
|
|
) -> bool:
|
|
"""
|
|
Return True if the given InstallationRequirement is satisfied by the
|
|
previously encountered version pin.
|
|
"""
|
|
version = next(iter(existing_pin.req.specifier)).version
|
|
result = ireq.req.specifier.contains(
|
|
version, prereleases=existing_pin.req.specifier.prereleases
|
|
)
|
|
return cast(bool, result)
|
|
|
|
|
|
class LocalRequirementsRepository(BaseRepository):
|
|
"""
|
|
The LocalRequirementsRepository proxied the _real_ repository by first
|
|
checking if a requirement can be satisfied by existing pins (i.e. the
|
|
result of a previous compile step).
|
|
|
|
In effect, if a requirement can be satisfied with a version pinned in the
|
|
requirements file, we prefer that version over the best match found in
|
|
PyPI. This keeps updates to the requirements.txt down to a minimum.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
existing_pins: Mapping[str, InstallationCandidate],
|
|
proxied_repository: PyPIRepository,
|
|
reuse_hashes: bool = True,
|
|
):
|
|
self._reuse_hashes = reuse_hashes
|
|
self.repository = proxied_repository
|
|
self.existing_pins = existing_pins
|
|
|
|
@property
|
|
def options(self) -> optparse.Values:
|
|
return self.repository.options
|
|
|
|
@property
|
|
def finder(self) -> PackageFinder:
|
|
return self.repository.finder
|
|
|
|
@property
|
|
def session(self) -> PipSession:
|
|
return self.repository.session
|
|
|
|
@property
|
|
def command(self) -> InstallCommand:
|
|
"""Return an install command instance."""
|
|
return self.repository.command
|
|
|
|
def clear_caches(self) -> None:
|
|
self.repository.clear_caches()
|
|
|
|
def find_best_match(
|
|
self, ireq: InstallRequirement, prereleases: bool | None = None
|
|
) -> InstallationCandidate:
|
|
key = key_from_ireq(ireq)
|
|
existing_pin = self.existing_pins.get(key)
|
|
if existing_pin and ireq_satisfied_by_existing_pin(ireq, existing_pin):
|
|
project, version, _ = as_tuple(existing_pin)
|
|
return make_install_requirement(project, version, ireq)
|
|
else:
|
|
return self.repository.find_best_match(ireq, prereleases)
|
|
|
|
def get_dependencies(self, ireq: InstallRequirement) -> set[InstallRequirement]:
|
|
return self.repository.get_dependencies(ireq)
|
|
|
|
def get_hashes(self, ireq: InstallRequirement) -> set[str]:
|
|
existing_pin = self._reuse_hashes and self.existing_pins.get(
|
|
key_from_ireq(ireq)
|
|
)
|
|
if existing_pin and ireq_satisfied_by_existing_pin(ireq, existing_pin):
|
|
hashes = existing_pin.hash_options
|
|
hexdigests = hashes.get(FAVORITE_HASH)
|
|
if hexdigests:
|
|
return {
|
|
":".join([FAVORITE_HASH, hexdigest]) for hexdigest in hexdigests
|
|
}
|
|
return self.repository.get_hashes(ireq)
|
|
|
|
@contextmanager
|
|
def allow_all_wheels(self) -> Iterator[None]:
|
|
with self.repository.allow_all_wheels():
|
|
yield
|