diff --git a/news/9243.bugfix.rst b/news/9243.bugfix.rst new file mode 100644 index 00000000000..2e588c90534 --- /dev/null +++ b/news/9243.bugfix.rst @@ -0,0 +1 @@ +Filter available distributions using hash declarations from constraints files. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 02dbda1941f..637b6ce10af 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -52,7 +52,7 @@ hide_url, redact_auth_from_url, ) -from pip._internal.utils.packaging import safe_extra +from pip._internal.utils.packaging import is_pinned, safe_extra from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.virtualenv import running_under_virtualenv @@ -238,8 +238,7 @@ def is_pinned(self) -> bool: For example, some-package==1.2 is pinned; some-package>1.2 is not. """ - specifiers = self.specifier - return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="} + return is_pinned(self.specifier) def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool: if not extras_requested: diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 4569033db0a..3cfcac865ff 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -45,7 +45,7 @@ from pip._internal.resolution.base import InstallRequirementProvider from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.hashes import Hashes -from pip._internal.utils.packaging import get_requirement +from pip._internal.utils.packaging import get_requirement, is_pinned from pip._internal.utils.virtualenv import running_under_virtualenv from .base import Candidate, CandidateVersion, Constraint, Requirement @@ -303,19 +303,13 @@ def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]: # solely satisfied by a yanked release. all_yanked = all(ican.link.is_yanked for ican in icans) - def is_pinned(specifier: SpecifierSet) -> bool: - for sp in specifier: - if sp.operator == "===": - return True - if sp.operator != "==": - continue - if sp.version.endswith(".*"): - continue - return True - return False - pinned = is_pinned(specifier) + if not template.is_pinned: + assert template.req, "Candidates found on index must be PEP 508" + template.req.specifier = specifier + template.hash_options = hashes.allowed + # PackageFinder returns earlier versions first, so we reverse. for ican in reversed(icans): if not (all_yanked and pinned) and ican.link.is_yanked: diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 82eb035a06e..2f89bbf22f5 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -63,6 +63,10 @@ def __and__(self, other: "Hashes") -> "Hashes": def digest_count(self) -> int: return sum(len(digests) for digests in self._allowed.values()) + @property + def allowed(self) -> Dict[str, List[str]]: + return self._allowed + def is_hash_allowed(self, hash_name: str, hex_digest: str) -> bool: """Return whether the given hex digest is allowed.""" return hex_digest in self._allowed.get(hash_name, []) diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py index b9f6af4d174..7c77371a205 100644 --- a/src/pip/_internal/utils/packaging.py +++ b/src/pip/_internal/utils/packaging.py @@ -5,6 +5,7 @@ from pip._vendor.packaging import specifiers, version from pip._vendor.packaging.requirements import Requirement +from pip._vendor.packaging.specifiers import SpecifierSet NormalizedExtra = NewType("NormalizedExtra", str) @@ -55,3 +56,15 @@ def safe_extra(extra: str) -> NormalizedExtra: the same to either ``canonicalize_name`` or ``_egg_link_name``. """ return cast(NormalizedExtra, re.sub("[^A-Za-z0-9.-]+", "_", extra).lower()) + + +def is_pinned(specifier: SpecifierSet) -> bool: + for sp in specifier: + if sp.operator == "===": + return True + if sp.operator != "==": + continue + if sp.version.endswith(".*"): + continue + return True + return False diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index 80ed86219d4..008a4284c1d 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -373,3 +373,34 @@ def test_new_resolver_hash_with_extras(script: PipTestEnvironment) -> None: child="0.1.0", extra="0.1.0", ) + + +def test_new_resolver_hash_with_pin(script: PipTestEnvironment) -> None: + find_links = _create_find_links(script) + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text("base") + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text( + """ + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + """.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ) + ) + + script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--find-links", + find_links.index_html, + "--requirement", + requirements_txt, + "--constraint", + constraints_txt, + ) + + script.assert_installed(base="0.1.0")