Skip to content

Commit 0f1f5fe

Browse files
committed
Move backtrack cause preference to narrow_requirement_selection
1 parent a2c40d4 commit 0f1f5fe

File tree

2 files changed

+57
-40
lines changed

2 files changed

+57
-40
lines changed

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

+22-18
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,36 @@ def narrow_requirement_selection(
114114
"""Produce a subset of identifiers that should be considered before others.
115115
116116
Currently pip narrows the following selection:
117-
* Requires-Python is always considered first.
117+
* Requires-Python, if present is always returned by itself
118+
* Backtrack causes are considered next because they can be identified
119+
in linear time here, whereas because get_preference() is called
120+
for each identifier, it would be quadratic to check for them there.
121+
Further, the current backtrack causes likely need to be resolved
122+
before other requirements as a resolution can't be found while
123+
there is a conflict.
118124
"""
125+
backtrack_idenifiers = set()
126+
for info in backtrack_causes:
127+
backtrack_idenifiers.add(info.requirement.name)
128+
if info.parent is not None:
129+
backtrack_idenifiers.add(info.parent.name)
130+
131+
current_backtrack_causes = []
119132
for identifier in identifiers:
120133
# Requires-Python has only one candidate and the check is basically
121134
# free, so we always do it first to avoid needless work if it fails.
122135
# This skips calling get_preference() for all other identifiers.
123136
if identifier == REQUIRES_PYTHON_IDENTIFIER:
124137
return [identifier]
125138

139+
# Check if this identifier is a backtrack cause
140+
if identifier in backtrack_idenifiers:
141+
current_backtrack_causes.append(identifier)
142+
continue
143+
144+
if current_backtrack_causes:
145+
return current_backtrack_causes
146+
126147
return identifiers
127148

128149
def get_preference(
@@ -175,15 +196,9 @@ def get_preference(
175196
unfree = bool(operators)
176197
requested_order = self._user_requested.get(identifier, math.inf)
177198

178-
# Prefer the causes of backtracking on the assumption that the problem
179-
# resolving the dependency tree is related to the failures that caused
180-
# the backtracking
181-
backtrack_cause = self.is_backtrack_cause(identifier, backtrack_causes)
182-
183199
return (
184200
not direct,
185201
not pinned,
186-
not backtrack_cause,
187202
requested_order,
188203
not unfree,
189204
identifier,
@@ -238,14 +253,3 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo
238253
def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
239254
with_requires = not self._ignore_dependencies
240255
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]
241-
242-
@staticmethod
243-
def is_backtrack_cause(
244-
identifier: str, backtrack_causes: Sequence["PreferenceInformation"]
245-
) -> bool:
246-
for backtrack_cause in backtrack_causes:
247-
if identifier == backtrack_cause.requirement.name:
248-
return True
249-
if backtrack_cause.parent and identifier == backtrack_cause.parent.name:
250-
return True
251-
return False

tests/unit/resolution_resolvelib/test_provider.py

+35-22
Original file line numberDiff line numberDiff line change
@@ -42,39 +42,31 @@ def build_req_info(
4242
{"pinned-package": [build_req_info("pinned-package==1.0")]},
4343
[],
4444
{},
45-
(False, False, True, math.inf, False, "pinned-package"),
46-
),
47-
# Package that caused backtracking
48-
(
49-
"backtrack-package",
50-
{"backtrack-package": [build_req_info("backtrack-package")]},
51-
[build_req_info("backtrack-package")],
52-
{},
53-
(False, True, False, math.inf, True, "backtrack-package"),
45+
(False, False, math.inf, False, "pinned-package"),
5446
),
5547
# Root package requested by user
5648
(
5749
"root-package",
5850
{"root-package": [build_req_info("root-package")]},
5951
[],
6052
{"root-package": 1},
61-
(False, True, True, 1, True, "root-package"),
53+
(False, True, 1, True, "root-package"),
6254
),
6355
# Unfree package (with specifier operator)
6456
(
6557
"unfree-package",
6658
{"unfree-package": [build_req_info("unfree-package<1")]},
6759
[],
6860
{},
69-
(False, True, True, math.inf, False, "unfree-package"),
61+
(False, True, math.inf, False, "unfree-package"),
7062
),
7163
# Free package (no operator)
7264
(
7365
"free-package",
7466
{"free-package": [build_req_info("free-package")]},
7567
[],
7668
{},
77-
(False, True, True, math.inf, True, "free-package"),
69+
(False, True, math.inf, True, "free-package"),
7870
),
7971
],
8072
)
@@ -102,37 +94,56 @@ def test_get_preference(
10294

10395

10496
@pytest.mark.parametrize(
105-
"identifiers, expected",
97+
"identifiers, backtrack_causes, expected",
10698
[
107-
# Case 1: REQUIRES_PYTHON_IDENTIFIER is present at the beginning
99+
# REQUIRES_PYTHON_IDENTIFIER is present
108100
(
109-
[REQUIRES_PYTHON_IDENTIFIER, "package1", "package2"],
101+
[REQUIRES_PYTHON_IDENTIFIER, "package1", "package2", "backtrack-package"],
102+
[build_req_info("backtrack-package")],
110103
[REQUIRES_PYTHON_IDENTIFIER],
111104
),
112-
# Case 2: REQUIRES_PYTHON_IDENTIFIER is present in the middle
105+
# REQUIRES_PYTHON_IDENTIFIER is present after backtrack causes
113106
(
114-
["package1", REQUIRES_PYTHON_IDENTIFIER, "package2"],
107+
["package1", "package2", "backtrack-package", REQUIRES_PYTHON_IDENTIFIER],
108+
[build_req_info("backtrack-package")],
115109
[REQUIRES_PYTHON_IDENTIFIER],
116110
),
117-
# Case 3: REQUIRES_PYTHON_IDENTIFIER is not present
111+
# Backtrack causes present (direct requirement)
112+
(
113+
["package1", "package2", "backtrack-package"],
114+
[build_req_info("backtrack-package")],
115+
["backtrack-package"],
116+
),
117+
# Multiple backtrack causes
118+
(
119+
["package1", "backtrack1", "backtrack2", "package2"],
120+
[build_req_info("backtrack1"), build_req_info("backtrack2")],
121+
["backtrack1", "backtrack2"],
122+
),
123+
# No special identifiers - return all
118124
(
119125
["package1", "package2"],
126+
[],
120127
["package1", "package2"],
121128
),
122-
# Case 4: Empty list of identifiers
129+
# Empty list of identifiers
123130
(
124131
[],
125132
[],
133+
[],
126134
),
127135
],
128136
)
129137
def test_narrow_requirement_selection(
130138
identifiers: List[str],
139+
backtrack_causes: Sequence["PreferenceInformation"],
131140
expected: List[str],
132141
factory: Factory,
133142
) -> None:
134-
"""Test that narrow_requirement_selection correctly prioritizes
135-
REQUIRES_PYTHON_IDENTIFIER when present in the list of identifiers.
143+
"""Test that narrow_requirement_selection correctly prioritizes identifiers:
144+
1. REQUIRES_PYTHON_IDENTIFIER (if present)
145+
2. Backtrack causes (if present)
146+
3. All other identifiers (as-is)
136147
"""
137148
provider = PipProvider(
138149
factory=factory,
@@ -142,6 +153,8 @@ def test_narrow_requirement_selection(
142153
user_requested={},
143154
)
144155

145-
result = provider.narrow_requirement_selection(identifiers, {}, {}, {}, [])
156+
result = provider.narrow_requirement_selection(
157+
identifiers, {}, {}, {}, backtrack_causes
158+
)
146159

147160
assert list(result) == expected, f"Expected {expected}, got {list(result)}"

0 commit comments

Comments
 (0)