Skip to content

Commit

Permalink
Fix specifier matching when the specifier is long and has an epoch
Browse files Browse the repository at this point in the history
`_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
  • Loading branch information
SpecLad committed Oct 1, 2023
1 parent cc0c65c commit c648bc0
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 8 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Expand Up @@ -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
~~~~~~~~~~~~~~~~~
Expand Down
31 changes: 24 additions & 7 deletions src/packaging/specifiers.py
Expand Up @@ -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]
)

Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand All @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions tests/test_specifiers.py
Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down

0 comments on commit c648bc0

Please sign in to comment.