Skip to content

Conversation

@notatallshaw
Copy link
Member

@notatallshaw notatallshaw commented Nov 27, 2025

Takes advantage of being able to strip parts of a Version and get a Version without having to revalidate a version string.

Not had chance to benchmark this yet, but should significantly improve performance of specifier comparison when local, pre, post, or dev parts are involved.

@notatallshaw notatallshaw force-pushed the use-Version.replace-in-specifiers branch from 001fbe4 to eddb254 Compare November 30, 2025 01:59
@notatallshaw notatallshaw changed the title perf: use version.replace in specifiers perf: use version.__replace__ in specifier comparison Nov 30, 2025
@henryiii
Copy link
Contributor

henryiii commented Nov 30, 2025

Would this also make sense to add to this PR?

diff --git a/src/packaging/specifiers.py b/src/packaging/specifiers.py
index f027aeb..2ba5541 100644
--- a/src/packaging/specifiers.py
+++ b/src/packaging/specifiers.py
@@ -420,7 +420,7 @@ class Specifier(BaseSpecifier):
         if spec.endswith(".*"):
             # In the case of prefix matching we want to ignore local segment.
             normalized_prospective = canonicalize_version(
-                prospective.public, strip_trailing_zero=False
+                _public_version(prospective), strip_trailing_zero=False
             )
             # Get the normalized version string ignoring the trailing .*
             normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)

Also _canonical_spec access self._spec[1] directly and converts it to a version inside canonicalize_version, maybe that should use _get_spec_version instead? Also maybe _spec[1] could be cached separately, as _get_spec_version will change the cache to a different value if a different version is passed into it? Unless it's always called with the same value. I also see it used directly in a few other places, like .prerelease.

Full patch:
diff --git a/src/packaging/specifiers.py b/src/packaging/specifiers.py
index f027aeb..18edd34 100644
--- a/src/packaging/specifiers.py
+++ b/src/packaging/specifiers.py
@@ -285,11 +285,13 @@ class Specifier(BaseSpecifier):
             # The == specifier can include a trailing .*, if it does we
             # want to remove before parsing.
             if operator == "==" and version.endswith(".*"):
-                version = version[:-2]
+                version_object = Version(version[:-2])
+            else:
+                version_object = self._get_spec_version(version)

             # Parse the version, and if it is a pre-release than this
             # specifier allows pre-releases.
-            if Version(version).is_prerelease:
+            if version_object.is_prerelease:
                 return True

         return False
@@ -351,7 +353,7 @@ class Specifier(BaseSpecifier):
             return operator, version

         canonical_version = canonicalize_version(
-            version,
+            version if version.endswith(".*") else self._get_spec_version(version),
             strip_trailing_zero=(operator != "~="),
         )

@@ -420,7 +422,7 @@ class Specifier(BaseSpecifier):
         if spec.endswith(".*"):
             # In the case of prefix matching we want to ignore local segment.
             normalized_prospective = canonicalize_version(
-                prospective.public, strip_trailing_zero=False
+                _public_version(prospective), strip_trailing_zero=False
             )
             # Get the normalized version string ignoring the trailing .*
             normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)

@notatallshaw
Copy link
Member Author

Would this also make sense to add to this PR?

Yes, turns out there were two places I was missing public and base version calls where I could have been using them.

Also _canonical_spec access self._spec[1] directly and converts it to a version inside canonicalize_version, maybe that should use _get_spec_version instead? Also maybe _spec[1] could be cached separately, as _get_spec_version will change the cache to a different value if a different version is passed into it? Unless it's always called with the same value. I also see it used directly in a few other places, like .prerelease.

I would need to carefully go over this, the one-element cache is always called with the same value at the moment (assuming you don't mutate the specifier, in which case the cache invalidates itself when you call _get_spec_version), I need to validate that it would always be the same version under _canonical_spec.

@notatallshaw notatallshaw marked this pull request as ready for review November 30, 2025 07:02
@notatallshaw notatallshaw enabled auto-merge (squash) November 30, 2025 19:55
@notatallshaw notatallshaw merged commit 5bf7018 into pypa:main Nov 30, 2025
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants