Skip to content

Commit e46888b

Browse files
authored
Merge pull request #10615 from uranusjr/new-resolver-respect-extra-in-user-requested
2 parents b160e83 + 26eaddc commit e46888b

File tree

3 files changed

+88
-19
lines changed

3 files changed

+88
-19
lines changed

news/10613.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When a package is requested by the user for upgrade, correctly identify that
2+
the extra-ed variant of that same package depended by another user-requested
3+
package is requesting the same package, and upgrade it accordingly.

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

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import collections
22
import math
3-
from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Mapping, Sequence, Union
3+
from typing import (
4+
TYPE_CHECKING,
5+
Dict,
6+
Iterable,
7+
Iterator,
8+
Mapping,
9+
Sequence,
10+
TypeVar,
11+
Union,
12+
)
413

514
from pip._vendor.resolvelib.providers import AbstractProvider
615

@@ -37,6 +46,35 @@
3746
# services to those objects (access to pip's finder and preparer).
3847

3948

49+
D = TypeVar("D")
50+
V = TypeVar("V")
51+
52+
53+
def _get_with_identifier(
54+
mapping: Mapping[str, V],
55+
identifier: str,
56+
default: D,
57+
) -> Union[D, V]:
58+
"""Get item from a package name lookup mapping with a resolver identifier.
59+
60+
This extra logic is needed when the target mapping is keyed by package
61+
name, which cannot be directly looked up with an identifier (which may
62+
contain requested extras). Additional logic is added to also look up a value
63+
by "cleaning up" the extras from the identifier.
64+
"""
65+
if identifier in mapping:
66+
return mapping[identifier]
67+
# HACK: Theoretically we should check whether this identifier is a valid
68+
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
69+
# some regular expression. But since pip's resolver only spits out three
70+
# kinds of identifiers: normalized PEP 503 names, normalized names plus
71+
# extras, and Requires-Python, we can cheat a bit here.
72+
name, open_bracket, _ = identifier.partition("[")
73+
if open_bracket and name in mapping:
74+
return mapping[name]
75+
return default
76+
77+
4078
class PipProvider(_ProviderBase):
4179
"""Pip's provider implementation for resolvelib.
4280
@@ -150,28 +188,13 @@ def get_preference( # type: ignore
150188
identifier,
151189
)
152190

153-
def _get_constraint(self, identifier: str) -> Constraint:
154-
if identifier in self._constraints:
155-
return self._constraints[identifier]
156-
157-
# HACK: Theoretically we should check whether this identifier is a valid
158-
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
159-
# some regular expression. But since pip's resolver only spits out
160-
# three kinds of identifiers: normalized PEP 503 names, normalized names
161-
# plus extras, and Requires-Python, we can cheat a bit here.
162-
name, open_bracket, _ = identifier.partition("[")
163-
if open_bracket and name in self._constraints:
164-
return self._constraints[name]
165-
166-
return Constraint.empty()
167-
168191
def find_matches(
169192
self,
170193
identifier: str,
171194
requirements: Mapping[str, Iterator[Requirement]],
172195
incompatibilities: Mapping[str, Iterator[Candidate]],
173196
) -> Iterable[Candidate]:
174-
def _eligible_for_upgrade(name: str) -> bool:
197+
def _eligible_for_upgrade(identifier: str) -> bool:
175198
"""Are upgrades allowed for this project?
176199
177200
This checks the upgrade strategy, and whether the project was one
@@ -185,13 +208,23 @@ def _eligible_for_upgrade(name: str) -> bool:
185208
if self._upgrade_strategy == "eager":
186209
return True
187210
elif self._upgrade_strategy == "only-if-needed":
188-
return name in self._user_requested
211+
user_order = _get_with_identifier(
212+
self._user_requested,
213+
identifier,
214+
default=None,
215+
)
216+
return user_order is not None
189217
return False
190218

219+
constraint = _get_with_identifier(
220+
self._constraints,
221+
identifier,
222+
default=Constraint.empty(),
223+
)
191224
return self._factory.find_candidates(
192225
identifier=identifier,
193226
requirements=requirements,
194-
constraint=self._get_constraint(identifier),
227+
constraint=constraint,
195228
prefers_installed=(not _eligible_for_upgrade(identifier)),
196229
incompatibilities=incompatibilities,
197230
)

tests/functional/test_new_resolver.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,3 +2180,36 @@ def test_new_resolver_dont_backtrack_on_extra_if_base_constrained(script):
21802180
)
21812181
assert "pkg-2.0" not in result.stdout, "Should not try 2.0 due to constraint"
21822182
script.assert_installed(pkg="1.0", dep="1.0")
2183+
2184+
2185+
def test_new_resolver_respect_user_requested_if_extra_is_installed(script):
2186+
create_basic_wheel_for_package(script, "pkg1", "1.0")
2187+
create_basic_wheel_for_package(script, "pkg2", "1.0", extras={"ext": ["pkg1"]})
2188+
create_basic_wheel_for_package(script, "pkg2", "2.0", extras={"ext": ["pkg1"]})
2189+
create_basic_wheel_for_package(script, "pkg3", "1.0", depends=["pkg2[ext]"])
2190+
2191+
# Install pkg3 with an older pkg2.
2192+
script.pip(
2193+
"install",
2194+
"--no-cache-dir",
2195+
"--no-index",
2196+
"--find-links",
2197+
script.scratch_path,
2198+
"pkg3",
2199+
"pkg2==1.0",
2200+
)
2201+
script.assert_installed(pkg3="1.0", pkg2="1.0", pkg1="1.0")
2202+
2203+
# Now upgrade both pkg3 and pkg2. pkg2 should be upgraded although pkg2[ext]
2204+
# is not requested by the user.
2205+
script.pip(
2206+
"install",
2207+
"--no-cache-dir",
2208+
"--no-index",
2209+
"--find-links",
2210+
script.scratch_path,
2211+
"--upgrade",
2212+
"pkg3",
2213+
"pkg2",
2214+
)
2215+
script.assert_installed(pkg3="1.0", pkg2="2.0", pkg1="1.0")

0 commit comments

Comments
 (0)