Skip to content

Commit

Permalink
Canonicalize names for requirements comparison
Browse files Browse the repository at this point in the history
Fix pypagh-644.

Signed-off-by: Juan Luis Cano Rodríguez <juan_luis_cano@mckinsey.com>
  • Loading branch information
astrojuanlu committed Jun 19, 2023
1 parent f8893d4 commit bceb58e
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 5 deletions.
16 changes: 12 additions & 4 deletions src/packaging/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ._tokenizer import ParserSyntaxError
from .markers import Marker, _normalize_extra_values
from .specifiers import SpecifierSet
from .utils import canonicalize_name


class InvalidRequirement(ValueError):
Expand Down Expand Up @@ -44,8 +45,12 @@ def __init__(self, requirement_string: str) -> None:
self.marker = Marker.__new__(Marker)
self.marker._markers = _normalize_extra_values(parsed.marker)

def __str__(self) -> str:
parts: List[str] = [self.name]
@property
def canonical_name(self) -> str:
return canonicalize_name(self.name)

def _to_str(self, name: str) -> str:
parts: List[str] = [name]

if self.extras:
formatted_extras = ",".join(sorted(self.extras))
Expand All @@ -64,18 +69,21 @@ def __str__(self) -> str:

return "".join(parts)

def __str__(self) -> str:
return self._to_str(self.name)

def __repr__(self) -> str:
return f"<Requirement('{self}')>"

def __hash__(self) -> int:
return hash((self.__class__.__name__, str(self)))
return hash((self.__class__.__name__, self._to_str(self.canonical_name)))

def __eq__(self, other: Any) -> bool:
if not isinstance(other, Requirement):
return NotImplemented

return (
self.name == other.name
self.canonical_name == other.canonical_name
and self.extras == other.extras
and self.specifier == other.specifier
and self.url == other.url
Expand Down
30 changes: 29 additions & 1 deletion tests/test_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
),
]

EQUIVALENT_DEPENDENCIES = [
("scikit-learn==1.0.1", "scikit_learn==1.0.1"),
]

DIFFERENT_DEPENDENCIES = [
("package_one", "package_two"),
("packaging>20.1", "packaging>=20.1"),
Expand Down Expand Up @@ -201,6 +205,17 @@ def test_empty_specifier(self) -> None:
assert req.name == "name"
assert req.specifier == ""

def test_canonical_name(self) -> None:
# GIVEN
to_parse = "split_name"

# WHEN
req = Requirement(to_parse)

# THEN
assert req.name == "split_name"
assert req.canonical_name == "split-name"

# ----------------------------------------------------------------------------------
# Everything below this (in this class) should be parsing failure modes
# ----------------------------------------------------------------------------------
Expand Down Expand Up @@ -632,12 +647,25 @@ def test_str_and_repr(

@pytest.mark.parametrize("dep1, dep2", EQUAL_DEPENDENCIES)
def test_equal_reqs_equal_hashes(self, dep1: str, dep2: str) -> None:
"""Requirement objects created from equivalent strings should be equal."""
"""Requirement objects created from equal strings should be equal."""
# GIVEN / WHEN
req1, req2 = Requirement(dep1), Requirement(dep2)

assert req1 == req2
assert hash(req1) == hash(req2)

@pytest.mark.parametrize("dep1, dep2", EQUIVALENT_DEPENDENCIES)
def test_equivalent_reqs_equal_hashes_unequal_strings(
self, dep1: str, dep2: str
) -> None:
"""Requirement objects created from equivalent strings should be equal,
even though their string representation will not."""
# GIVEN / WHEN
req1, req2 = Requirement(dep1), Requirement(dep2)

assert req1 == req2
assert hash(req1) == hash(req2)
assert str(req1) != str(req2)

@pytest.mark.parametrize("dep1, dep2", DIFFERENT_DEPENDENCIES)
def test_different_reqs_different_hashes(self, dep1: str, dep2: str) -> None:
Expand Down

0 comments on commit bceb58e

Please sign in to comment.