Skip to content

Implement Specifiers and SpecifierSets filtering using ranges#1120

Open
notatallshaw wants to merge 8 commits into
pypa:mainfrom
notatallshaw:is-unsatisfiable/specifier_intervals
Open

Implement Specifiers and SpecifierSets filtering using ranges#1120
notatallshaw wants to merge 8 commits into
pypa:mainfrom
notatallshaw:is-unsatisfiable/specifier_intervals

Conversation

@notatallshaw
Copy link
Copy Markdown
Member

@notatallshaw notatallshaw commented Mar 15, 2026

This is a proof of concept of switching the internal mechanics of Specifier and SpecifierSet to using intervals instead of iterative version filters. In general this speeds up any mildly complex specifier and can make very complex specifiers significantly faster (more than 10x).

However this is a large overhaul, first it is intended we land #1119 which implements just enough of this to introduce a new is_unsatisfiable method.

This implementation is designed to make it easy to build a public API on top of this, I imagine having some to_range() method which returns a PEP 440 compliant VersionRange object that can be manipulated with set algebra.

However, to make this more performant, especially for very simple one off cases where it is currently slower, the internal machinery probably needs to move away from bounds logic, implementing hot paths and side channels. Will leave this in draft until after #1119 lands.

@notatallshaw
Copy link
Copy Markdown
Member Author

I'll remove this from pip: pypa/pip#13850

But I think it's best to either wrap a deprecated around it and/or make a public version of "operators".

@henryiii
Copy link
Copy Markdown
Contributor

I played around asking copilot in vscode to optimize this branch. It got about 0.7 or so on both complex and simple filtering. This is still slower for simple specifiers, but it's about 1.2x slower instead of 1.6x or so. Here's the AI generated summary of the things it thinks it optimized:

Yes. Here is a tighter PR-comment version:

Compared against notatallshaw/is-unsatisfiable/specifier_intervals, the optimizations are mostly in five areas:

  1. Exclusion-bound comparisons got cheaper.
  • _ExclusionBound now avoids re-trimming release tuples on every comparison, rejects mismatches earlier, and adds a direct __gt__ fast path.
  • This reduces work in the hot comparison path used by interval membership checks.
  1. Prerelease handling moved into the interval filter.
  • _filter_by_intervals now handles prereleases=None itself, including the PEP 440 “buffer prereleases unless no finals match” behavior.
  • That removes an extra wrapper layer and keeps filtering decisions in one place.
  1. Single-interval cases got a dedicated fast path.
  • Both filtering and membership checks now special-case the common len(intervals) == 1 case.
  • This avoids the generic nested interval loop for simple specifiers.
  1. contains now uses direct interval membership.
  • A new _version_in_intervals helper lets Specifier.contains and SpecifierSet.contains do one parsed-version membership check instead of routing through more generic logic.
  1. SpecifierSet uses cheaper fast paths when possible.
  • Single-specifier sets delegate directly to that one specifier.
  • Multi-specifier sets use cached intersected intervals when possible.
  • The slower per-specifier fallback is now mostly reserved for === cases.

And the diff:

Details
diff --git a/src/packaging/specifiers.py b/src/packaging/specifiers.py
index c341232..62cc181 100644
--- a/src/packaging/specifiers.py
+++ b/src/packaging/specifiers.py
@@ -84,12 +84,22 @@ class _ExclusionBound:
     def _is_family(self, other: Version) -> bool:
         """Is ``other`` a version that this sentinel sorts above?"""
         v = self.version
-        if not (
-            other.epoch == v.epoch
-            and _trim_release(other.release) == self._trimmed_release
-            and other.pre == v.pre
-        ):
+        if other.epoch != v.epoch or other.pre != v.pre:
+            return False
+
+        # Compare trimmed release equality without allocating a new tuple
+        # for ``other.release`` on each call.
+        other_release = other.release
+        trimmed = self._trimmed_release
+        if len(other_release) < len(trimmed):
             return False
+        for idx, value in enumerate(trimmed):
+            if other_release[idx] != value:
+                return False
+        for value in other_release[len(trimmed) :]:
+            if value != 0:
+                return False
+
         if self._kind == _AFTER_LOCALS:
             # Local family: exact same public version (any local label).
             return other.post == v.post and other.dev == v.dev
@@ -107,8 +117,25 @@ class _ExclusionBound:
                 return self.version < other.version
             return self._kind < other._kind
         assert isinstance(other, Version)
+        # Cheap reject first: if ``other`` is not above ``V``,
+        # ``self < other`` can never hold.
+        if not (self.version < other):
+            return False
         # self < other iff other is NOT in the family and other > V
-        return not self._is_family(other) and self.version < other
+        return not self._is_family(other)
+
+    def __gt__(self, other: object) -> bool:
+        if isinstance(other, _ExclusionBound):
+            if self.version != other.version:
+                return self.version > other.version
+            return self._kind > other._kind
+        assert isinstance(other, Version)
+        # Fast path: base version already dominates — no family check needed.
+        if self.version >= other:
+            return True
+        # Slower path: other > V, but might still be in the family
+        # (e.g. a post-release counted as V.postN with AFTER_POSTS semantics).
+        return self._is_family(other)
 
     def __hash__(self) -> int:
         return hash((self.version, self._kind))
