Skip to content

Commit 7cb145a

Browse files
add LinkHash.{from_archive_info(),__post_init__()}
these two objects are intended to be fungible, but play different roles, since LinkHash actually parses its input and ArchiveInfo does not. ArchiveInfo's JSON (de)?serialization does not employ hash name or value validation, while LinkHash does not offer a JSON serialization.
1 parent b1b1bc0 commit 7cb145a

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

src/pip/_internal/models/link.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@
3737
class LinkHash:
3838
"""Links to content may have embedded hash values. This class parses those.
3939
40-
`name` must be any member of `_SUPPORTED_HASHES`."""
40+
`name` must be any member of `_SUPPORTED_HASHES`.
41+
42+
This class can be converted to and from `ArchiveInfo`. While ArchiveInfo intends to
43+
be JSON-serializable to conform to PEP 610, this class contains the logic for
44+
parsing a hash name and value for correctness, and then checking whether that hash
45+
conforms to a schema with `.is_hash_allowed()`."""
4146

4247
name: str
4348
value: str
@@ -52,6 +57,9 @@ class LinkHash:
5257
)
5358
)
5459

60+
def __post_init__(self) -> None:
61+
assert self._hash_re.match(f"{self.name}={self.value}")
62+
5563
@classmethod
5664
@functools.lru_cache(maxsize=None)
5765
def split_hash_name_and_value(cls, url: str) -> Optional["LinkHash"]:
@@ -66,6 +74,13 @@ def to_archive_info(self) -> ArchiveInfo:
6674
"""Convert to ArchiveInfo to form a DirectUrl instance (see PEP 610)."""
6775
return ArchiveInfo(hash=f"{self.name}={self.value}")
6876

77+
@classmethod
78+
def from_archive_info(cls, info: ArchiveInfo) -> Optional["LinkHash"]:
79+
"""Parse an ArchiveInfo hash into a LinkHash instance."""
80+
if info.hash is None:
81+
return None
82+
return cls.split_hash_name_and_value(info.hash)
83+
6984
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
7085
"""
7186
Return True if the current hash is allowed by `hashes`.

tests/unit/test_collector.py

+33
Original file line numberDiff line numberDiff line change
@@ -1031,3 +1031,36 @@ def expand_path(path: str) -> str:
10311031
)
10321032
def test_link_hash_parsing(url: str, result: Optional[LinkHash]) -> None:
10331033
assert LinkHash.split_hash_name_and_value(url) == result
1034+
1035+
1036+
@pytest.mark.parametrize(
1037+
"archive_info, link_hash",
1038+
[
1039+
(
1040+
ArchiveInfo(hash=None),
1041+
None,
1042+
),
1043+
(
1044+
ArchiveInfo(hash="sha256=aabe42af"),
1045+
LinkHash(name="sha256", value="aabe42af"),
1046+
),
1047+
# Test invalid hash strings, which ArchiveInfo doesn't validate.
1048+
(
1049+
# Invalid hash name.
1050+
ArchiveInfo(hash="sha500=aabe42af"),
1051+
None,
1052+
),
1053+
(
1054+
# Invalid hash value.
1055+
ArchiveInfo(hash="sha256=g42afbe"),
1056+
None,
1057+
),
1058+
],
1059+
)
1060+
def test_link_hash_archive_info_fungibility(
1061+
archive_info: ArchiveInfo,
1062+
link_hash: Optional[LinkHash],
1063+
) -> None:
1064+
assert LinkHash.from_archive_info(archive_info) == link_hash
1065+
if link_hash is not None:
1066+
assert link_hash.to_archive_info() == archive_info

0 commit comments

Comments
 (0)