Skip to content

Commit c068eb5

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 76520a5 commit c068eb5

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
@@ -1048,3 +1048,36 @@ def expand_path(path: str) -> str:
10481048
)
10491049
def test_link_hash_parsing(url: str, result: Optional[LinkHash]) -> None:
10501050
assert LinkHash.split_hash_name_and_value(url) == result
1051+
1052+
1053+
@pytest.mark.parametrize(
1054+
"archive_info, link_hash",
1055+
[
1056+
(
1057+
ArchiveInfo(hash=None),
1058+
None,
1059+
),
1060+
(
1061+
ArchiveInfo(hash="sha256=aabe42af"),
1062+
LinkHash(name="sha256", value="aabe42af"),
1063+
),
1064+
# Test invalid hash strings, which ArchiveInfo doesn't validate.
1065+
(
1066+
# Invalid hash name.
1067+
ArchiveInfo(hash="sha500=aabe42af"),
1068+
None,
1069+
),
1070+
(
1071+
# Invalid hash value.
1072+
ArchiveInfo(hash="sha256=g42afbe"),
1073+
None,
1074+
),
1075+
],
1076+
)
1077+
def test_link_hash_archive_info_fungibility(
1078+
archive_info: ArchiveInfo,
1079+
link_hash: Optional[LinkHash],
1080+
) -> None:
1081+
assert LinkHash.from_archive_info(archive_info) == link_hash
1082+
if link_hash is not None:
1083+
assert link_hash.to_archive_info() == archive_info

0 commit comments

Comments
 (0)