|
32 | 32 |
|
33 | 33 |
|
34 | 34 | if TYPE_CHECKING: |
| 35 | + from poetry.core.semver.version_constraint import VersionConstraint |
| 36 | + from poetry.core.version.markers import BaseMarker |
35 | 37 | from tomlkit.items import InlineTable |
36 | 38 | from tomlkit.toml_document import TOMLDocument |
37 | 39 |
|
@@ -203,152 +205,130 @@ def locked_repository(self, with_dev_reqs: bool = False) -> Repository: |
203 | 205 |
|
204 | 206 | @staticmethod |
205 | 207 | def __get_locked_package( |
206 | | - _dependency: Dependency, packages_by_name: dict[str, list[Package]] |
| 208 | + dependency: Dependency, |
| 209 | + packages_by_name: dict[str, list[Package]], |
| 210 | + decided: dict[Package, Dependency] | None = None, |
207 | 211 | ) -> Package | None: |
208 | 212 | """ |
209 | 213 | Internal helper to identify corresponding locked package using dependency |
210 | 214 | version constraints. |
211 | 215 | """ |
212 | | - for _package in packages_by_name.get(_dependency.name, []): |
213 | | - if _dependency.constraint.allows(_package.version): |
214 | | - return _package |
215 | | - return None |
| 216 | + decided = decided or {} |
| 217 | + |
| 218 | + # Get the packages that are consistent with this dependency. |
| 219 | + packages = [ |
| 220 | + package |
| 221 | + for package in packages_by_name.get(dependency.name, []) |
| 222 | + if package.python_constraint.allows_all(dependency.python_constraint) |
| 223 | + and dependency.constraint.allows(package.version) |
| 224 | + ] |
| 225 | + |
| 226 | + # If we've previously made a choice that is compatible with the current |
| 227 | + # requirement, stick with it. |
| 228 | + for package in packages: |
| 229 | + old_decision = decided.get(package) |
| 230 | + if ( |
| 231 | + old_decision is not None |
| 232 | + and not old_decision.marker.intersect(dependency.marker).is_empty() |
| 233 | + ): |
| 234 | + return package |
| 235 | + |
| 236 | + return next(iter(packages), None) |
216 | 237 |
|
217 | 238 | @classmethod |
218 | | - def __walk_dependency_level( |
| 239 | + def __walk_dependencies( |
219 | 240 | cls, |
220 | 241 | dependencies: list[Dependency], |
221 | | - level: int, |
222 | | - pinned_versions: bool, |
223 | 242 | 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 = [] |
| 243 | + ) -> dict[Package, Dependency]: |
| 244 | + nested_dependencies: dict[Package, Dependency] = {} |
231 | 245 |
|
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) |
| 246 | + visited: set[tuple[Dependency, BaseMarker]] = set() |
| 247 | + while dependencies: |
| 248 | + requirement = dependencies.pop(0) |
| 249 | + if (requirement, requirement.marker) in visited: |
| 250 | + continue |
| 251 | + visited.add((requirement, requirement.marker)) |
246 | 252 |
|
247 | | - if not pinned_versions: |
248 | | - requirement.set_constraint(constraint) |
| 253 | + locked_package = cls.__get_locked_package( |
| 254 | + requirement, packages_by_name, nested_dependencies |
| 255 | + ) |
249 | 256 |
|
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) |
| 257 | + if not locked_package: |
| 258 | + raise RuntimeError(f"Dependency walk failed at {requirement}") |
255 | 259 |
|
256 | | - require.marker = require.marker.intersect(locked_package.marker) |
| 260 | + # create dependency from locked package to retain dependency metadata |
| 261 | + # if this is not done, we can end-up with incorrect nested dependencies |
| 262 | + constraint = requirement.constraint |
| 263 | + marker = requirement.marker |
| 264 | + extras = requirement.extras |
| 265 | + requirement = locked_package.to_dependency() |
| 266 | + requirement.marker = requirement.marker.intersect(marker) |
257 | 267 |
|
258 | | - if key not in nested_dependencies: |
259 | | - next_level_dependencies.append(require) |
| 268 | + requirement.set_constraint(constraint) |
260 | 269 |
|
261 | | - if requirement.name in project_level_dependencies and level == 0: |
262 | | - # project level dependencies take precedence |
263 | | - continue |
| 270 | + for require in locked_package.requires: |
| 271 | + if require.in_extras and extras.isdisjoint(require.in_extras): |
| 272 | + continue |
264 | 273 |
|
265 | | - if not locked_package: |
266 | | - # we make a copy to avoid any side-effects |
267 | | - requirement = deepcopy(requirement) |
| 274 | + require = deepcopy(require) |
| 275 | + require.marker = require.marker.intersect( |
| 276 | + requirement.marker.without_extras() |
| 277 | + ) |
| 278 | + if not require.marker.is_empty(): |
| 279 | + dependencies.append(require) |
268 | 280 |
|
| 281 | + key = locked_package |
269 | 282 | if key not in nested_dependencies: |
270 | 283 | nested_dependencies[key] = requirement |
271 | 284 | else: |
272 | 285 | nested_dependencies[key].marker = nested_dependencies[key].marker.union( |
273 | 286 | requirement.marker |
274 | 287 | ) |
275 | 288 |
|
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 | | - ) |
| 289 | + return nested_dependencies |
284 | 290 |
|
285 | 291 | @classmethod |
286 | 292 | def get_project_dependencies( |
287 | 293 | cls, |
288 | 294 | project_requires: list[Dependency], |
289 | 295 | locked_packages: list[Package], |
290 | | - pinned_versions: bool = False, |
291 | | - with_nested: bool = False, |
292 | | - ) -> Iterable[Dependency]: |
| 296 | + ) -> Iterable[tuple[Package, Dependency]]: |
293 | 297 | # group packages entries by name, this is required because requirement might use |
294 | | - # different constraints |
| 298 | + # different constraints. |
295 | 299 | packages_by_name: dict[str, list[Package]] = {} |
296 | 300 | for pkg in locked_packages: |
297 | 301 | if pkg.name not in packages_by_name: |
298 | 302 | packages_by_name[pkg.name] = [] |
299 | 303 | packages_by_name[pkg.name].append(pkg) |
300 | 304 |
|
301 | | - project_level_dependencies = set() |
302 | | - dependencies = [] |
303 | | - |
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 |
| 305 | + # Put higher versions first so that we prefer them. |
| 306 | + for packages in packages_by_name.values(): |
| 307 | + packages.sort(key=lambda package: package.version, reverse=True) |
324 | 308 |
|
325 | | - nested_dependencies = cls.__walk_dependency_level( |
326 | | - dependencies=dependencies, |
327 | | - level=0, |
328 | | - pinned_versions=pinned_versions, |
| 309 | + nested_dependencies = cls.__walk_dependencies( |
| 310 | + dependencies=project_requires, |
329 | 311 | packages_by_name=packages_by_name, |
330 | | - project_level_dependencies=project_level_dependencies, |
331 | | - nested_dependencies={}, |
332 | 312 | ) |
333 | 313 |
|
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()) |
| 314 | + return nested_dependencies.items() |
345 | 315 |
|
346 | 316 | def get_project_dependency_packages( |
347 | 317 | self, |
348 | 318 | project_requires: list[Dependency], |
| 319 | + project_python_marker: VersionConstraint | None = None, |
349 | 320 | dev: bool = False, |
350 | 321 | extras: bool | Sequence[str] | None = None, |
351 | 322 | ) -> Iterator[DependencyPackage]: |
| 323 | + # Apply the project python marker to all requirements. |
| 324 | + if project_python_marker is not None: |
| 325 | + marked_requires: list[Dependency] = [] |
| 326 | + for require in project_requires: |
| 327 | + require = deepcopy(require) |
| 328 | + require.marker = require.marker.intersect(project_python_marker) |
| 329 | + marked_requires.append(require) |
| 330 | + project_requires = marked_requires |
| 331 | + |
352 | 332 | repository = self.locked_repository(with_dev_reqs=dev) |
353 | 333 |
|
354 | 334 | # Build a set of all packages required by our selected extras |
@@ -379,16 +359,10 @@ def get_project_dependency_packages( |
379 | 359 |
|
380 | 360 | selected.append(dependency) |
381 | 361 |
|
382 | | - for dependency in self.get_project_dependencies( |
| 362 | + for package, dependency in self.get_project_dependencies( |
383 | 363 | project_requires=selected, |
384 | 364 | locked_packages=repository.packages, |
385 | | - with_nested=True, |
386 | 365 | ): |
387 | | - try: |
388 | | - package = repository.find_packages(dependency=dependency)[0] |
389 | | - except IndexError: |
390 | | - continue |
391 | | - |
392 | 366 | for extra in dependency.extras: |
393 | 367 | package.requires_extras.append(extra) |
394 | 368 |
|
|
0 commit comments