@@ -203,7 +230,7 @@ def _filter_by_intervals(
     intervals: list[_SpecifierInterval],
     iterable: Iterable[Any],
     key: Callable[[Any], UnparsedVersion] | None,
-    prereleases: bool,
+    prereleases: bool | None,
 ) -> Iterator[Any]:
     """Filter versions against precomputed intervals.
 
@@ -211,12 +238,116 @@ def _filter_by_intervals(
     use :class:`_ExclusionBound` to handle local-version semantics.
 
     Used by both :class:`Specifier` and :class:`SpecifierSet`.
-    Prerelease buffering (PEP 440 default) is NOT handled here;
-    callers wrap the result with :func:`_pep440_filter_prereleases`
-    when needed.
+    When ``prereleases`` is ``None``, PEP 440 default semantics apply:
+    prereleases are excluded unless no final releases match.
     """
+    if not intervals:
+        return
+
+    # PEP 440 default behavior: exclude prereleases unless no finals match.
+    if prereleases is None:
+        prereleases_buffer: list[Any] = []
+        found_final = False
+
+        if len(intervals) == 1:
+            (
+                (lower_version, lower_inclusive),
+                (
+                    upper_version,
+                    upper_inclusive,
+                ),
+            ) = intervals[0]
+
+            for item in iterable:
+                parsed = _coerce_version(item if key is None else key(item))
+                if parsed is None:
+                    continue
+                if lower_version is not None:
+                    if lower_inclusive:
+                        if parsed < lower_version:
+                            continue
+                    elif not (parsed > lower_version):
+                        continue
+                if upper_version is not None:
+                    if upper_inclusive:
+                        if parsed > upper_version:
+                            continue
+                    elif not (parsed < upper_version):
+                        continue
+                if parsed.is_prerelease:
+                    prereleases_buffer.append(item)
+                else:
+                    found_final = True
+                    yield item
+            if not found_final:
+                yield from prereleases_buffer
+            return
+
+        for item in iterable:
+            parsed = _coerce_version(item if key is None else key(item))
+            if parsed is None:
+                continue
+            # Check if version falls within any interval. Intervals are sorted
+            # and non-overlapping, so at most one can match.
+            for (lower_version, lower_inclusive), (
+                upper_version,
+                upper_inclusive,
+            ) in intervals:
+                if lower_version is not None:
+                    if lower_inclusive:
+                        if parsed < lower_version:
+                            break
+                    elif not (parsed > lower_version):
+                        break
+                if upper_version is None:
+                    matched = True
+                elif upper_inclusive:
+                    matched = not (parsed > upper_version)
+                else:
+                    matched = parsed < upper_version
+                if matched:
+                    if parsed.is_prerelease:
+                        prereleases_buffer.append(item)
+                    else:
+                        found_final = True
+                        yield item
+                    break
+        if not found_final:
+            yield from prereleases_buffer
+        return
+
     exclude_prereleases = prereleases is False
 
+    if len(intervals) == 1:
+        (
+            (lower_version, lower_inclusive),
+            (
+                upper_version,
+                upper_inclusive,
+            ),
+        ) = intervals[0]
+
+        for item in iterable:
+            parsed = _coerce_version(item if key is None else key(item))
+            if parsed is None:
+                continue
+            if exclude_prereleases and parsed.is_prerelease:
+                continue
+            if lower_version is not None:
+                if lower_inclusive:
+                    if parsed < lower_version:
+                        continue
+                elif not (parsed > lower_version):
+                    continue
+            if upper_version is not None:
+                if upper_inclusive:
+                    if parsed > upper_version:
+                        continue
+                elif not (parsed < upper_version):
+                    continue
+            yield item
+        return
+
     for item in iterable:
         parsed = _coerce_version(item if key is None else key(item))
         if parsed is None:
@@ -229,20 +360,67 @@ def _filter_by_intervals(
             upper_version,
             upper_inclusive,
         ) in intervals:
-            if lower_version is not None and (
-                parsed < lower_version
-                or (parsed == lower_version and not lower_inclusive)
-            ):
-                break
-            if (
-                upper_version is None
-                or parsed < upper_version
-                or (parsed == upper_version and upper_inclusive)
-            ):
+            if lower_version is not None:
+                if lower_inclusive:
+                    if parsed < lower_version:
+                        break
+                elif not (parsed > lower_version):
+                    break
+            if upper_version is None:
+                matched = True
+            elif upper_inclusive:
+                matched = not (parsed > upper_version)
+            else:
+                matched = parsed < upper_version
+            if matched:
                 yield item
                 break
 
 
+def _version_in_intervals(
+    version: Version, intervals: list[_SpecifierInterval]
+) -> bool:
+    """Return whether ``version`` falls within any of ``intervals``."""
+    if not intervals:
+        return False
+
+    if len(intervals) == 1:
+        (
+            (lower_version, lower_inclusive),
+            (
+                upper_version,
+                upper_inclusive,
+            ),
+        ) = intervals[0]
+        if lower_version is not None:
+            if lower_inclusive:
+                if version < lower_version:
+                    return False
+            elif not (version > lower_version):
+                return False
+        if upper_version is None:
+            return True
+        if upper_inclusive:
+            return not (version > upper_version)
+        return version < upper_version
+
+    for (lower_version, lower_inclusive), (upper_version, upper_inclusive) in intervals:
+        if lower_version is not None:
+            if lower_inclusive:
+                if version < lower_version:
+                    break
+            elif not (version > lower_version):
+                break
+        if upper_version is None:
+            return True
+        if upper_inclusive:
+            if not (version > upper_version):
+                return True
+        elif version < upper_version:
+            return True
+    return False
+
+
 def _pep440_filter_prereleases(
     iterable: Iterable[Any], key: Callable[[Any], UnparsedVersion] | None
 ) -> Iterator[Any]:
@@ -820,7 +998,24 @@ class Specifier(BaseSpecifier):
         True
         """
 
-        return bool(list(self.filter([item], prereleases=prereleases)))
+        if self.operator == "===":
+            return str(item).lower() == self.version.lower()
+
+        version = _coerce_version(item)
+        if version is None:
+            return False
+
+        if prereleases is None:
+            if self._prereleases is not None:
+                prereleases = self._prereleases
+            elif self.prereleases:
+                prereleases = True
+
+        resolve_pre = True if prereleases is None else prereleases
+        if not resolve_pre and version.is_prerelease:
+            return False
+
+        return _version_in_intervals(version, self._to_intervals())
 
     @typing.overload
     def filter(
@@ -890,22 +1085,15 @@ class Specifier(BaseSpecifier):
             elif self.prereleases:
                 prereleases = True
 
-        # When prereleases is still None, pass True to include all versions
-        # and let _pep440_filter_prereleases handle the buffering.
-        resolve_pre = True if prereleases is None else prereleases
-
-        filtered = _filter_by_intervals(
+        # _filter_by_intervals handles prereleases=None (PEP 440 semantics)
+        # directly, so no wrapper needed.
+        yield from _filter_by_intervals(
             self._to_intervals(),
             iterable,
             key,
-            prereleases=resolve_pre,
+            prereleases=prereleases,
         )
 
-        if prereleases is not None:
-            yield from filtered
-        else:
-            yield from _pep440_filter_prereleases(filtered, key)
-
 
 class SpecifierSet(BaseSpecifier):
     """This class abstracts handling of a set of version specifiers.
@@ -1244,8 +1432,30 @@ class SpecifierSet(BaseSpecifier):
         if version is not None and installed and version.is_prerelease:
             prereleases = True
 
-        check_item = item if version is None else version
-        return bool(list(self.filter([check_item], prereleases=prereleases)))
+        if prereleases is None:
+            default_prereleases = self.prereleases
+            if default_prereleases is not None:
+                prereleases = default_prereleases
+
+        allow_prereleases = True if prereleases is None else prereleases
+
+        if self._specs:
+            intervals = self._get_intervals()
+            if intervals is not None:
+                if version is None:
+                    return False
+                if not allow_prereleases and version.is_prerelease:
+                    return False
+                return _version_in_intervals(version, intervals)
+
+            candidate = item if version is None else version
+            return all(
+                s.contains(candidate, prereleases=allow_prereleases)
+                for s in self._specs
+            )
+
+        # Empty SpecifierSet matches everything unless prereleases are disabled.
+        return allow_prereleases or version is None or not version.is_prerelease
 
     @typing.overload
     def filter(
@@ -1313,37 +1523,48 @@ class SpecifierSet(BaseSpecifier):
         # Determine if we're forcing a prerelease or not, if we're not forcing
         # one for this particular filter call, then we'll use whatever the
         # SpecifierSet thinks for whether or not we should support prereleases.
-        if prereleases is None and self.prereleases is not None:
-            prereleases = self.prereleases
+        if prereleases is None:
+            default_prereleases = self.prereleases
+            if default_prereleases is not None:
+                prereleases = default_prereleases
 
         # Filter versions that match all specifiers.
         if self._specs:
-            resolve_pre = True if prereleases is None else prereleases
+            # Fast path: a single specifier can delegate directly.
+            # This avoids an extra PEP 440 pass in the common one-spec case.
+            if len(self._specs) == 1:
+                return self._specs[0].filter(
+                    iterable,
+                    prereleases=prereleases,
+                    key=key,
+                )
 
-            filtered: Iterator[Any]
             intervals = self._get_intervals()
             if intervals is not None:
-                filtered = _filter_by_intervals(
+                # _filter_by_intervals handles prereleases=None (PEP 440
+                # semantics) directly.
+                return _filter_by_intervals(
                     intervals,
                     iterable,
                     key,
-                    prereleases=resolve_pre,
+                    prereleases=prereleases,
                 )
-            else:
-                # _get_intervals returns None when specs include ===
-                # (arbitrary string matching, not version comparison).
-                specs = self._specs
-                filtered = (
-                    item
-                    for item in iterable
-                    if all(
-                        s.contains(
-                            item if key is None else key(item),
-                            prereleases=resolve_pre,
-                        )
-                        for s in specs
+
+            # _get_intervals returns None when specs include ===
+            # (arbitrary string matching, not version comparison).
+            allow_prereleases = True if prereleases is None else prereleases
+            specs = self._specs
+            filtered: Iterator[Any] = (
+                item
+                for item in iterable
+                if all(
+                    s.contains(
+                        item if key is None else key(item),
+                        prereleases=allow_prereleases,
                     )
+                    for s in specs
                 )
+            )
 
             if prereleases is not None:
                 return filtered

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch 6 times, most recently from ae9865c to 489edcf Compare March 27, 2026 13:44
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch 3 times, most recently from 7ccace1 to 522b695 Compare March 28, 2026 16:01
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch 6 times, most recently from 71684a2 to c39cac8 Compare April 12, 2026 05:45
@notatallshaw notatallshaw changed the title [PoC] Reimplement Specifiers and SpecifierSets using intervals [PoC] Reimplement Specifiers and SpecifierSets using ranges Apr 12, 2026
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch 5 times, most recently from 0f3f135 to 2ac24b5 Compare April 12, 2026 16:07
@notatallshaw
Copy link
Copy Markdown
Member Author

notatallshaw commented Apr 12, 2026

I've rebased and cleaned this up now #1119 has been merged, it's going to be a few weeks before I can start working on this, but my plan is the following:

  1. Improve the performance of this PR so there is no performance regression (and in many places significant performance improvement) over main
  2. Open a second PR, that will be based on top of his PR, that will provide a Public API for ranges

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch from 2ac24b5 to 92a7a46 Compare May 2, 2026 15:24
@notatallshaw notatallshaw changed the title [PoC] Reimplement Specifiers and SpecifierSets using ranges Implement Specifiers and SpecifierSets filtering using ranges May 2, 2026
@notatallshaw notatallshaw marked this pull request as ready for review May 2, 2026 15:38
@notatallshaw
Copy link
Copy Markdown
Member Author

This PR is now ready for review, it is completely overhauled but keeps the same concepts and logic. The diff was bigger than expected, because this PR is a stepping stone to a public VersionRanges API and that was a lot more work than I anticpated.

This PR now does all the range based calculations in a new private _ranges.py but keeps most of the PEP 440 special case handling in specifiers.py.

Performance is now improved for all contains and filter operations, without performance regression anywhere else. Performance improvement is especially dramatic for complex specifiers with lots of versions, as after the initial range construction version filtering has gone from O(n) to O(1):

All benchmarks:

| Change   | Before [966bbd2c]    | After [9229f94d]    |   Ratio | Benchmark (Parameter)                                                                                  |
|----------|----------------------|---------------------|---------|--------------------------------------------------------------------------------------------------------|
|          | 5.55±0.06ms          | 5.50±0.05ms         |    0.99 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]             |
|          | 3.69±0.03ms          | 3.72±0.04ms         |    1.01 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]             |
|          | 4.44±0.07ms          | 4.39±0.03ms         |    0.99 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]             |
|          | 4.23±0.04ms          | 4.26±0.02ms         |    1.01 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]             |
|          | 4.19±0.02ms          | 4.24±0.06ms         |    1.01 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]             |
|          | 5.82±0.06ms          | 5.84±0.06ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]              |
|          | 1.72±0.01ms          | 1.75±0.02ms         |    1.02 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]                |
|          | 1.14±0.01ms          | 1.16±0.01ms         |    1.02 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]                |
|          | 1.28±0.01ms          | 1.34±0.01ms         |    1.05 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]                |
|          | 1.24±0.02ms          | 1.27±0.02ms         |    1.02 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]                |
|          | 1.20±0.01ms          | 1.26±0.01ms         |    1.05 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]                |
|          | 1.69±0.02ms          | 1.76±0.03ms         |    1.04 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                 |
|          | 17.3±0.08ms          | 17.4±0.1ms          |    1.01 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]    |
|          | 11.6±0.2ms           | 11.7±0.09ms         |    1.01 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]    |
|          | 13.7±0.2ms           | 13.6±0.08ms         |    0.99 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]    |
|          | 12.7±0.1ms           | 12.7±0.1ms          |    1    | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]    |
|          | 12.7±0.09ms          | 12.7±0.1ms          |    1    | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]    |
|          | 18.5±0.09ms          | 18.5±0.1ms          |    1    | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]     |
| -        | 890±6μs              | 698±3μs             |    0.78 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]        |
| -        | 606±4μs              | 470±5μs             |    0.78 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]        |
| -        | 650±4μs              | 494±4μs             |    0.76 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]        |
| -        | 604±4μs              | 475±3μs             |    0.79 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]        |
| -        | 570±3μs              | 451±3μs             |    0.79 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]        |
| -        | 959±2μs              | 785±3μs             |    0.82 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]         |
|          | 2.32±0.03ms          | 2.29±0.02ms         |    0.98 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]            |
|          | 1.89±0.01ms          | 1.89±0.01ms         |    1    | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]            |
|          | 2.15±0.02ms          | 2.13±0.01ms         |    0.99 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]            |
|          | 1.91±0.01ms          | 1.90±0.01ms         |    1    | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]            |
|          | 1.88±0.02ms          | 1.88±0.01ms         |    1    | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]            |
|          | 2.29±0.01ms          | 2.27±0.04ms         |    0.99 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]             |
| -        | 7.67±0.07ms          | 5.44±0.06ms         |    0.71 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]          |
| -        | 5.12±0.04ms          | 3.19±0.03ms         |    0.62 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]          |
| -        | 5.66±0.04ms          | 3.59±0.03ms         |    0.63 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]          |
| -        | 5.18±0.03ms          | 3.32±0.02ms         |    0.64 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]          |
| -        | 5.07±0.03ms          | 3.14±0.04ms         |    0.62 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]          |
| -        | 8.13±0.06ms          | 6.63±0.07ms         |    0.82 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]           |
| -        | 1.68±0.01ms          | 497±3μs             |    0.3  | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]  |
| -        | 1.24±0.01ms          | 325±1μs             |    0.26 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]  |
| -        | 1.36±0ms             | 315±3μs             |    0.23 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]  |
| -        | 1.20±0.01ms          | 307±4μs             |    0.26 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]  |
| -        | 1.16±0ms             | 297±3μs             |    0.26 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]  |
| -        | 1.80±0ms             | 566±8μs             |    0.32 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]   |
| -        | 3.94±0.02ms          | 2.30±0.01ms         |    0.58 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]          |
| -        | 2.66±0.02ms          | 1.42±0.01ms         |    0.53 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]          |
| -        | 2.82±0.01ms          | 1.50±0.01ms         |    0.53 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]          |
| -        | 2.60±0.03ms          | 1.43±0.01ms         |    0.55 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]          |
| -        | 2.58±0.03ms          | 1.41±0.01ms         |    0.55 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]          |
| -        | 4.35±0.04ms          | 2.76±0.01ms         |    0.63 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]           |
| -        | 938±10μs             | 70.1±0.7μs          |    0.07 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0] |
| -        | 711±8μs              | 44.9±0.4μs          |    0.06 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0] |
| -        | 801±4μs              | 43.9±0.4μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0] |
| -        | 674±4μs              | 42.8±0.4μs          |    0.06 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0] |
| -        | 683±3μs              | 42.9±0.3μs          |    0.06 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0] |
| -        | 964±3μs              | 77.5±0.4μs          |    0.08 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]  |
| -        | 1.23±0.01ms          | 62.7±0.5μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]    |
| -        | 919±7μs              | 41.0±0.4μs          |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]    |
| -        | 1.05±0ms             | 43.7±0.3μs          |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]    |
| -        | 879±5μs              | 43.0±0.4μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]    |
| -        | 875±3μs              | 42.9±0.3μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]    |
| -        | 1.29±0.01ms          | 70.5±1μs            |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]     |
| -        | 1.19±0.01ms          | 55.4±0.7μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]    |
| -        | 900±10μs             | 35.9±0.4μs          |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]    |
| -        | 1.02±0.01ms          | 38.6±0.4μs          |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]    |
| -        | 845±5μs              | 38.2±0.3μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]    |
| -        | 846±3μs              | 37.9±0.2μs          |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]    |
| -        | 1.24±0.01ms          | 64.0±0.5μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]     |
| -        | 20.8±0.1μs           | 11.3±0.2μs          |    0.54 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]     |
| -        | 13.0±0.04μs          | 7.24±0.09μs         |    0.56 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]     |
| -        | 11.4±0.06μs          | 6.93±0.06μs         |    0.61 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]     |
| -        | 11.0±0.1μs           | 6.71±0.07μs         |    0.61 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]     |
| -        | 10.4±0.05μs          | 6.73±0.03μs         |    0.65 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]     |
| -        | 21.5±0.2μs           | 12.0±0.09μs         |    0.56 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]      |
| -        | 17.9±0.2μs           | 8.87±0.05μs         |    0.5  | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]     |
| -        | 11.1±0.08μs          | 5.78±0.09μs         |    0.52 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]     |
| -        | 9.29±0.02μs          | 5.22±0.06μs         |    0.56 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]     |
| -        | 9.25±0.09μs          | 5.24±0.07μs         |    0.57 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]     |
| -        | 8.53±0.1μs           | 5.32±0.05μs         |    0.62 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]     |
| -        | 18.4±0.09μs          | 9.78±0.06μs         |    0.53 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]      |
|          | 4.85±0.1μs           | 4.85±0.05μs         |    1    | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]               |
|          | 3.55±0.04μs          | 3.58±0.02μs         |    1.01 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]               |
|          | 3.59±0.03μs          | 3.62±0.09μs         |    1.01 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]               |
|          | 3.63±0.01μs          | 3.61±0.03μs         |    1    | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]               |
|          | 3.81±0.03μs          | 3.86±0.04μs         |    1.01 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]               |
|          | 5.11±0.04μs          | 5.06±0.05μs         |    0.99 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                |
|          | 2.66±0.02ms          | 2.64±0.02ms         |    0.99 | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]            |
|          | 1.65±0.01ms          | 1.65±0.01ms         |    1    | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]            |
|          | 2.14±0.01ms          | 2.14±0.01ms         |    1    | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]            |
|          | 1.90±0.02ms          | 1.91±0.01ms         |    1    | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]            |
|          | 1.70±0.02ms          | 1.71±0.01ms         |    1    | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]            |
|          | 2.69±0.02ms          | 2.71±0.01ms         |    1.01 | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]             |
|          | 3.65±0.05ms          | 3.56±0.05ms         |    0.98 | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]                   |
|          | 2.27±0.02ms          | 2.26±0.01ms         |    0.99 | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]                   |
|          | 2.84±0.03ms          | 2.81±0.02ms         |    0.99 | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]                   |
|          | 2.59±0.03ms          | 2.58±0.03ms         |    1    | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]                   |
|          | 2.40±0.03ms          | 2.40±0.02ms         |    1    | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]                   |
|          | 3.73±0.04ms          | 3.76±0.02ms         |    1.01 | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                    |
|          | 165±3μs              | 164±2μs             |    1    | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]              |
|          | 106±0.7μs            | 105±2μs             |    1    | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]              |
|          | 132±2μs              | 135±3μs             |    1.03 | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]              |
|          | 129±4μs              | 129±1μs             |    1    | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]              |
|          | 136±2μs              | 137±2μs             |    1    | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]              |
|          | 187±2μs              | 183±4μs             |    0.98 | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]               |
|          | 2.67±0.02ms          | 2.65±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]              |
|          | 1.90±0.01ms          | 1.91±0.02ms         |    1.01 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]              |
|          | 2.24±0.01ms          | 2.21±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]              |
|          | 2.27±0.04ms          | 2.27±0ms            |    1    | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]              |
|          | 2.22±0.03ms          | 2.21±0.02ms         |    1    | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]              |
|          | 3.33±0.03ms          | 3.36±0.04ms         |    1.01 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]               |
|          | 2.05±0.02ms          | 2.04±0.02ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]              |
|          | 1.52±0.02ms          | 1.55±0.01ms         |    1.02 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]              |
|          | 1.83±0.03ms          | 1.83±0.01ms         |    1    | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]              |
|          | 1.85±0.01ms          | 1.84±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]              |
|          | 1.82±0.03ms          | 1.80±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]              |
|          | 2.57±0.01ms          | 2.57±0.06ms         |    1    | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]               |
|          | 4.41±0.08ms          | 4.44±0.05ms         |    1.01 | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]                    |
|          | 3.00±0.03ms          | 3.02±0.02ms         |    1.01 | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]                    |
|          | 3.52±0.01ms          | 3.51±0.02ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]                    |
|          | 3.13±0.03ms          | 3.11±0.02ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]                    |
|          | 3.03±0.03ms          | 3.03±0.04ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]                    |
|          | 4.72±0.02ms          | 4.73±0.03ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                     |

SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE INCREASED.

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented May 5, 2026

This seems good, I'm not seeing anything significant. I'll trigger a copilot review just to see if it sees anything.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request refactors packaging.specifiers.Specifier and SpecifierSet filtering to use precomputed PEP 440 “version ranges” (intervals) rather than repeatedly applying per-version operator predicates, with the goal of significantly improving performance for complex specifiers.

Changes:

  • Introduces a new private _ranges module that models specifiers as sorted, non-overlapping version ranges and provides range intersection + filtering utilities.
  • Reworks Specifier/SpecifierSet to lazily compute/cache ranges and use range-based filtering and unsatisfiability checks (including prerelease-only range detection).
  • Updates and expands tests to cover new caching behavior, boundary semantics, prerelease filtering behavior, and pickle cache reset/loading scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/packaging/specifiers.py Switches specifier mechanics to range-based filtering/intersection, updates caching and prerelease/unsatisfiable handling to use _ranges.
src/packaging/_ranges.py Adds private range/boundary primitives plus range intersection, filtering, and prerelease-only range detection helpers.
tests/test_specifiers.py Updates expectations around caching and pickle cache reset; adds regression tests for multirange + prerelease filtering and lazy construction.
tests/test_version.py Adds coverage for a Version.__gt__ comparison fast-path involving cached comparison keys.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/packaging/specifiers.py
Comment thread src/packaging/specifiers.py Outdated
@notatallshaw
Copy link
Copy Markdown
Member Author

Looking at the benchmark numbers again I see marker evaluation is a few % slower, I'm going to quickly profile and see if this can be simply fixed.

There are actually a lot of minor performance optimizations I haven't included due to the tradeoff in complexity.

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented May 5, 2026

I noticed that, but it was under 5% so wasn't too worried about it, but if you see a quick fix, that's good. I'd focus on readability for a first iteration, so if it's too ugly to fix now, that could be a followup.

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch from e9da7e4 to e524f48 Compare May 9, 2026 15:49
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch from 472c258 to cea3be1 Compare May 9, 2026 18:43
@notatallshaw
Copy link
Copy Markdown
Member Author

Bad news/Good news:

Bad news, investigating the marker performance loss I found that the cold Specifier.contains benchmark was not clearing out the new caches, fixing that shows that for a single cold Specifier.contains we still have a noticeable performance loss:

| +        | 8.85±0.06ms          | 13.8±0.2ms          |    1.56 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]          |
| +        | 5.82±0.06ms          | 9.25±0.1ms          |    1.59 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]          |
| +        | 6.47±0.07ms          | 9.69±0.3ms          |    1.5  | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]          |
| +        | 6.11±0.2ms           | 9.39±0.2ms          |    1.54 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]          |
| +        | 6.22±0.09ms          | 9.22±0.09ms         |    1.48 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]          |
| +        | 9.11±0.07ms          | 15.1±0.2ms          |    1.65 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]           |

Good news, rather than implementing a full fallback to the old code we can add a fairly simple fast path for when we've not constructed a range object yet and the comparison reduces down to a simple Version comparison: cea3be1

This actually speeds up the marker evaluation benchmark a couple of percent:

All benchmarks:

