3232
3333
3434if TYPE_CHECKING :
35+ from poetry .core .version .markers import BaseMarker
3536 from tomlkit .items import InlineTable
3637 from tomlkit .toml_document import TOMLDocument
3738
@@ -203,102 +204,105 @@ def locked_repository(self, with_dev_reqs: bool = False) -> Repository:
203204
204205 @staticmethod
205206 def __get_locked_package (
206- _dependency : Dependency , packages_by_name : dict [str , list [Package ]]
207+ dependency : Dependency ,
208+ packages_by_name : dict [str , list [Package ]],
209+ decided : dict [Package , Dependency ] | None = None ,
207210 ) -> Package | None :
208211 """
209212 Internal helper to identify corresponding locked package using dependency
210213 version constraints.
211214 """
212- for _package in packages_by_name .get (_dependency .name , []):
213- if _dependency .constraint .allows (_package .version ):
214- return _package
215- return None
215+ decided = decided or {}
216+
217+ # Get the packages that are consistent with this dependency.
218+ packages = [
219+ package
220+ for package in packages_by_name .get (dependency .name , [])
221+ if package .python_constraint .allows_all (dependency .python_constraint )
222+ and dependency .constraint .allows (package .version )
223+ ]
224+
225+ # If we've previously made a choice that is compatible with the current
226+ # requirement, stick with it.
227+ for package in packages :
228+ old_decision = decided .get (package )
229+ if (
230+ old_decision is not None
231+ and not old_decision .marker .intersect (dependency .marker ).is_empty ()
232+ ):
233+ return package
234+
235+ return next (iter (packages ), None )
216236
217237 @classmethod
218- def __walk_dependency_level (
238+ def __walk_dependencies (
219239 cls ,
220240 dependencies : list [Dependency ],
221- level : int ,
222- pinned_versions : bool ,
223241 packages_by_name : dict [str , list [Package ]],
224- project_level_dependencies : set [str ],
225- nested_dependencies : dict [tuple [str , str ], Dependency ],
226- ) -> dict [tuple [str , str ], Dependency ]:
227- if not dependencies :
228- return nested_dependencies
229-
230- next_level_dependencies = []
231-
232- for requirement in dependencies :
233- key = (requirement .name , requirement .pretty_constraint )
234- locked_package = cls .__get_locked_package (requirement , packages_by_name )
235-
236- if locked_package :
237- # create dependency from locked package to retain dependency metadata
238- # if this is not done, we can end-up with incorrect nested dependencies
239- constraint = requirement .constraint
240- pretty_constraint = requirement .pretty_constraint
241- marker = requirement .marker
242- requirement = locked_package .to_dependency ()
243- requirement .marker = requirement .marker .intersect (marker )
242+ ) -> dict [Package , Dependency ]:
243+ nested_dependencies : dict [Package , Dependency ] = {}
244244
245- key = (requirement .name , pretty_constraint )
245+ visited : set [tuple [Dependency , BaseMarker ]] = set ()
246+ while dependencies :
247+ requirement = dependencies .pop (0 )
248+ if (requirement , requirement .marker ) in visited :
249+ continue
250+ visited .add ((requirement , requirement .marker ))
246251
247- if not pinned_versions :
248- requirement .set_constraint (constraint )
252+ locked_package = cls .__get_locked_package (
253+ requirement , packages_by_name , nested_dependencies
254+ )
249255
250- for require in locked_package .requires :
251- if require .marker .is_empty ():
252- require .marker = requirement .marker
253- else :
254- require .marker = require .marker .intersect (requirement .marker )
256+ if not locked_package :
257+ raise RuntimeError (f"Dependency walk failed at { requirement } " )
255258
256- require .marker = require .marker .intersect (locked_package .marker )
259+ # create dependency from locked package to retain dependency metadata
260+ # if this is not done, we can end-up with incorrect nested dependencies
261+ constraint = requirement .constraint
262+ marker = requirement .marker
263+ extras = requirement .extras
264+ requirement = locked_package .to_dependency ()
265+ requirement .marker = requirement .marker .intersect (marker )
257266
258- if key not in nested_dependencies :
259- next_level_dependencies .append (require )
267+ requirement .set_constraint (constraint )
260268
261- if requirement . name in project_level_dependencies and level == 0 :
262- # project level dependencies take precedence
263- continue
269+ for require in locked_package . requires :
270+ if require . in_extras and extras . isdisjoint ( require . in_extras ):
271+ continue
264272
265- if not locked_package :
266- # we make a copy to avoid any side-effects
267- requirement = deepcopy (requirement )
273+ require = deepcopy (require )
274+ require .marker = require .marker .intersect (requirement .marker )
275+ if not require .marker .is_empty ():
276+ dependencies .append (require )
268277
278+ key = locked_package
269279 if key not in nested_dependencies :
270280 nested_dependencies [key ] = requirement
271281 else :
272282 nested_dependencies [key ].marker = nested_dependencies [key ].marker .union (
273283 requirement .marker
274284 )
275285
276- return cls .__walk_dependency_level (
277- dependencies = next_level_dependencies ,
278- level = level + 1 ,
279- pinned_versions = pinned_versions ,
280- packages_by_name = packages_by_name ,
281- project_level_dependencies = project_level_dependencies ,
282- nested_dependencies = nested_dependencies ,
283- )
286+ return nested_dependencies
284287
285288 @classmethod
286289 def get_project_dependencies (
287290 cls ,
288291 project_requires : list [Dependency ],
289292 locked_packages : list [Package ],
290- pinned_versions : bool = False ,
291- with_nested : bool = False ,
292- ) -> Iterable [Dependency ]:
293+ ) -> Iterable [tuple [Package , Dependency ]]:
293294 # group packages entries by name, this is required because requirement might use
294- # different constraints
295+ # different constraints.
295296 packages_by_name : dict [str , list [Package ]] = {}
296297 for pkg in locked_packages :
297298 if pkg .name not in packages_by_name :
298299 packages_by_name [pkg .name ] = []
299300 packages_by_name [pkg .name ].append (pkg )
300301
301- project_level_dependencies = set ()
302+ # Put higher versions first so that we prefer them.
303+ for packages in packages_by_name .values ():
304+ packages .sort (key = lambda package : package .version , reverse = True )
305+
302306 dependencies = []
303307
304308 for dependency in project_requires :
@@ -310,38 +314,18 @@ def get_project_dependencies(
310314 locked_package .marker
311315 )
312316
313- if not pinned_versions :
314- locked_dependency .set_constraint (dependency .constraint )
317+ locked_dependency .set_constraint (dependency .constraint )
315318
316319 dependency = locked_dependency
317320
318- project_level_dependencies .add (dependency .name )
319321 dependencies .append (dependency )
320322
321- if not with_nested :
322- # return only with project level dependencies
323- return dependencies
324-
325- nested_dependencies = cls .__walk_dependency_level (
323+ nested_dependencies = cls .__walk_dependencies (
326324 dependencies = dependencies ,
327- level = 0 ,
328- pinned_versions = pinned_versions ,
329325 packages_by_name = packages_by_name ,
330- project_level_dependencies = project_level_dependencies ,
331- nested_dependencies = {},
332326 )
333327
334- # Merge same dependencies using marker union
335- for requirement in dependencies :
336- key = (requirement .name , requirement .pretty_constraint )
337- if key not in nested_dependencies :
338- nested_dependencies [key ] = requirement
339- else :
340- nested_dependencies [key ].marker = nested_dependencies [key ].marker .union (
341- requirement .marker
342- )
343-
344- return sorted (nested_dependencies .values (), key = lambda x : x .name .lower ())
328+ return nested_dependencies .items ()
345329
346330 def get_project_dependency_packages (
347331 self ,
@@ -379,16 +363,10 @@ def get_project_dependency_packages(
379363
380364 selected .append (dependency )
381365
382- for dependency in self .get_project_dependencies (
366+ for package , dependency in self .get_project_dependencies (
383367 project_requires = selected ,
384368 locked_packages = repository .packages ,
385- with_nested = True ,
386369 ):
387- try :
388- package = repository .find_packages (dependency = dependency )[0 ]
389- except IndexError :
390- continue
391-
392370 for extra in dependency .extras :
393371 package .requires_extras .append (extra )
394372
0 commit comments