Skip to content

Commit bb88244

Browse files
committed
(joe) requirement creation is very expensive, and at least in my test case there were only ~200 unique requirement objects created in ~5-10 minutes of resolution time
1 parent 0981e07 commit bb88244

File tree

4 files changed

+24
-8
lines changed

4 files changed

+24
-8
lines changed

news/10550.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve performance of dependency resolution.

src/pip/_internal/req/constructors.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
InstallRequirement.
99
"""
1010

11+
import functools
1112
import logging
1213
import os
1314
import re
@@ -39,6 +40,11 @@
3940
operators = Specifier._operators.keys()
4041

4142

43+
@functools.lru_cache(maxsize=None)
44+
def get_or_create_requirement(req_string: str) -> Requirement:
45+
return Requirement(req_string)
46+
47+
4248
def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
4349
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
4450
extras = None
@@ -54,7 +60,7 @@ def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
5460
def convert_extras(extras: Optional[str]) -> Set[str]:
5561
if not extras:
5662
return set()
57-
return Requirement("placeholder" + extras.lower()).extras
63+
return get_or_create_requirement("placeholder" + extras.lower()).extras
5864

5965

6066
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
@@ -83,7 +89,7 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
8389
return (
8490
package_name,
8591
url_no_extras,
86-
Requirement("placeholder" + extras.lower()).extras,
92+
get_or_create_requirement("placeholder" + extras.lower()).extras,
8793
)
8894
else:
8995
return package_name, url_no_extras, set()
@@ -309,7 +315,7 @@ def with_source(text: str) -> str:
309315

310316
def _parse_req_string(req_as_string: str) -> Requirement:
311317
try:
312-
req = Requirement(req_as_string)
318+
req = get_or_create_requirement(req_as_string)
313319
except InvalidRequirement:
314320
if os.path.sep in req_as_string:
315321
add_msg = "It looks like a path."
@@ -386,7 +392,7 @@ def install_req_from_req_string(
386392
user_supplied: bool = False,
387393
) -> InstallRequirement:
388394
try:
389-
req = Requirement(req_string)
395+
req = get_or_create_requirement(req_string)
390396
except InvalidRequirement:
391397
raise InstallationError(f"Invalid requirement: '{req_string}'")
392398

src/pip/_internal/resolution/resolvelib/factory.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
cast,
1919
)
2020

21-
from pip._vendor.packaging.requirements import InvalidRequirement
22-
from pip._vendor.packaging.requirements import Requirement as PackagingRequirement
21+
from pip._vendor.packaging.requirements import (
22+
InvalidRequirement,
23+
)
2324
from pip._vendor.packaging.specifiers import SpecifierSet
2425
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
2526
from pip._vendor.resolvelib import ResolutionImpossible
@@ -38,7 +39,7 @@
3839
from pip._internal.models.link import Link
3940
from pip._internal.models.wheel import Wheel
4041
from pip._internal.operations.prepare import RequirementPreparer
41-
from pip._internal.req.constructors import install_req_from_link_and_ireq
42+
from pip._internal.req.constructors import get_or_create_requirement, install_req_from_link_and_ireq
4243
from pip._internal.req.req_install import (
4344
InstallRequirement,
4445
check_invalid_constraint_type,
@@ -365,7 +366,7 @@ def find_candidates(
365366
# If the current identifier contains extras, add explicit candidates
366367
# from entries from extra-less identifier.
367368
with contextlib.suppress(InvalidRequirement):
368-
parsed_requirement = PackagingRequirement(identifier)
369+
parsed_requirement = get_or_create_requirement(identifier)
369370
explicit_candidates.update(
370371
self._iter_explicit_candidates_from_base(
371372
requirements.get(parsed_requirement.name, ()),

tests/unit/test_req.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,14 @@ def test_parse_editable_local_extras(
658658
)
659659

660660

661+
def test_get_or_create_caching() -> None:
662+
"""test caching of get_or_create requirement"""
663+
teststr = "affinegap==1.10"
664+
assert get_or_create_requirement(teststr) == Requirement(teststr)
665+
assert not (get_or_create_requirement(teststr) is Requirement(teststr))
666+
assert get_or_create_requirement(teststr) is get_or_create_requirement(teststr)
667+
668+
661669
def test_exclusive_environment_markers() -> None:
662670
"""Make sure RequirementSet accepts several excluding env markers"""
663671
eq36 = install_req_from_line("Django>=1.6.10,<1.7 ; python_version == '3.6'")

0 commit comments

Comments
 (0)