33
44import click
55from packaging .requirements import Requirement
6+ from packaging .utils import NormalizedName
7+ from packaging .version import Version
68
79from .. import (
810 bootstrapper ,
@@ -180,7 +182,17 @@ def write_constraints_file(
180182 conflicts = graph .get_install_dependency_versions ()
181183 ret = True
182184 conflicting_deps = set ()
183- for dep_name , nodes in sorted (conflicts .items ()):
185+
186+ # Map for already resolved versions for a given dependency Eg: {"a": "0.4"}
187+ resolved : dict [NormalizedName , Version ] = {}
188+
189+ # List of unresolved dependencies
190+ unresolved_dependencies = sorted (conflicts .items ())
191+
192+ dep_name : NormalizedName
193+
194+ # Loop over dependencies and resolve dependencies with single version first. This will shrink the unresolved_dependencies to begin with.
195+ for dep_name , nodes in unresolved_dependencies [:]:
184196 versions = [node .version for node in nodes ]
185197 if len (versions ) == 0 :
186198 # This should never happen.
@@ -189,64 +201,99 @@ def write_constraints_file(
189201 if len (versions ) == 1 :
190202 # This is going to be the situation for most dependencies, where we
191203 # only have one version.
192- output .write (f"{ dep_name } =={ versions [0 ]} \n " )
193- continue
194-
195- # Below this point we have built multiple versions of the same thing, so
196- # we need to try to determine if any one of those versions meets all of
197- # the requirements.
198- logger .debug ("%s: found multiple versions in install requirements" , dep_name )
199-
200- # Track which versions can be used by which parent requirement.
201- usable_versions : dict [str , list [str ]] = {}
202- # Track how many total users of a requirement (by name) there are so we
203- # can tell later if any version can be used by all of them.
204- user_counter = 0
205-
206- # Which parent requirements can use which versions of the dependency we
207- # are working on?
208- for node in nodes :
209- parent_edges = node .get_incoming_install_edges ()
210- user_counter += len (parent_edges )
211- for parent_edge in parent_edges :
212- for matching_version in parent_edge .req .specifier .filter (versions ):
213- usable_versions .setdefault (str (matching_version ), []).append (
214- str (parent_edge .destination_node .version )
204+ resolved [dep_name ] = versions [0 ]
205+ # Remove from unresolved dependencies list
206+ unresolved_dependencies .remove ((dep_name , nodes ))
207+ multiple_versions = dict (unresolved_dependencies )
208+
209+ # Below this point we have built multiple versions of the same thing, so
210+ # we need to try to determine if any one of those versions meets all of
211+ # the requirements.
212+
213+ # Flag to see if something is resolved
214+ resolved_something = True
215+
216+ # Outer while loop to resolve remaining dependencies with multiple versions
217+ while unresolved_dependencies and resolved_something :
218+ resolved_something = False
219+ # Make copy of the original list and loop over unresolved dependencies
220+ for dep_name , nodes in unresolved_dependencies [:]:
221+ # Track which versions can be used by which parent requirement.
222+ usable_versions : dict [str , list [Version ]] = {}
223+ # Track how many total users of a requirement (by name) there are so we
224+ # can tell later if any version can be used by all of them.
225+ user_counter = 0
226+ # Which parent requirements can use which versions of the dependency we
227+ # are working on?
228+ dep_versions = [node .version for node in nodes ]
229+ # Loop over the nodes list
230+ for node in nodes :
231+ parent_edges = node .get_incoming_install_edges ()
232+ # Loop over parent_edges list
233+ for parent_edge in parent_edges :
234+ parent_name = parent_edge .destination_node .canonicalized_name
235+ # Condition to select the right version.
236+ # We check whether parent_name is already in resolved dict and the version associated with that
237+ # is not the version of the destination node
238+ if (
239+ parent_name in resolved
240+ and resolved [parent_name ]
241+ != parent_edge .destination_node .version
242+ ):
243+ continue
244+ # Loop to find the usable versions
245+ for matching_version in parent_edge .req .specifier .filter (
246+ dep_versions
247+ ):
248+ usable_versions .setdefault (str (matching_version ), []).append (
249+ parent_edge .destination_node .version
250+ )
251+ user_counter += 1
252+
253+ # Look for one version that can be used by all the parent dependencies
254+ # and output that if we find it. Otherwise, include a warning and report
255+ # all versions so a human reading the file can make their own decision
256+ # about how to resolve the conflict.
257+ for v , users in usable_versions .items ():
258+ if len (users ) == user_counter :
259+ version_strs = [str (v ) for v in sorted (versions )]
260+ logger .debug (
261+ "%s: selecting %s from multiple candidates %s" ,
262+ dep_name ,
263+ v ,
264+ version_strs ,
215265 )
266+ resolved [dep_name ] = Version (v )
267+ resolved_something = True
268+ unresolved_dependencies .remove ((dep_name , nodes ))
216269
217- # Look for one version that can be used by all the parent dependencies
218- # and output that if we find it. Otherwise, include a warning and report
219- # all versions so a human reading the file can make their own decision
220- # about how to resolve the conflict.
221- for v , users in usable_versions .items ():
222- if len (users ) == user_counter :
223- version_strs = [str (v ) for v in sorted (versions )]
224- output .write (
225- f"# NOTE: fromager selected { dep_name } =={ v } from: { version_strs } \n "
226- )
227- logger .debug (
228- "%s: selecting %s from multiple candidates %s" ,
229- dep_name ,
230- v ,
231- version_strs ,
232- )
233- output .write (f"{ dep_name } =={ v } \n " )
234- break
235- else :
236- # No single version could be used, so go ahead and print all the
237- # versions with a warning message
238- ret = False
270+ # Write resolved versions to constraints file
271+ for dep_name , resolved_version in sorted (resolved .items ()):
272+ if dep_name in multiple_versions :
273+ version_strs = [
274+ str (node .version )
275+ for node in sorted (multiple_versions [dep_name ], key = lambda n : n .version )
276+ ]
239277 output .write (
240- f"# ERROR: no single version of { dep_name } met all requirements \n "
278+ f"# NOTE: fromager selected { dep_name } == { resolved_version } from: { version_strs } \n "
241279 )
242- logger .error ("%s: no single version meets all requirements" , dep_name )
243- conflicting_deps .add (dep_name )
244- for dv in sorted (versions ):
245- output .write (f"{ dep_name } =={ dv } \n " )
280+ output .write (f"{ dep_name } =={ resolved_version } \n " )
281+
282+ # No single version could be used, so go ahead and print all the
283+ # versions with a warning message
284+ for dep_name , nodes in unresolved_dependencies :
285+ ret = False
286+ logger .error ("%s: no single version meets all requirements" , dep_name )
287+ output .write (f"# ERROR: no single version of { dep_name } met all requirements\n " )
288+ conflicting_deps .add (dep_name )
289+ for node in sorted (nodes , key = lambda n : n .version ):
290+ output .write (f"{ dep_name } =={ node .version } \n " )
291+
246292 for dep_name in conflicting_deps :
247293 logger .error ("finding why %s was being used" , dep_name )
248294 for node in graph .get_nodes_by_name (dep_name ):
249295 find_why (graph , node , - 1 , 1 , [])
296+
250297 return ret
251298
252299
0 commit comments