Skip to content

Commit 9d8e765

Browse files
authored
Consistently use get_requirement to cache Requirement construction (#12663)
1 parent f0bb386 commit 9d8e765

File tree

8 files changed

+34
-20
lines changed

8 files changed

+34
-20
lines changed

news/12663.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve performance when the same requirement string appears many times during resolution, by consistently caching the parsed requirement string.

src/pip/_internal/build_env.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
1313

1414
from pip._vendor.certifi import where
15-
from pip._vendor.packaging.requirements import Requirement
1615
from pip._vendor.packaging.version import Version
1716

1817
from pip import __file__ as pip_location
1918
from pip._internal.cli.spinners import open_spinner
2019
from pip._internal.locations import get_platlib, get_purelib, get_scheme
2120
from pip._internal.metadata import get_default_environment, get_environment
2221
from pip._internal.utils.logging import VERBOSE
22+
from pip._internal.utils.packaging import get_requirement
2323
from pip._internal.utils.subprocess import call_subprocess
2424
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
2525

@@ -184,7 +184,7 @@ def check_requirements(
184184
else get_default_environment()
185185
)
186186
for req_str in reqs:
187-
req = Requirement(req_str)
187+
req = get_requirement(req_str)
188188
# We're explicitly evaluating with an empty extra value, since build
189189
# environments are not provided any mechanism to select specific extras.
190190
if req.marker is not None and not req.marker.evaluate({"extra": ""}):

src/pip/_internal/metadata/importlib/_dists.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
Wheel,
2828
)
2929
from pip._internal.utils.misc import normalize_path
30+
from pip._internal.utils.packaging import get_requirement
3031
from pip._internal.utils.temp_dir import TempDirectory
3132
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
3233

@@ -219,7 +220,7 @@ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requiremen
219220
for req_string in self.metadata.get_all("Requires-Dist", []):
220221
# strip() because email.message.Message.get_all() may return a leading \n
221222
# in case a long header was wrapped.
222-
req = Requirement(req_string.strip())
223+
req = get_requirement(req_string.strip())
223224
if not req.marker:
224225
yield req
225226
elif not extras and req.marker.evaluate({"extra": ""}):

src/pip/_internal/pyproject.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
else:
1010
from pip._vendor import tomli as tomllib
1111

12-
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
12+
from pip._vendor.packaging.requirements import InvalidRequirement
1313

1414
from pip._internal.exceptions import (
1515
InstallationError,
1616
InvalidPyProjectBuildRequires,
1717
MissingPyProjectBuildRequires,
1818
)
19+
from pip._internal.utils.packaging import get_requirement
1920

2021

2122
def _is_list_of_str(obj: Any) -> bool:
@@ -156,7 +157,7 @@ def load_pyproject_toml(
156157
# Each requirement must be valid as per PEP 508
157158
for requirement in requires:
158159
try:
159-
Requirement(requirement)
160+
get_requirement(requirement)
160161
except InvalidRequirement as error:
161162
raise InvalidPyProjectBuildRequires(
162163
package=req_name,

src/pip/_internal/req/constructors.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def _set_requirement_extras(req: Requirement, new_extras: Set[str]) -> Requireme
8181
pre is not None and post is not None
8282
), f"regex group selection for requirement {req} failed, this should never happen"
8383
extras: str = "[%s]" % ",".join(sorted(new_extras)) if new_extras else ""
84-
return Requirement(f"{pre}{extras}{post}")
84+
return get_requirement(f"{pre}{extras}{post}")
8585

8686

8787
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
@@ -163,7 +163,7 @@ def check_first_requirement_in_file(filename: str) -> None:
163163
# If there is a line continuation, drop it, and append the next line.
164164
if line.endswith("\\"):
165165
line = line[:-2].strip() + next(lines, "")
166-
Requirement(line)
166+
get_requirement(line)
167167
return
168168

169169

@@ -205,7 +205,7 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
205205

206206
if name is not None:
207207
try:
208-
req: Optional[Requirement] = Requirement(name)
208+
req: Optional[Requirement] = get_requirement(name)
209209
except InvalidRequirement as exc:
210210
raise InstallationError(f"Invalid requirement: {name!r}: {exc}")
211211
else:

src/pip/_internal/req/req_install.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
redact_auth_from_requirement,
5353
redact_auth_from_url,
5454
)
55+
from pip._internal.utils.packaging import get_requirement
5556
from pip._internal.utils.subprocess import runner_with_spinner_message
5657
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
5758
from pip._internal.utils.unpacking import unpack_file
@@ -395,7 +396,7 @@ def _set_requirement(self) -> None:
395396
else:
396397
op = "==="
397398

