|
32 | 32 |
|
33 | 33 |
|
34 | 34 | if TYPE_CHECKING: |
| 35 | + from poetry.core.version.markers import BaseMarker |
35 | 36 | from tomlkit.items import InlineTable |
36 | 37 | from tomlkit.toml_document import TOMLDocument |
37 | 38 |
|
@@ -203,145 +204,113 @@ def locked_repository(self, with_dev_reqs: bool = False) -> Repository: |
203 | 204 |
|
204 | 205 | @staticmethod |
205 | 206 | 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, |
207 | 210 | ) -> Package | None: |
208 | 211 | """ |
209 | 212 | Internal helper to identify corresponding locked package using dependency |
210 | 213 | version constraints. |
211 | 214 | """ |
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) |
216 | 236 |
|
217 | 237 | @classmethod |
218 | | - def __walk_dependency_level( |
| 238 | + def __walk_dependencies( |
219 | 239 | cls, |
220 | 240 | dependencies: list[Dependency], |
221 | | - level: int, |
222 | | - pinned_versions: bool, |
223 | 241 | 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 = [] |
| 242 | + ) -> dict[Package, Dependency]: |
| 243 | + nested_dependencies: dict[Package, Dependency] = {} |
231 | 244 |
|
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) |
244 | | - |
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)) |
246 | 251 |
|
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 | + ) |
249 | 255 |
|
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}") |
255 | 258 |
|
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) |
257 | 266 |
|
258 | | - if key not in nested_dependencies: |
259 | | - next_level_dependencies.append(require) |
| 267 | + requirement.set_constraint(constraint) |
260 | 268 |
|
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 |
264 | 272 |
|
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( |
| 275 | + requirement.marker.without_extras() |
| 276 | + ) |
| 277 | + if not require.marker.is_empty(): |
| 278 | + dependencies.append(require) |
268 | 279 |
|
| 280 | + key = locked_package |
269 | 281 | if key not in nested_dependencies: |
270 | 282 | nested_dependencies[key] = requirement |
271 | 283 | else: |
272 | 284 | nested_dependencies[key].marker = nested_dependencies[key].marker.union( |
273 | 285 | requirement.marker |
274 | 286 | ) |
275 | 287 |
|
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 | | - ) |
| 288 | + return nested_dependencies |
284 | 289 |
|
285 | 290 | @classmethod |
286 | 291 | def get_project_dependencies( |
287 | 292 | cls, |
288 | 293 | project_requires: list[Dependency], |
289 | 294 | locked_packages: list[Package], |
290 | | - pinned_versions: bool = False, |
291 | | - with_nested: bool = False, |
292 | | - ) -> Iterable[Dependency]: |
| 295 | + ) -> Iterable[tuple[Package, Dependency]]: |
293 | 296 | # group packages entries by name, this is required because requirement might use |
294 | | - # different constraints |
| 297 | + # different constraints. |
295 | 298 | packages_by_name: dict[str, list[Package]] = {} |
296 | 299 | for pkg in locked_packages: |
297 | 300 | if pkg.name not in packages_by_name: |
298 | 301 | packages_by_name[pkg.name] = [] |
299 | 302 | packages_by_name[pkg.name].append(pkg) |
300 | 303 |
|
301 | | - project_level_dependencies = set() |
302 | | - dependencies = [] |
| 304 | + # Put higher versions first so that we prefer them. |
| 305 | + for packages in packages_by_name.values(): |
| 306 | + packages.sort(key=lambda package: package.version, reverse=True) |
303 | 307 |
|
304 | | - for dependency in project_requires: |
305 | | - dependency = deepcopy(dependency) |
306 | | - locked_package = cls.__get_locked_package(dependency, packages_by_name) |
307 | | - if locked_package: |
308 | | - locked_dependency = locked_package.to_dependency() |
309 | | - locked_dependency.marker = dependency.marker.intersect( |
310 | | - locked_package.marker |
311 | | - ) |
312 | | - |
313 | | - if not pinned_versions: |
314 | | - locked_dependency.set_constraint(dependency.constraint) |
315 | | - |
316 | | - dependency = locked_dependency |
317 | | - |
318 | | - project_level_dependencies.add(dependency.name) |
319 | | - dependencies.append(dependency) |
320 | | - |
321 | | - if not with_nested: |
322 | | - # return only with project level dependencies |
323 | | - return dependencies |
324 | | - |
325 | | - nested_dependencies = cls.__walk_dependency_level( |
326 | | - dependencies=dependencies, |
327 | | - level=0, |
328 | | - pinned_versions=pinned_versions, |
| 308 | + nested_dependencies = cls.__walk_dependencies( |
| 309 | + dependencies=project_requires, |
329 | 310 | packages_by_name=packages_by_name, |
330 | | - project_level_dependencies=project_level_dependencies, |
331 | | - nested_dependencies={}, |
332 | 311 | ) |
333 | 312 |
|
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()) |
| 313 | + return nested_dependencies.items() |
345 | 314 |
|
346 | 315 | def get_project_dependency_packages( |
347 | 316 | self, |
@@ -379,16 +348,10 @@ def get_project_dependency_packages( |
379 | 348 |
|
380 | 349 | selected.append(dependency) |
381 | 350 |
|
382 | | - for dependency in self.get_project_dependencies( |
| 351 | + for package, dependency in self.get_project_dependencies( |
383 | 352 | project_requires=selected, |
384 | 353 | locked_packages=repository.packages, |
385 | | - with_nested=True, |
386 | 354 | ): |
387 | | - try: |
388 | | - package = repository.find_packages(dependency=dependency)[0] |
389 | | - except IndexError: |
390 | | - continue |
391 | | - |
392 | 355 | for extra in dependency.extras: |
393 | 356 | package.requires_extras.append(extra) |
394 | 357 |
|
|
0 commit comments