22
33import dataclasses
44import os
5+ from collections import defaultdict
56from functools import cached_property
67from typing import TYPE_CHECKING , Callable
78
3940
4041
4142_PROVIDER_REGISTRY : dict [str , type [BaseProvider ]] = {}
43+ _CONFLICT_PRIORITY_THRESHOLD = 5
4244
4345
4446def get_provider (strategy : str ) -> type [BaseProvider ]:
@@ -79,6 +81,8 @@ def __init__(
7981 self .excludes = {normalize_name (k ) for k in project .pyproject .resolution .get ("excludes" , [])}
8082 self .direct_minimal_versions = direct_minimal_versions
8183 self .locked_repository = locked_repository
84+ self ._conflict_counts : defaultdict [str , int ] = defaultdict (int )
85+ self ._conflict_promoted : set [str ] = set ()
8286
8387 def requirement_preference (self , requirement : Requirement ) -> Comparable :
8488 """Return the preference of a requirement to find candidates.
@@ -97,12 +101,49 @@ def requirement_preference(self, requirement: Requirement) -> Comparable:
97101 def identify (self , requirement_or_candidate : Requirement | Candidate ) -> str :
98102 return requirement_or_candidate .identify ()
99103
104+ def narrow_requirement_selection (
105+ self ,
106+ identifiers : Iterable [str ],
107+ resolutions : Mapping [str , Candidate ],
108+ candidates : Mapping [str , Iterator [Candidate ]],
109+ information : Mapping [str , Iterator [RequirementInformation ]],
110+ backtrack_causes : Sequence [RequirementInformation ],
111+ ) -> Iterable [str ]:
112+ backtrack_identifiers : set [str ] = set ()
113+ for requirement , parent in backtrack_causes :
114+ names = [requirement .identify ()]
115+ if parent is not None :
116+ names .append (parent .identify ())
117+ for name in names :
118+ backtrack_identifiers .add (name )
119+ if name not in resolutions :
120+ self ._conflict_counts [name ] += 1
121+ if self ._conflict_counts [name ] >= _CONFLICT_PRIORITY_THRESHOLD :
122+ self ._conflict_promoted .add (name )
123+
124+ current_backtrack_causes : list [str ] = []
125+ promoted : list [str ] = []
126+ for identifier in identifiers :
127+ if identifier == "python" :
128+ return [identifier ]
129+ if identifier in backtrack_identifiers :
130+ current_backtrack_causes .append (identifier )
131+ continue
132+ if identifier in self ._conflict_promoted :
133+ promoted .append (identifier )
134+
135+ if current_backtrack_causes :
136+ return current_backtrack_causes
137+ if promoted :
138+ return promoted
139+ return identifiers
140+
100141 def get_preference (
101142 self ,
102143 identifier : str ,
103- resolutions : dict [str , Candidate ],
104- candidates : dict [str , Iterator [Candidate ]],
105- information : dict [str , Iterator [RequirementInformation ]],
144+ resolutions : Mapping [str , Candidate ],
145+ candidates : Mapping [str , Iterator [Candidate ]],
146+ information : Mapping [str , Iterator [RequirementInformation ]],
106147 backtrack_causes : Sequence [RequirementInformation ],
107148 ) -> tuple [Comparable , ...]:
108149 is_top = any (parent is None for _ , parent in information [identifier ])
@@ -123,9 +164,11 @@ def get_preference(
123164 is_python = identifier == "python"
124165 is_pinned = any (op [:2 ] == "==" for op in operators )
125166 constraints = len (operators )
167+ is_conflict_promoted = identifier in self ._conflict_promoted
126168 return (
127169 not is_python ,
128170 not is_top ,
171+ not is_conflict_promoted ,
129172 not is_file_or_url ,
130173 not is_pinned ,
131174 not is_backtrack_cause ,
@@ -458,9 +501,9 @@ def get_dependencies(self, candidate: Candidate) -> list[Requirement]:
458501 def get_preference (
459502 self ,
460503 identifier : str ,
461- resolutions : dict [str , Candidate ],
462- candidates : dict [str , Iterator [Candidate ]],
463- information : dict [str , Iterator [RequirementInformation ]],
504+ resolutions : Mapping [str , Candidate ],
505+ candidates : Mapping [str , Iterator [Candidate ]],
506+ information : Mapping [str , Iterator [RequirementInformation ]],
464507 backtrack_causes : Sequence [RequirementInformation ],
465508 ) -> tuple [Comparable , ...]:
466509 # Resolve tracking packages so we have a chance to unpin them first.
0 commit comments