diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 88e2b172..f7b96d8d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,8 @@ Changelog *unreleased* ~~~~~~~~~~~~ -No unreleased changes. +* Do specifier matching correctly when the specifier contains an epoch number + and has more components than the version (:issue:`683`) 23.2 - 2023-10-01 ~~~~~~~~~~~~~~~~~ diff --git a/src/packaging/specifiers.py b/src/packaging/specifiers.py index ba8fe37b..87d0e0ad 100644 --- a/src/packaging/specifiers.py +++ b/src/packaging/specifiers.py @@ -383,7 +383,7 @@ def _compare_compatible(self, prospective: Version, spec: str) -> bool: # We want everything but the last item in the version, but we want to # ignore suffix segments. - prefix = ".".join( + prefix = _version_join( list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] ) @@ -404,13 +404,13 @@ def _compare_equal(self, prospective: Version, spec: str) -> bool: ) # Get the normalized version string ignoring the trailing .* normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) - # Split the spec out by dots, and pretend that there is an implicit - # dot in between a release segment and a pre-release segment. + # Split the spec out by bangs and dots, and pretend that there is + # an implicit dot in between a release segment and a pre-release segment. split_spec = _version_split(normalized_spec) - # Split the prospective version out by dots, and pretend that there - # is an implicit dot in between a release segment and a pre-release - # segment. + # Split the prospective version out by bangs and dots, and pretend + # that there is an implicit dot in between a release segment and + # a pre-release segment. split_prospective = _version_split(normalized_prospective) # 0-pad the prospective version before shortening it to get the correct @@ -645,7 +645,15 @@ def filter( def _version_split(version: str) -> List[str]: result: List[str] = [] - for item in version.split("."): + + epoch, sep, rest = version.rpartition("!") + + if sep: + result.append(epoch) + else: + result.append("0") + + for item in rest.split("."): match = _prefix_regex.search(item) if match: result.extend(match.groups()) @@ -654,6 +662,15 @@ def _version_split(version: str) -> List[str]: return result +def _version_join(components: List[str]) -> str: + # This function only works with numeric components. + assert all(c.isdigit() for c in components) + + epoch, *rest = components + + return epoch + "!" + ".".join(rest) + + def _is_not_suffix(segment: str) -> bool: return not any( segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py index 799f873a..ebcd9d98 100644 --- a/tests/test_specifiers.py +++ b/tests/test_specifiers.py @@ -369,6 +369,7 @@ def test_comparison_non_specifier(self): ("2!1.0", "==2!1.*"), ("2!1.0", "==2!1.0"), ("2!1.0", "!=1.0"), + ("2!1.0.0", "==2!1.0.0.0.*"), ("2!1.0.0", "==2!1.0.*"), ("2!1.0.0", "==2!1.*"), ("1.0", "!=2!1.0"), @@ -467,6 +468,8 @@ def test_comparison_non_specifier(self): ("2!1.0", "~=1.0"), ("2!1.0", "==1.0"), ("1.0", "==2!1.0"), + ("2!1.0", "==1.0.0.*"), + ("1.0", "==2!1.0.0.*"), ("2!1.0", "==1.*"), ("1.0", "==2!1.*"), ("2!1.0", "!=2!1.0"),