Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions src/packaging/specifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ def _coerce_version(version: UnparsedVersion) -> Version | None:
return version


def _public_version(version: Version) -> Version:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this would be cleaner if it was part of version; .local_version could return self if there is no local version, for example; it's a bit of an issue that these returned strings requiring reparsing. But unfortunately, .base_version already has the word "version" in it but returns a string, so not sure there's an obvious way forward with naming if we went that route.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Designing a new API for Version objects to return Version objects doesn't need to block this performance improvement though. We can migrate this to new API once that lands.

I am working on a .replace() method, and I hadn't thought about it, but I can efficiently return self when nothing is modified. That could be the target API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not blocking, already approved. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Actually, I approved #986, which I felt was sort of a prerequisite, but I guess it isn't.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the two PRs are technically independent of each other, they just touch some of the exact same lines, I will need to resolve conflicts once one or the other merges, no issue though, I already know the target state.

"""Skip creation of a new Version instance if no local version to strip."""
if version.local is None:
return version
return Version(version.public)


def _base_version(version: Version) -> Version:
"""Skip creation of a new Version instance if already a base version."""
if (
version.pre is None
and version.post is None
and version.dev is None
and version.local is None
):
return version
return Version(version.base_version)


class InvalidSpecifier(ValueError):
"""
Raised when attempting to create a :class:`Specifier` with a specifier
Expand Down Expand Up @@ -427,7 +446,7 @@ def _compare_equal(self, prospective: Version, spec: str) -> bool:
# act as if the prospective version also does not have a local
# segment.
if not spec_version.local:
prospective = Version(prospective.public)
prospective = _public_version(prospective)

return prospective == spec_version

Expand All @@ -438,13 +457,13 @@ def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
# NB: Local version identifiers are NOT permitted in the version
# specifier, so local version labels can be universally removed from
# the prospective version.
return Version(prospective.public) <= Version(spec)
return _public_version(prospective) <= Version(spec)

def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
# NB: Local version identifiers are NOT permitted in the version
# specifier, so local version labels can be universally removed from
# the prospective version.
return Version(prospective.public) >= Version(spec)
return _public_version(prospective) >= Version(spec)

def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
# Convert our spec to a Version instance, since we'll want to work with
Expand All @@ -464,7 +483,7 @@ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
if (
not spec.is_prerelease
and prospective.is_prerelease
and Version(prospective.base_version) == Version(spec.base_version)
and Version(prospective.base_version) == _base_version(spec)
):
return False

Expand All @@ -491,15 +510,15 @@ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
if (
not spec.is_postrelease
and prospective.is_postrelease
and Version(prospective.base_version) == Version(spec.base_version)
and Version(prospective.base_version) == _base_version(spec)
):
return False

# Ensure that we do not allow a local version of the version mentioned
# in the specifier, which is technically greater than, to match.
if prospective.local is not None and Version(
prospective.base_version
) == Version(spec.base_version):
) == _base_version(spec):
return False

# If we've gotten to here, it means that prospective version is both
Expand Down
1 change: 1 addition & 0 deletions tests/test_specifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ def test_comparison_non_specifier(self) -> None:
("2.0.post1", ">2"),
("2.0.post1.dev1", ">2"),
("2.0+local.version", ">2"),
("1.0+local", ">1.0.dev1"),
# Test the less than operation
("2.0.dev1", "<2"),
("2.0a1", "<2"),
Expand Down
Loading