1212from pip ._internal .resolution .resolvelib .base import Candidate
1313from pip ._internal .resolution .resolvelib .candidates import REQUIRES_PYTHON_IDENTIFIER
1414from pip ._internal .resolution .resolvelib .factory import Factory
15- from pip ._internal .resolution .resolvelib .provider import PipProvider
15+ from pip ._internal .resolution .resolvelib .provider import (
16+ _CONFLICT_PRIORITY_THRESHOLD ,
17+ PipProvider ,
18+ )
1619from pip ._internal .resolution .resolvelib .requirements import (
1720 ExplicitRequirement ,
1821 SpecifierRequirement ,
@@ -60,55 +63,55 @@ def build_explicit_req_info(
6063 {"pinned-package" : [build_req_info ("pinned-package==1.0" )]},
6164 [],
6265 {},
63- (True , False , True , math .inf , False , "pinned-package" ),
66+ (True , True , False , True , math .inf , False , "pinned-package" ),
6467 ),
6568 # Star-specified package, i.e. with "*"
6669 (
6770 "star-specified-package" ,
6871 {"star-specified-package" : [build_req_info ("star-specified-package==1.*" )]},
6972 [],
7073 {},
71- (True , True , False , math .inf , False , "star-specified-package" ),
74+ (True , True , True , False , math .inf , False , "star-specified-package" ),
7275 ),
7376 # Package that caused backtracking
7477 (
7578 "backtrack-package" ,
7679 {"backtrack-package" : [build_req_info ("backtrack-package" )]},
7780 [build_req_info ("backtrack-package" )],
7881 {},
79- (True , True , True , math .inf , True , "backtrack-package" ),
82+ (True , True , True , True , math .inf , True , "backtrack-package" ),
8083 ),
8184 # Root package requested by user
8285 (
8386 "root-package" ,
8487 {"root-package" : [build_req_info ("root-package" )]},
8588 [],
8689 {"root-package" : 1 },
87- (True , True , True , 1 , True , "root-package" ),
90+ (True , True , True , True , 1 , True , "root-package" ),
8891 ),
8992 # Unfree package (with specifier operator)
9093 (
9194 "unfree-package" ,
9295 {"unfree-package" : [build_req_info ("unfree-package!=1" )]},
9396 [],
9497 {},
95- (True , True , True , math .inf , False , "unfree-package" ),
98+ (True , True , True , True , math .inf , False , "unfree-package" ),
9699 ),
97100 # Free package (no operator)
98101 (
99102 "free-package" ,
100103 {"free-package" : [build_req_info ("free-package" )]},
101104 [],
102105 {},
103- (True , True , True , math .inf , True , "free-package" ),
106+ (True , True , True , True , math .inf , True , "free-package" ),
104107 ),
105108 # Test case for "direct" preference (explicit URL)
106109 (
107110 "direct-package" ,
108111 {"direct-package" : [build_explicit_req_info ("direct-package" )]},
109112 [],
110113 {},
111- (False , True , True , math .inf , True , "direct-package" ),
114+ (True , False , True , True , math .inf , True , "direct-package" ),
112115 ),
113116 # Upper bounded with <= operator
114117 (
@@ -120,15 +123,15 @@ def build_explicit_req_info(
120123 },
121124 [],
122125 {},
123- (True , True , False , math .inf , False , "upper-bound-lte-package" ),
126+ (True , True , True , False , math .inf , False , "upper-bound-lte-package" ),
124127 ),
125128 # Upper bounded with < operator
126129 (
127130 "upper-bound-lt-package" ,
128131 {"upper-bound-lt-package" : [build_req_info ("upper-bound-lt-package<2.0" )]},
129132 [],
130133 {},
131- (True , True , False , math .inf , False , "upper-bound-lt-package" ),
134+ (True , True , True , False , math .inf , False , "upper-bound-lt-package" ),
132135 ),
133136 # Upper bounded with ~= operator
134137 (
@@ -140,15 +143,23 @@ def build_explicit_req_info(
140143 },
141144 [],
142145 {},
143- (True , True , False , math .inf , False , "upper-bound-compatible-package" ),
146+ (
147+ True ,
148+ True ,
149+ True ,
150+ False ,
151+ math .inf ,
152+ False ,
153+ "upper-bound-compatible-package" ,
154+ ),
144155 ),
145156 # Not upper bounded, using only >= operator
146157 (
147158 "lower-bound-package" ,
148159 {"lower-bound-package" : [build_req_info ("lower-bound-package>=1.0" )]},
149160 [],
150161 {},
151- (True , True , True , math .inf , False , "lower-bound-package" ),
162+ (True , True , True , True , math .inf , False , "lower-bound-package" ),
152163 ),
153164 ],
154165)
@@ -225,7 +236,8 @@ def test_narrow_requirement_selection(
225236 """Test that narrow_requirement_selection correctly prioritizes identifiers:
226237 1. REQUIRES_PYTHON_IDENTIFIER (if present)
227238 2. Backtrack causes (if present)
228- 3. All other identifiers (as-is)
239+ 3. Conflict-promoted identifiers (if present)
240+ 4. All other identifiers (as-is)
229241 """
230242 provider = PipProvider (
231243 factory = factory ,
@@ -240,3 +252,41 @@ def test_narrow_requirement_selection(
240252 )
241253
242254 assert list (result ) == expected , f"Expected { expected } , got { list (result )} "
255+
256+
257+ def test_conflict_promotion_after_threshold (provider : PipProvider ) -> None :
258+ """Repeated unresolved backtrack causes get promoted after the threshold."""
259+ narrow = provider .narrow_requirement_selection
260+ cause = [build_req_info ("conflict-pkg" )]
261+
262+ # Below threshold: no promotion, all identifiers returned.
263+ for i in range (1 , _CONFLICT_PRIORITY_THRESHOLD ):
264+ result = list (narrow (["other-pkg" ], {}, {}, {}, cause ))
265+ assert result == ["other-pkg" ], f"Unexpected promotion at call { i } "
266+
267+ # At threshold: conflict-pkg is a backtrack cause so it wins on that basis.
268+ result = list (narrow (["other-pkg" , "conflict-pkg" ], {}, {}, {}, cause ))
269+ assert result == ["conflict-pkg" ]
270+
271+ # Without active backtrack causes, the promoted package is still preferred.
272+ result = list (narrow (["other-pkg" , "conflict-pkg" ], {}, {}, {}, []))
273+ assert result == ["conflict-pkg" ]
274+
275+ # Backtrack causes still win over promoted-only packages.
276+ other_cause = [build_req_info ("other-pkg" )]
277+ result = list (narrow (["other-pkg" , "conflict-pkg" ], {}, {}, {}, other_cause ))
278+ assert result == ["other-pkg" ]
279+
280+
281+ def test_conflict_promoted_get_preference (provider : PipProvider ) -> None :
282+ """Promoted packages sort before non-promoted in get_preference."""
283+ provider ._conflict_promoted .add ("promoted-pkg" )
284+
285+ info = {
286+ "promoted-pkg" : [build_req_info ("promoted-pkg" )],
287+ "normal-pkg" : [build_req_info ("normal-pkg" )],
288+ }
289+ pref = provider .get_preference ("promoted-pkg" , {}, {}, info , [])
290+ pref_other = provider .get_preference ("normal-pkg" , {}, {}, info , [])
291+
292+ assert pref < pref_other
0 commit comments