diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index c7c4b0e9b..a1e4d5a08 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -79,6 +79,9 @@ class LinkHash: name, value = match.groups() return cls(name=name, value=value) + def as_dict(self) -> Dict[str, str]: + return {self.name: self.value} + def as_hashes(self) -> Hashes: """Return a Hashes instance which checks only for the current hash.""" return Hashes({self.name: [self.value]}) @@ -165,7 +168,6 @@ class Link(KeyBasedCompareMixin): "requires_python", "yanked_reason", "dist_info_metadata", - "link_hash", "cache_link_parsing", "egg_fragment", ] @@ -177,7 +179,6 @@ class Link(KeyBasedCompareMixin): requires_python: Optional[str] = None, yanked_reason: Optional[str] = None, dist_info_metadata: Optional[str] = None, - link_hash: Optional[LinkHash] = None, cache_link_parsing: bool = True, hashes: Optional[Mapping[str, str]] = None, ) -> None: @@ -200,16 +201,11 @@ class Link(KeyBasedCompareMixin): attribute, if present, in a simple repository HTML link. This may be parsed into its own `Link` by `self.metadata_link()`. See PEP 658 for more information and the specification. - :param link_hash: a checksum for the content the link points to. If not - provided, this will be extracted from the link URL, if the URL has - any checksum. :param cache_link_parsing: A flag that is used elsewhere to determine - whether resources retrieved from this link - should be cached. PyPI index urls should - generally have this set to False, for - example. + whether resources retrieved from this link should be cached. PyPI + URLs should generally have this set to False, for example. :param hashes: A mapping of hash names to digests to allow us to - determine the validity of a download. + determine the validity of a download. """ # url can be a UNC windows share @@ -220,13 +216,18 @@ class Link(KeyBasedCompareMixin): # Store the url as a private attribute to prevent accidentally # trying to set a new value. self._url = url - self._hashes = hashes if hashes is not None else {} + + link_hash = LinkHash.split_hash_name_and_value(url) + hashes_from_link = {} if link_hash is None else link_hash.as_dict() + if hashes is None: + self._hashes = hashes_from_link + else: + self._hashes = {**hashes, **hashes_from_link} self.comes_from = comes_from self.requires_python = requires_python if requires_python else None self.yanked_reason = yanked_reason self.dist_info_metadata = dist_info_metadata - self.link_hash = link_hash or LinkHash.split_hash_name_and_value(self._url) super().__init__(key=url, defining_class=Link) @@ -401,29 +402,26 @@ class Link(KeyBasedCompareMixin): if self.dist_info_metadata is None: return None metadata_url = f"{self.url_without_fragment}.metadata" - link_hash: Optional[LinkHash] = None # If data-dist-info-metadata="true" is set, then the metadata file exists, # but there is no information about its checksum or anything else. if self.dist_info_metadata != "true": link_hash = LinkHash.split_hash_name_and_value(self.dist_info_metadata) - return Link(metadata_url, link_hash=link_hash) + else: + link_hash = None + if link_hash is None: + return Link(metadata_url) + return Link(metadata_url, hashes=link_hash.as_dict()) - def as_hashes(self) -> Optional[Hashes]: - if self.link_hash is not None: - return self.link_hash.as_hashes() - return None + def as_hashes(self) -> Hashes: + return Hashes({k: [v] for k, v in self._hashes.items()}) @property def hash(self) -> Optional[str]: - if self.link_hash is not None: - return self.link_hash.value - return None + return next(iter(self._hashes.values()), None) @property def hash_name(self) -> Optional[str]: - if self.link_hash is not None: - return self.link_hash.name - return None + return next(iter(self._hashes), None) @property def show_url(self) -> str: @@ -452,15 +450,15 @@ class Link(KeyBasedCompareMixin): @property def has_hash(self) -> bool: - return self.link_hash is not None + return bool(self._hashes) def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool: """ Return True if the link has a hash and it is allowed by `hashes`. """ - if self.link_hash is None: + if hashes is None: return False - return self.link_hash.is_hash_allowed(hashes) + return any(hashes.is_hash_allowed(k, v) for k, v in self._hashes.items()) class _CleanResult(NamedTuple): diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 55676a4fc..47307c00e 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -6,7 +6,7 @@ import re import uuid from pathlib import Path from textwrap import dedent -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple from unittest import mock import pytest @@ -538,7 +538,7 @@ def test_parse_links_json() -> None: metadata_link.url == "https://example.com/files/holygrail-1.0-py3-none-any.whl.metadata" ) - assert metadata_link.link_hash == LinkHash("sha512", "aabdd41") + assert metadata_link._hashes == {"sha512": "aabdd41"} @pytest.mark.parametrize( @@ -575,41 +575,41 @@ _pkg1_requirement = Requirement("pkg1==1.0") @pytest.mark.parametrize( - "anchor_html, expected, link_hash", + "anchor_html, expected, hashes", [ # Test not present. ( '', None, - None, + {}, ), # Test with value "true". ( '', "true", - None, + {}, ), # Test with a provided hash value. ( '', # noqa: E501 "sha256=aa113592bbe", - None, + {}, ), # Test with a provided hash value for both the requirement as well as metadata. ( '', # noqa: E501 "sha256=aa113592bbe", - LinkHash("sha512", "abc132409cb"), + {"sha512": "abc132409cb"}, ), ], ) def test_parse_links__dist_info_metadata( anchor_html: str, expected: Optional[str], - link_hash: Optional[LinkHash], + hashes: Dict[str, str], ) -> None: link = _test_parse_links_data_attribute(anchor_html, "dist_info_metadata", expected) - assert link.link_hash == link_hash + assert link._hashes == hashes def test_parse_links_caches_same_page_by_url() -> None: