From 12632485a3172de822e2b0f62eb2e35734377345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:01:10 +0200 Subject: [PATCH] perf: `Version.__eq__` --- .../core/constraints/version/version.py | 7 ++ src/poetry/core/version/pep440/version.py | 78 ++++++------------- 2 files changed, 30 insertions(+), 55 deletions(-) diff --git a/src/poetry/core/constraints/version/version.py b/src/poetry/core/constraints/version/version.py index 6bcd13205..f6ea5a6ec 100644 --- a/src/poetry/core/constraints/version/version.py +++ b/src/poetry/core/constraints/version/version.py @@ -134,6 +134,13 @@ def __str__(self) -> str: return self.text def __eq__(self, other: object) -> bool: + # Common case: comparing against another Version. Handle it first + # (inlining the semantics of the dataclass-generated __eq__) to avoid + # extra call layers and the (otherwise per-call) import of VersionRange + # below. + if self.__class__ is other.__class__: + return self._compare_key == other._compare_key + from poetry.core.constraints.version.version_range import VersionRange if isinstance(other, VersionRange): diff --git a/src/poetry/core/version/pep440/version.py b/src/poetry/core/version/pep440/version.py index ad49d605e..5bf778601 100644 --- a/src/poetry/core/version/pep440/version.py +++ b/src/poetry/core/version/pep440/version.py @@ -1,7 +1,6 @@ from __future__ import annotations import dataclasses -import functools import warnings from typing import TYPE_CHECKING @@ -21,44 +20,22 @@ from poetry.core.version.pep440.segments import LocalSegmentType -@functools.total_ordering -class AlwaysSmaller: - def __lt__(self, other: object) -> bool: - return True - - def __eq__(self, other: object) -> bool: - return isinstance(other, AlwaysSmaller) - - def __hash__(self) -> int: - return id(AlwaysSmaller) - - -@functools.total_ordering -class AlwaysGreater: - def __gt__(self, other: object) -> bool: - return True - - def __eq__(self, other: object) -> bool: - return isinstance(other, AlwaysGreater) - - def __hash__(self) -> int: - return id(AlwaysGreater) - - -class Infinity(AlwaysGreater, int): - pass - - -class NegativeInfinity(AlwaysSmaller, int): - pass - - T = TypeVar("T", bound="PEP440Version") +# The compare key only contains built-in types (ints, floats, strings and +# tuples thereof), so that comparing and hashing versions stays entirely in +# C code. Infinity sentinels are expressed as floats; phases as strings that +# sort correctly against the real phase identifiers: # we use the phase "z" to ensure we always sort this after other phases -_INF_TAG = ReleaseTag("z", Infinity()) +_INF_TAG = ("z", float("inf")) # we use the phase "" to ensure we always sort this before other phases -_NEG_INF_TAG = ReleaseTag("", NegativeInfinity()) +_NEG_INF_TAG = ("", float("-inf")) + +_CompareTag = tuple[str, int | float] +_CompareLocal = tuple[tuple[int | float, str], ...] +_CompareKey = tuple[ + int, tuple[int, ...], _CompareTag, _CompareTag, _CompareTag, _CompareLocal +] @dataclasses.dataclass(frozen=True, eq=True, order=True) @@ -70,9 +47,7 @@ class PEP440Version: dev: ReleaseTag | None = dataclasses.field(default=None, compare=False) local: LocalSegmentType = dataclasses.field(default=None, compare=False) text: str = dataclasses.field(init=False, compare=False) - _compare_key: tuple[ - int, Release, ReleaseTag, ReleaseTag, ReleaseTag, tuple[int | str, ...] - ] = dataclasses.field(init=False, compare=True) + _compare_key: _CompareKey = dataclasses.field(init=False, compare=True) def __post_init__(self) -> None: if self.local is not None and not isinstance(self.local, tuple): @@ -85,16 +60,7 @@ def __post_init__(self) -> None: object.__setattr__(self, "_compare_key", self._make_compare_key()) - def _make_compare_key( - self, - ) -> tuple[ - int, - Release, - ReleaseTag, - ReleaseTag, - ReleaseTag, - tuple[tuple[int, int | str], ...], - ]: + def _make_compare_key(self) -> _CompareKey: """ This code is based on the implementation of packaging.version._cmpkey(..) """ @@ -109,18 +75,20 @@ def _make_compare_key( elif self.pre is None: _pre = _INF_TAG else: - _pre = self.pre + _pre = (self.pre.phase, self.pre.number) # Versions without a post segment should sort before those with one. - _post = _NEG_INF_TAG if self.post is None else self.post + _post = ( + _NEG_INF_TAG if self.post is None else (self.post.phase, self.post.number) + ) # Versions without a development segment should sort after those with one. - _dev = _INF_TAG if self.dev is None else self.dev + _dev = _INF_TAG if self.dev is None else (self.dev.phase, self.dev.number) - _local: tuple[tuple[int, int | str], ...] + _local: _CompareLocal if self.local is None: # Versions without a local segment should sort before those with one. - _local = ((NegativeInfinity(), ""),) + _local = ((float("-inf"), ""),) else: # Versions with a local segment need that segment parsed to implement # the sorting rules in PEP440. @@ -132,10 +100,10 @@ def _make_compare_key( assert isinstance(self.local, tuple) # We convert strings that are integers so that they can be compared _local = tuple( - (int(i), "") if str(i).isnumeric() else (NegativeInfinity(), i) + (int(i), "") if (s_i := str(i)).isnumeric() else (float("-inf"), s_i) for i in self.local ) - return self.epoch, self.release, _pre, _post, _dev, _local + return self.epoch, self.release._compare_key, _pre, _post, _dev, _local @property def major(self) -> int: