From f7f187e7d8bac469e6fdb334e6835c8813ae08e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=94=D0=BE=D0=BD=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Sat, 26 Aug 2023 16:51:53 +0300 Subject: [PATCH] Fix specifier matching when the specifier is long and has an epoch `_pad_version` assumes that all components that aren't numeric are suffixes. But that assumption breaks when epoch numbers are present, because `_version_split` outputs a component like "2!1". Fix the assumption by making `_version_split` separate the epoch number into its own component. Introduce `_version_join` to correctly join the output of `_version_split` back into a string, which is needed by `_compare_compatible`. Fixes #683 --- CHANGELOG.rst | 2 ++ src/packaging/specifiers.py | 31 ++++++++++++++++++++++++------- tests/test_specifiers.py | 1 + 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6255260a..c5920f5f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,8 @@ Changelog * Introduce ``metadata.Metadata`` (along with ``metadata.ExceptionGroup`` and ``metadata.InvalidMetadata``; :issue:`570`) * Introduce the ``validate`` keyword parameter to ``utils.validate_name()`` (:issue:`570`) * Introduce ``utils.is_normalized_name()`` (:issue:`570`) +* Do specifier matching correctly when the specifier contains an epoch number + and has more components than the version (:issue:`683`) 23.1 - 2023-04-12 ~~~~~~~~~~~~~~~~~ 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..d18c2d31 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"),