| Change   | Before [4772432b]    | After [731cfb1a]    |   Ratio | Benchmark (Parameter)                                                                                  |
|----------|----------------------|---------------------|---------|--------------------------------------------------------------------------------------------------------|
|          | 5.82±0.1ms           | 5.75±0.07ms         |    0.99 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]             |
|          | 3.97±0.05ms          | 3.97±0.04ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]             |
|          | 4.63±0.03ms          | 4.65±0.07ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]             |
|          | 4.54±0.03ms          | 4.55±0.05ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]             |
|          | 4.60±0.03ms          | 4.54±0.07ms         |    0.99 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]             |
|          | 5.97±0.05ms          | 6.00±0.06ms         |    1.01 | markers.TimeMarkerSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]              |
|          | 1.82±0.01ms          | 1.78±0.01ms         |    0.98 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]                |
|          | 1.22±0.01ms          | 1.17±0.02ms         |    0.96 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]                |
|          | 1.38±0.02ms          | 1.32±0.01ms         |    0.95 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]                |
|          | 1.33±0.02ms          | 1.29±0.01ms         |    0.97 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]                |
|          | 1.34±0.01ms          | 1.28±0ms            |    0.95 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]                |
|          | 1.83±0.01ms          | 1.77±0.02ms         |    0.97 | markers.TimeMarkerSuite.time_evaluate [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                 |
|          | 20.5±0.1ms           | 20.4±0.1ms          |    1    | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]    |
|          | 14.4±0.2ms           | 14.6±0.2ms          |    1.01 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]    |
|          | 16.4±0.3ms           | 16.3±0.2ms          |    0.99 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]    |
|          | 16.5±0.09ms          | 16.4±0.1ms          |    0.99 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]    |
|          | 16.4±0.1ms           | 16.5±0.2ms          |    1.01 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]    |
|          | 21.7±0.1ms           | 21.8±0.1ms          |    1.01 | requirement.TimeRequirementSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]     |
| -        | 953±3μs              | 708±8μs             |    0.74 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]        |
| -        | 657±3μs              | 488±7μs             |    0.74 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]        |
| -        | 712±4μs              | 493±3μs             |    0.69 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]        |
| -        | 676±7μs              | 470±3μs             |    0.7  | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]        |
| -        | 650±8μs              | 454±3μs             |    0.7  | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]        |
| -        | 1.02±0.02ms          | 766±5μs             |    0.75 | resolver.TimeResolverSuite.time_resolver_loop [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]         |
|          | 2.40±0.01ms          | 2.38±0.01ms         |    0.99 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]            |
|          | 1.98±0.01ms          | 2.01±0.03ms         |    1.02 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]            |
|          | 2.23±0.01ms          | 2.24±0.03ms         |    1.01 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]            |
|          | 1.95±0.02ms          | 2.00±0.03ms         |    1.02 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]            |
|          | 1.98±0.02ms          | 1.96±0.02ms         |    0.99 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]            |
|          | 2.37±0.03ms          | 2.32±0.01ms         |    0.98 | specifiers.TimeSpecSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]             |
|          | 8.85±0.06ms          | 9.27±0.09ms         |    1.05 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]          |
|          | 5.87±0.03ms          | 5.98±0.07ms         |    1.02 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]          |
|          | 6.60±0.09ms          | 6.57±0.1ms          |    0.99 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]          |
|          | 6.15±0.05ms          | 6.07±0.07ms         |    0.99 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]          |
|          | 6.27±0.05ms          | 5.98±0.1ms          |    0.95 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]          |
|          | 9.24±0.06ms          | 9.93±0.05ms         |    1.07 | specifiers.TimeSpecSuite.time_contains_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]           |
| -        | 1.75±0.02ms          | 487±2μs             |    0.28 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]  |
| -        | 1.29±0.01ms          | 330±5μs             |    0.26 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]  |
| -        | 1.47±0.01ms          | 321±3μs             |    0.22 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]  |
| -        | 1.27±0.01ms          | 318±3μs             |    0.25 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]  |
| -        | 1.30±0.01ms          | 304±2μs             |    0.23 | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]  |
| -        | 1.90±0.02ms          | 560±5μs             |    0.3  | specifiers.TimeSpecSuite.time_contains_complex_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]   |
| -        | 4.19±0.03ms          | 1.65±0.01ms         |    0.39 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]          |
| -        | 2.93±0.02ms          | 1.03±0.01ms         |    0.35 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]          |
| -        | 3.09±0.03ms          | 1.00±0.01ms         |    0.32 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]          |
| -        | 2.92±0.05ms          | 928±9μs             |    0.32 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]          |
| -        | 3.00±0.03ms          | 876±10μs            |    0.29 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]          |
| -        | 4.50±0.03ms          | 1.92±0.02ms         |    0.43 | specifiers.TimeSpecSuite.time_contains_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]           |
| -        | 941±10μs             | 67.9±0.6μs          |    0.07 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0] |
| -        | 723±4μs              | 44.0±0.3μs          |    0.06 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0] |
| -        | 845±4μs              | 42.8±0.2μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0] |
| -        | 719±10μs             | 43.0±1μs            |    0.06 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0] |
| -        | 736±4μs              | 40.5±0.4μs          |    0.06 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0] |
| -        | 1.00±0.01ms          | 76.9±1μs            |    0.08 | specifiers.TimeSpecSuite.time_filter_compatible_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]  |
| -        | 1.25±0.01ms          | 192±2μs             |    0.15 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]    |
| -        | 968±5μs              | 125±0.8μs           |    0.13 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]    |
| -        | 1.14±0.01ms          | 134±0.6μs           |    0.12 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]    |
| -        | 950±10μs             | 131±2μs             |    0.14 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]    |
| -        | 977±8μs              | 129±2μs             |    0.13 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]    |
| -        | 1.33±0.01ms          | 210±0.6μs           |    0.16 | specifiers.TimeSpecSuite.time_filter_complex_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]     |
| -        | 1.19±0.01ms          | 54.5±0.6μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]    |
| -        | 921±10μs             | 34.7±0.2μs          |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]    |
| -        | 1.09±0ms             | 37.2±0.2μs          |    0.03 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]    |
| -        | 911±5μs              | 38.6±0.05μs         |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]    |
| -        | 938±20μs             | 36.3±0.3μs          |    0.04 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]    |
| -        | 1.28±0.01ms          | 63.5±0.4μs          |    0.05 | specifiers.TimeSpecSuite.time_filter_complex_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]     |
| -        | 19.7±0.2μs           | 16.7±0.2μs          |    0.85 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]     |
| -        | 12.8±0.04μs          | 10.8±0.1μs          |    0.84 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]     |
| -        | 11.4±0.04μs          | 10.2±0.07μs         |    0.89 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]     |
| -        | 11.1±0.04μs          | 9.64±0.04μs         |    0.87 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]     |
|          | 10.7±0.09μs          | 9.83±0.1μs          |    0.92 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]     |
| -        | 20.8±0.2μs           | 18.6±0.2μs          |    0.89 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]      |
| -        | 16.5±0.2μs           | 8.74±0.08μs         |    0.53 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]     |
| -        | 11.0±0.1μs           | 5.79±0.09μs         |    0.53 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]     |
| -        | 9.12±0.04μs          | 5.29±0.04μs         |    0.58 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]     |
| -        | 9.21±0.1μs           | 5.23±0.04μs         |    0.57 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]     |
| -        | 8.74±0.06μs          | 5.51±0.07μs         |    0.63 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]     |
| -        | 17.7±0.2μs           | 9.62±0.1μs          |    0.54 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]      |
|          | 5.14±0.05μs          | 5.10±0.04μs         |    0.99 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]               |
|          | 3.61±0.04μs          | 3.61±0.02μs         |    1    | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]               |
|          | 3.73±0.02μs          | 3.74±0.06μs         |    1    | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]               |
|          | 3.78±0.04μs          | 3.73±0.04μs         |    0.99 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]               |
|          | 3.92±0.05μs          | 3.95±0.04μs         |    1.01 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]               |
|          | 5.47±0.01μs          | 5.58±0.02μs         |    1.02 | utils.TimeUtils.time_canonicalize_name [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                |
|          | 2.82±0.01ms          | 2.81±0.01ms         |    0.99 | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]            |
|          | 1.74±0.02ms          | 1.72±0.01ms         |    0.99 | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]            |
|          | 2.25±0.01ms          | 2.25±0.02ms         |    1    | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]            |
|          | 1.99±0.03ms          | 2.00±0.02ms         |    1.01 | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]            |
|          | 1.78±0.02ms          | 1.77±0.02ms         |    0.99 | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]            |
|          | 2.88±0.05ms          | 2.87±0.01ms         |    0.99 | version.TimeVersionSuite.time_constructor [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]             |
|          | 3.68±0.02ms          | 3.67±0.02ms         |    1    | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]                   |
|          | 2.39±0.02ms          | 2.36±0.01ms         |    0.99 | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]                   |
|          | 2.95±0.01ms          | 2.95±0.01ms         |    1    | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]                   |
|          | 2.65±0.01ms          | 2.64±0.01ms         |    1    | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]                   |
|          | 2.50±0.02ms          | 2.48±0.04ms         |    0.99 | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]                   |
|          | 3.84±0.02ms          | 3.94±0.04ms         |    1.03 | version.TimeVersionSuite.time_hash [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                    |
|          | 170±1μs              | 172±1μs             |    1.01 | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]              |
|          | 104±2μs              | 103±0.8μs           |    0.99 | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]              |
|          | 128±1μs              | 127±0.9μs           |    1    | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]              |
|          | 132±0.5μs            | 132±4μs             |    1    | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]              |
|          | 133±1μs              | 132±3μs             |    1    | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]              |
|          | 183±3μs              | 180±1μs             |    0.98 | version.TimeVersionSuite.time_hash_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]               |
|          | 2.74±0.04ms          | 2.70±0.02ms         |    0.99 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]              |
|          | 1.95±0.01ms          | 1.99±0.02ms         |    1.02 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]              |
|          | 2.24±0.01ms          | 2.26±0.01ms         |    1.01 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]              |
|          | 2.33±0.01ms          | 2.33±0.01ms         |    1    | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]              |
|          | 2.24±0.02ms          | 2.24±0.02ms         |    1    | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]              |
|          | 3.27±0.05ms          | 3.25±0.03ms         |    0.99 | version.TimeVersionSuite.time_sort_cold [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]               |
|          | 2.08±0.01ms          | 2.07±0.02ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]              |
|          | 1.57±0.02ms          | 1.55±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]              |
|          | 1.81±0.01ms          | 1.82±0.01ms         |    1    | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]              |
|          | 1.90±0.01ms          | 1.92±0.02ms         |    1.01 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]              |
|          | 1.79±0.01ms          | 1.79±0.02ms         |    1    | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]              |
|          | 2.51±0.05ms          | 2.49±0.02ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]               |
|          | 4.56±0.02ms          | 4.57±0.02ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.10-PYTHONHASHSEED0]                    |
|          | 3.12±0.02ms          | 3.13±0.04ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.11-PYTHONHASHSEED0]                    |
|          | 3.70±0.04ms          | 3.67±0.02ms         |    0.99 | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.12-PYTHONHASHSEED0]                    |
|          | 3.19±0.02ms          | 3.18±0.02ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.13-PYTHONHASHSEED0]                    |
|          | 3.17±0.04ms          | 3.17±0.06ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.14-PYTHONHASHSEED0]                    |
|          | 4.77±0.02ms          | 4.75±0.02ms         |    1    | version.TimeVersionSuite.time_str [runnervmeorf1/virtualenv-py3.9-PYTHONHASHSEED0]                     |

SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE INCREASED.

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented May 9, 2026

Why is trim_release now public? It just operates on a tuple, so I don't think it's something we want in our public API.

@notatallshaw
Copy link
Copy Markdown
Member Author

notatallshaw commented May 9, 2026

Why is trim_release now public?

It's not, it's inside the private _ranges module, but this fast path specifiers module needs to access it.

When ranges becomes public I will move trim_release and coerce_version into some shared private utility module.

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/specifier_intervals branch from 6139500 to 72bb0ba Compare May 10, 2026 15:20
@notatallshaw
Copy link
Copy Markdown
Member Author

notatallshaw commented May 10, 2026

Having the clankers do a couple of deep sweeps, they found two edge cases with ===:

  • Regression fixed: === and prerelease=False and a valid PEP 440 pre-release version is rejected on main but was being accepted on this branch
  • Test added for bug on main that was incidentally fixed: === and a non-PEP 440 version and a key function passed in, previously the key was not called and the === acted on the raw value.

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.

3 participants