From 4420611db99cb5cbab52a04501cefa8cfdc6ffaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Oct 2023 17:10:05 -0500 Subject: [PATCH] Cache parsing of version and names extracted from https://github.com/pypa/pip/pull/12316 --- src/packaging/specifiers.py | 28 ++++++++++++++-------------- src/packaging/utils.py | 7 +++++-- src/packaging/version.py | 20 ++++++++++++++++---- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/packaging/specifiers.py b/src/packaging/specifiers.py index 94448327..68907737 100644 --- a/src/packaging/specifiers.py +++ b/src/packaging/specifiers.py @@ -24,7 +24,7 @@ ) from .utils import canonicalize_version -from .version import Version +from .version import Version, parse UnparsedVersion = Union[Version, str] UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) @@ -33,7 +33,7 @@ def _coerce_version(version: UnparsedVersion) -> Version: if not isinstance(version, Version): - version = Version(version) + return parse(version) return version @@ -272,7 +272,7 @@ def prereleases(self) -> bool: # Parse the version, and if it is a pre-release than this # specifier allows pre-releases. - if Version(version).is_prerelease: + if parse(version).is_prerelease: return True return False @@ -425,13 +425,13 @@ def _compare_equal(self, prospective: Version, spec: str) -> bool: return shortened_prospective == split_spec else: # Convert our spec string into a Version - spec_version = Version(spec) + spec_version = parse(spec) # If the specifier does not have a local segment, then we want to # act as if the prospective version also does not have a local # segment. if not spec_version.local: - prospective = Version(prospective.public) + prospective = parse(prospective.public) return prospective == spec_version @@ -443,20 +443,20 @@ 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 parse(prospective.public) <= parse(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 parse(prospective.public) >= parse(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 # it as a version. - spec = Version(spec_str) + spec = parse(spec_str) # Check to see if the prospective version is less than the spec # version. If it's not we can short circuit and just return False now @@ -469,7 +469,7 @@ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: # versions for the version mentioned in the specifier (e.g. <3.1 should # not match 3.1.dev0, but should match 3.0.dev0). if not spec.is_prerelease and prospective.is_prerelease: - if Version(prospective.base_version) == Version(spec.base_version): + if parse(prospective.base_version) == parse(spec.base_version): return False # If we've gotten to here, it means that prospective version is both @@ -481,7 +481,7 @@ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec_str) + spec = parse(spec_str) # Check to see if the prospective version is greater than the spec # version. If it's not we can short circuit and just return False now @@ -494,13 +494,13 @@ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: # post-release versions for the version mentioned in the specifier # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). if not spec.is_postrelease and prospective.is_postrelease: - if Version(prospective.base_version) == Version(spec.base_version): + if parse(prospective.base_version) == parse(spec.base_version): 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: - if Version(prospective.base_version) == Version(spec.base_version): + if parse(prospective.base_version) == parse(spec.base_version): return False # If we've gotten to here, it means that prospective version is both @@ -924,7 +924,7 @@ def contains( """ # Ensure that our item is a Version instance. if not isinstance(item, Version): - item = Version(item) + item = parse(item) # 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 @@ -942,7 +942,7 @@ def contains( return False if installed and item.is_prerelease: - item = Version(item.base_version) + item = parse(item.base_version) # We simply dispatch to the underlying specs here to make sure that the # given version is contained within all of them. diff --git a/src/packaging/utils.py b/src/packaging/utils.py index c2c2f75a..27741abe 100644 --- a/src/packaging/utils.py +++ b/src/packaging/utils.py @@ -2,11 +2,12 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. +import functools import re from typing import FrozenSet, NewType, Tuple, Union, cast from .tags import Tag, parse_tag -from .version import InvalidVersion, Version +from .version import InvalidVersion, Version, parse BuildTag = Union[Tuple[()], Tuple[int, str]] NormalizedName = NewType("NormalizedName", str) @@ -40,6 +41,7 @@ class InvalidSdistFilename(ValueError): _build_tag_regex = re.compile(r"(\d+)(.*)") +@functools.lru_cache(maxsize=4096) def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: if validate and not _validate_regex.match(name): raise InvalidName(f"name is invalid: {name!r}") @@ -52,6 +54,7 @@ def is_normalized_name(name: str) -> bool: return _normalized_regex.match(name) is not None +@functools.lru_cache(maxsize=4096) def canonicalize_version( version: Union[Version, str], *, strip_trailing_zero: bool = True ) -> str: @@ -61,7 +64,7 @@ def canonicalize_version( """ if isinstance(version, str): try: - parsed = Version(version) + parsed = parse(version) except InvalidVersion: # Legacy versions cannot be normalized return version diff --git a/src/packaging/version.py b/src/packaging/version.py index 5faab9bd..9a9f586f 100644 --- a/src/packaging/version.py +++ b/src/packaging/version.py @@ -7,6 +7,7 @@ from packaging.version import parse, Version """ +import functools import itertools import re from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union @@ -42,6 +43,7 @@ class _Version(NamedTuple): local: Optional[LocalType] +@functools.lru_cache(maxsize=4096) def parse(version: str) -> "Version": """Parse the given version string. @@ -220,6 +222,12 @@ def __init__(self, version: str) -> None: self._version.dev, self._version.local, ) + # Pre-compute the version string since + # its almost always called anyways because + # public will stringify the object and + # we want to cache it to avoid the cost + # of re-computing it. + self._str = self._version_string() def __repr__(self) -> str: """A representation of the Version that shows all internal state. @@ -234,7 +242,11 @@ def __str__(self) -> str: >>> str(Version("1.0a5")) '1.0a5' - """ + """ + return self._str + + def _version_string(self) -> str: + """Build the version string to be used for __str__.""" parts = [] # Epoch @@ -393,7 +405,7 @@ def is_prerelease(self) -> bool: >>> Version("1.2.3dev1").is_prerelease True """ - return self.dev is not None or self.pre is not None + return self._version.dev is not None or self._version.pre is not None @property def is_postrelease(self) -> bool: @@ -404,7 +416,7 @@ def is_postrelease(self) -> bool: >>> Version("1.2.3.post1").is_postrelease True """ - return self.post is not None + return self._version.post is not None @property def is_devrelease(self) -> bool: @@ -415,7 +427,7 @@ def is_devrelease(self) -> bool: >>> Version("1.2.3.dev1").is_devrelease True """ - return self.dev is not None + return self._version.dev is not None @property def major(self) -> int: