Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache construction of name and version strings #12316

Closed
wants to merge 9 commits into from
Closed
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
1 change: 1 addition & 0 deletions news/12316.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Significantly improve performance when installing hundreds of packages
7 changes: 5 additions & 2 deletions src/pip/_internal/metadata/importlib/_dists.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def __init__(
self._dist = dist
self._info_location = info_location
self._installed_location = installed_location
self._cached_canonical_name: Optional[NormalizedName] = None

@classmethod
def from_directory(cls, directory: str) -> BaseDistribution:
Expand Down Expand Up @@ -169,8 +170,10 @@ def _get_dist_name_from_location(self) -> Optional[str]:

@property
bdraco marked this conversation as resolved.
Show resolved Hide resolved
def canonical_name(self) -> NormalizedName:
name = self._get_dist_name_from_location() or get_dist_name(self._dist)
return canonicalize_name(name)
if self._cached_canonical_name is None:
name = self._get_dist_name_from_location() or get_dist_name(self._dist)
self._cached_canonical_name = canonicalize_name(name)
return self._cached_canonical_name

@property
def version(self) -> DistributionVersion:
Expand Down
11 changes: 7 additions & 4 deletions src/pip/_internal/resolution/resolvelib/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast

from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import parse_version

from pip._internal.exceptions import (
HashError,
Expand Down Expand Up @@ -271,7 +271,7 @@ def __init__(
assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel"
# Version may not be present for PEP 508 direct URLs
if version is not None:
wheel_version = Version(wheel.version)
wheel_version = parse_version(wheel.version)
assert version == wheel_version, "{!r} != {!r} for wheel {}".format(
version, wheel_version, name
)
Expand Down Expand Up @@ -349,6 +349,7 @@ def __init__(
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
skip_reason = "already satisfied"
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
self._name: Optional[NormalizedName] = None

def __str__(self) -> str:
return str(self.dist)
Expand All @@ -369,7 +370,9 @@ def __eq__(self, other: Any) -> bool:

@property
def project_name(self) -> NormalizedName:
return self.dist.canonical_name
if self._name is None:
self._name = self.dist.canonical_name
return self._name

@property
def name(self) -> str:
Expand Down Expand Up @@ -565,7 +568,7 @@ def __init__(self, py_version_info: Optional[Tuple[int, ...]]) -> None:
version_info = normalize_version_info(py_version_info)
else:
version_info = sys.version_info[:3]
self._version = Version(".".join(str(c) for c in version_info))
self._version = parse_version(".".join(str(c) for c in version_info))

# We don't need to implement __eq__() and __ne__() since there is always
# only one RequiresPythonCandidate in a resolution, i.e. the host Python.
Expand Down
28 changes: 14 additions & 14 deletions src/pip/_vendor/packaging/specifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)

from .utils import canonicalize_version
from .version import LegacyVersion, Version, parse
from .version import LegacyVersion, Version, parse, parse_legacy_version, parse_version

ParsedVersion = Union[Version, LegacyVersion]
UnparsedVersion = Union[Version, LegacyVersion, str]
Expand Down Expand Up @@ -143,7 +143,7 @@ def _get_operator(self, op: str) -> CallableOperator:

def _coerce_version(self, version: UnparsedVersion) -> ParsedVersion:
if not isinstance(version, (LegacyVersion, Version)):
version = parse(version)
return parse(version)
return version

@property
Expand Down Expand Up @@ -180,7 +180,7 @@ def contains(
# Determine if we should be supporting prereleases in this specifier
# or not, if we do not support prereleases than we can short circuit
# logic if this version is a prereleases.
if normalized_item.is_prerelease and not prereleases:
if not prereleases and normalized_item.is_prerelease:
return False

# Actually do the comparison to determine if this item is contained
Expand Down Expand Up @@ -260,7 +260,7 @@ def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:

def _coerce_version(self, version: UnparsedVersion) -> LegacyVersion:
if not isinstance(version, LegacyVersion):
version = LegacyVersion(str(version))
version = parse_legacy_version(str(version))
return version

def _compare_equal(self, prospective: LegacyVersion, spec: str) -> bool:
Expand Down Expand Up @@ -432,7 +432,7 @@ def _compare_equal(self, prospective: ParsedVersion, spec: str) -> bool:
# We need special logic to handle prefix matching
if spec.endswith(".*"):
# In the case of prefix matching we want to ignore local segment.
prospective = Version(prospective.public)
prospective = parse_version(prospective.public)
# Split the spec out by dots, and pretend that there is an implicit
# dot in between a release segment and a pre-release segment.
split_spec = _version_split(spec[:-2]) # Remove the trailing .*
Expand All @@ -456,13 +456,13 @@ def _compare_equal(self, prospective: ParsedVersion, spec: str) -> bool:
return padded_prospective == padded_spec
else:
# Convert our spec string into a Version
spec_version = Version(spec)
spec_version = parse_version(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_version(prospective.public)

return prospective == spec_version

Expand All @@ -476,7 +476,7 @@ def _compare_less_than_equal(self, prospective: ParsedVersion, spec: str) -> boo
# 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_version(prospective.public) <= parse_version(spec)

@_require_version_compare
def _compare_greater_than_equal(
Expand All @@ -486,14 +486,14 @@ def _compare_greater_than_equal(
# 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_version(prospective.public) >= parse_version(spec)

@_require_version_compare
def _compare_less_than(self, prospective: ParsedVersion, 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_version(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
Expand All @@ -506,7 +506,7 @@ def _compare_less_than(self, prospective: ParsedVersion, 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_version(prospective.base_version) == parse_version(spec.base_version):
return False

# If we've gotten to here, it means that prospective version is both
Expand All @@ -519,7 +519,7 @@ def _compare_greater_than(self, prospective: ParsedVersion, spec_str: str) -> bo

# Convert our spec to a Version instance, since we'll want to work with
# it as a version.
spec = Version(spec_str)
spec = parse_version(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
Expand All @@ -532,13 +532,13 @@ def _compare_greater_than(self, prospective: ParsedVersion, spec_str: str) -> bo
# 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_version(prospective.base_version) == parse_version(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_version(prospective.base_version) == parse_version(spec.base_version):
return False

# If we've gotten to here, it means that prospective version is both
Expand Down
7 changes: 5 additions & 2 deletions src/pip/_vendor/packaging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 parse_version, InvalidVersion, Version

BuildTag = Union[Tuple[()], Tuple[int, str]]
NormalizedName = NewType("NormalizedName", str)
Expand All @@ -29,20 +30,22 @@ class InvalidSdistFilename(ValueError):
_build_tag_regex = re.compile(r"(\d+)(.*)")


@functools.lru_cache(maxsize=4096)
def canonicalize_name(name: str) -> NormalizedName:
# This is taken from PEP 503.
value = _canonicalize_regex.sub("-", name).lower()
return cast(NormalizedName, value)


@functools.lru_cache(maxsize=4096)
def canonicalize_version(version: Union[Version, str]) -> str:
"""
This is very similar to Version.__str__, but has one subtle difference
with the way it handles the release segment.
"""
if isinstance(version, str):
try:
parsed = Version(version)
parsed = parse_version(version)
except InvalidVersion:
# Legacy versions cannot be normalized
return version
Expand Down
38 changes: 31 additions & 7 deletions src/pip/_vendor/packaging/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
# for complete details.

import collections
import functools
import itertools
import re
import warnings
from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union

from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType

__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
__all__ = ["parse", "parse_legacy_version", "parse_version", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]

InfiniteTypes = Union[InfinityType, NegativeInfinityType]
PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
Expand Down Expand Up @@ -38,19 +39,33 @@
"_Version", ["epoch", "release", "dev", "pre", "post", "local"]
)


def parse(version: str) -> Union["LegacyVersion", "Version"]:
"""
Parse the given version string and return either a :class:`Version` object
or a :class:`LegacyVersion` object depending on if the given version is
a valid PEP 440 version or a legacy version.
"""
try:
return Version(version)
return parse_version(version)
except InvalidVersion:
return LegacyVersion(version)
return parse_legacy_version(version)


@functools.lru_cache(maxsize=4096)
def parse_legacy_version(version: str) -> "LegacyVersion":
"""
Parse the given version string and return a :class:`LegacyVersion` object.
"""
return LegacyVersion(version)


@functools.lru_cache(maxsize=4096)
def parse_version(version: str) -> "Version":
"""
Parse the given version string and return a :class:`Version` object.
"""
return Version(version)

class InvalidVersion(ValueError):
"""
An invalid version was found, users should refer to PEP 440.
Expand Down Expand Up @@ -287,10 +302,19 @@ def __init__(self, version: str) -> None:
self._version.local,
)

# Pre-compute the version string since
# its almost always called anyways and
# we want to cache it to avoid the cost
# of re-computing it.
self._str = self._version_string()

def __repr__(self) -> str:
return f"<Version('{self}')>"

def __str__(self) -> str:
return self._str

def _version_string(self) -> str:
parts = []

# Epoch
Expand Down Expand Up @@ -367,15 +391,15 @@ def base_version(self) -> str:

@property
def is_prerelease(self) -> bool:
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:
return self.post is not None
return self._version.post is not None

@property
def is_devrelease(self) -> bool:
return self.dev is not None
return self._version.dev is not None

@property
def major(self) -> int:
Expand Down