398-
self.req = Requirement(
399+
self.req = get_requirement(
399400
"".join(
400401
[
401402
self.metadata["Name"],
@@ -421,7 +422,7 @@ def warn_on_mismatching_name(self) -> None:
421422
metadata_name,
422423
self.name,
423424
)
424-
self.req = Requirement(metadata_name)
425+
self.req = get_requirement(metadata_name)
425426

426427
def check_if_exists(self, use_user_site: bool) -> None:
427428
"""Find an installed distribution that satisfies or conflicts

src/pip/_internal/utils/packaging.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def check_requires_python(
3434
return python_version in requires_python_specifier
3535

3636

37-
@functools.lru_cache(maxsize=512)
37+
@functools.lru_cache(maxsize=2048)
3838
def get_requirement(req_string: str) -> Requirement:
3939
"""Construct a packaging.Requirement object with caching"""
4040
# Parsing requirement strings is expensive, and is also expected to happen

tests/unit/test_req.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -770,11 +770,16 @@ def test_install_req_drop_extras(self, inp: str, out: str) -> None:
770770
without_extras = install_req_drop_extras(req)
771771
assert not without_extras.extras
772772
assert str(without_extras.req) == out
773-
# should always be a copy
774-
assert req is not without_extras
775-
assert req.req is not without_extras.req
773+
774+
# if there are no extras they should be the same object,
775+
# otherwise they may be a copy due to cache
776+
if req.extras:
777+
assert req is not without_extras
778+
assert req.req is not without_extras.req
779+
776780
# comes_from should point to original
777781
assert without_extras.comes_from is req
782+
778783
# all else should be the same
779784
assert without_extras.link == req.link
780785
assert without_extras.markers == req.markers
@@ -790,9 +795,9 @@ def test_install_req_drop_extras(self, inp: str, out: str) -> None:
790795
@pytest.mark.parametrize(
791796
"inp, extras, out",
792797
[
793-
("pkg", {}, "pkg"),
794-
("pkg==1.0", {}, "pkg==1.0"),
795-
("pkg[ext]", {}, "pkg[ext]"),
798+
("pkg", set(), "pkg"),
799+
("pkg==1.0", set(), "pkg==1.0"),
800+
("pkg[ext]", set(), "pkg[ext]"),
796801
("pkg", {"ext"}, "pkg[ext]"),
797802
("pkg==1.0", {"ext"}, "pkg[ext]==1.0"),
798803
("pkg==1.0", {"ext1", "ext2"}, "pkg[ext1,ext2]==1.0"),
@@ -816,9 +821,14 @@ def test_install_req_extend_extras(
816821
assert str(extended.req) == out
817822
assert extended.req is not None
818823
assert set(extended.extras) == set(extended.req.extras)
819-
# should always be a copy
820-
assert req is not extended
821-
assert req.req is not extended.req
824+
825+
# if extras is not a subset of req.extras then the extended
826+
# requirement object should not be the same, otherwise they
827+
# might be a copy due to cache
828+
if not extras.issubset(req.extras):
829+
assert req is not extended
830+
assert req.req is not extended.req
831+
822832
# all else should be the same
823833
assert extended.link == req.link
824834
assert extended.markers == req.markers

0 commit comments

Comments
 (0)