0
# Dependency Resolution
1
2
Advanced dependency resolution supporting both ResolveLib and UV backends with conflict resolution, constraint handling, and performance optimization. PDM's resolver system handles complex dependency graphs and provides multiple resolution strategies.
3
4
## Capabilities
5
6
### Base Resolver Interface
7
8
Abstract base class defining the resolver interface for dependency resolution systems.
9
10
```python { .api }
11
from abc import ABC, abstractmethod
12
from typing import Dict, List, Set
13
14
class Resolver(ABC):
15
"""
16
Abstract base class for dependency resolvers.
17
18
Defines the interface for resolving package dependencies with
19
conflict resolution and constraint satisfaction.
20
"""
21
22
@abstractmethod
23
def resolve(
24
self,
25
requirements: list[Requirement],
26
groups: list[str] | None = None,
27
prefer_pinned: bool = True
28
) -> dict[str, Candidate]:
29
"""
30
Resolve dependencies to concrete package candidates.
31
32
Args:
33
requirements: List of requirement specifications to resolve
34
groups: Dependency groups to include in resolution
35
prefer_pinned: Prefer pinned versions from existing lockfile
36
37
Returns:
38
Dictionary mapping package names to resolved candidates
39
40
Raises:
41
ResolutionError: Dependency resolution failed
42
ResolutionImpossible: No valid resolution exists
43
"""
44
45
@abstractmethod
46
def get_resolution_context(self) -> ResolutionContext:
47
"""
48
Get current resolution context with state and constraints.
49
50
Returns:
51
Context object containing resolution state
52
"""
53
54
def validate_resolution(
55
self,
56
candidates: dict[str, Candidate]
57
) -> bool:
58
"""
59
Validate that resolved candidates form a consistent set.
60
61
Args:
62
candidates: Resolved candidate mapping
63
64
Returns:
65
True if resolution is valid and consistent
66
"""
67
```
68
69
### ResolveLib-based Resolver
70
71
Standard resolver implementation using the ResolveLib algorithm for robust dependency resolution.
72
73
```python { .api }
74
class RLResolver(Resolver):
75
"""
76
ResolveLib-based dependency resolver (default).
77
78
Provides comprehensive dependency resolution using the ResolveLib
79
algorithm with backtracking, conflict resolution, and constraint satisfaction.
80
"""
81
82
def __init__(
83
self,
84
repository: BaseRepository,
85
environment: BaseEnvironment,
86
allow_prereleases: bool = False,
87
strategy: str = "reuse-installed"
88
):
89
"""
90
Initialize ResolveLib resolver.
91
92
Args:
93
repository: Package repository for candidate discovery
94
environment: Target environment for resolution
95
allow_prereleases: Include pre-release versions
96
strategy: Resolution strategy ("reuse-installed", "eager", "conservative")
97
"""
98
99
def resolve(
100
self,
101
requirements: list[Requirement],
102
groups: list[str] | None = None,
103
prefer_pinned: bool = True
104
) -> dict[str, Candidate]:
105
"""
106
Resolve dependencies using ResolveLib algorithm.
107
108
Performs comprehensive dependency resolution with backtracking
109
and conflict resolution to find a consistent package set.
110
"""
111
112
def get_resolution_context(self) -> ResolutionContext:
113
"""Get ResolveLib resolution context"""
114
115
def set_allow_prereleases(self, allow: bool) -> None:
116
"""
117
Set whether to allow pre-release versions.
118
119
Args:
120
allow: True to include pre-release versions
121
"""
122
123
def set_resolution_strategy(self, strategy: str) -> None:
124
"""
125
Set resolution strategy.
126
127
Args:
128
strategy: Strategy name ("reuse-installed", "eager", "conservative")
129
"""
130
```
131
132
### UV-based Resolver
133
134
High-performance resolver implementation using UV for faster dependency resolution.
135
136
```python { .api }
137
class UvResolver(Resolver):
138
"""
139
UV-based dependency resolver for high performance.
140
141
Provides faster dependency resolution using UV's optimized
142
algorithms, particularly beneficial for large dependency sets.
143
"""
144
145
def __init__(
146
self,
147
repository: BaseRepository,
148
environment: BaseEnvironment,
149
allow_prereleases: bool = False
150
):
151
"""
152
Initialize UV resolver.
153
154
Args:
155
repository: Package repository for candidate discovery
156
environment: Target environment for resolution
157
allow_prereleases: Include pre-release versions
158
"""
159
160
def resolve(
161
self,
162
requirements: list[Requirement],
163
groups: list[str] | None = None,
164
prefer_pinned: bool = True
165
) -> dict[str, Candidate]:
166
"""
167
Resolve dependencies using UV algorithm.
168
169
Performs fast dependency resolution optimized for performance
170
while maintaining compatibility with standard resolution.
171
"""
172
173
def get_resolution_context(self) -> ResolutionContext:
174
"""Get UV resolution context"""
175
176
def is_available(self) -> bool:
177
"""
178
Check if UV resolver is available.
179
180
Returns:
181
True if UV is installed and functional
182
"""
183
```
184
185
### Resolution Context and State
186
187
Context management for resolution operations and state tracking.
188
189
```python { .api }
190
@dataclass
191
class ResolutionContext:
192
"""
193
Context for dependency resolution operations.
194
195
Tracks resolution state, constraints, and intermediate results
196
during the resolution process.
197
"""
198
199
requirements: list[Requirement]
200
candidates: dict[str, Candidate]
201
constraints: dict[str, Requirement]
202
overrides: dict[str, str]
203
204
@property
205
def resolved_count(self) -> int:
206
"""Number of resolved packages"""
207
208
@property
209
def constraint_count(self) -> int:
210
"""Number of active constraints"""
211
212
def add_constraint(self, name: str, requirement: Requirement) -> None:
213
"""
214
Add constraint for package resolution.
215
216
Args:
217
name: Package name
218
requirement: Constraint requirement
219
"""
220
221
def get_conflicts(self) -> list[tuple[str, list[Requirement]]]:
222
"""
223
Get current resolution conflicts.
224
225
Returns:
226
List of conflicts with package names and conflicting requirements
227
"""
228
229
class ResolutionProvider:
230
"""
231
Provider interface for resolution backend integration.
232
233
Handles candidate discovery, metadata retrieval, and constraint
234
evaluation for the resolution algorithm.
235
"""
236
237
def find_candidates(
238
self,
239
identifier: str,
240
requirement: Requirement
241
) -> list[Candidate]:
242
"""
243
Find candidates matching requirement.
244
245
Args:
246
identifier: Package identifier
247
requirement: Requirement specification
248
249
Returns:
250
List of matching candidates
251
"""
252
253
def get_dependencies(self, candidate: Candidate) -> list[Requirement]:
254
"""
255
Get dependencies for a candidate.
256
257
Args:
258
candidate: Package candidate
259
260
Returns:
261
List of dependency requirements
262
"""
263
```
264
265
### Resolution Strategies
266
267
Different resolution strategies for handling version selection and conflict resolution.
268
269
```python { .api }
270
class ResolutionStrategy:
271
"""Base class for resolution strategies"""
272
273
def select_candidate(
274
self,
275
candidates: list[Candidate],
276
requirement: Requirement
277
) -> Candidate:
278
"""
279
Select best candidate from options.
280
281
Args:
282
candidates: Available candidates
283
requirement: Requirement being resolved
284
285
Returns:
286
Selected candidate
287
"""
288
289
class EagerStrategy(ResolutionStrategy):
290
"""
291
Eager resolution strategy selecting newest compatible versions.
292
293
Prefers the latest versions that satisfy requirements,
294
potentially leading to more frequent updates.
295
"""
296
297
class ConservativeStrategy(ResolutionStrategy):
298
"""
299
Conservative resolution strategy preferring minimal changes.
300
301
Minimizes version changes from existing installations,
302
providing stability at the cost of potentially older versions.
303
"""
304
305
class ReuseInstalledStrategy(ResolutionStrategy):
306
"""
307
Strategy that reuses installed packages when possible.
308
309
Prefers already installed versions if they satisfy requirements,
310
reducing installation time and maintaining stability.
311
"""
312
```
313
314
### Resolution Graph
315
316
Dependency graph representation and analysis for resolution results.
317
318
```python { .api }
319
class ResolutionGraph:
320
"""
321
Dependency graph representation for resolved packages.
322
323
Provides graph analysis capabilities including dependency
324
traversal, conflict detection, and visualization.
325
"""
326
327
def __init__(self, candidates: dict[str, Candidate]):
328
"""
329
Initialize resolution graph.
330
331
Args:
332
candidates: Resolved candidate mapping
333
"""
334
335
def get_dependencies(self, package: str) -> list[str]:
336
"""
337
Get direct dependencies of a package.
338
339
Args:
340
package: Package name
341
342
Returns:
343
List of direct dependency package names
344
"""
345
346
def get_dependents(self, package: str) -> list[str]:
347
"""
348
Get packages that depend on the specified package.
349
350
Args:
351
package: Package name
352
353
Returns:
354
List of dependent package names
355
"""
356
357
def get_install_order(self) -> list[str]:
358
"""
359
Get topologically sorted installation order.
360
361
Returns:
362
List of package names in installation order
363
"""
364
365
def detect_cycles(self) -> list[list[str]]:
366
"""
367
Detect circular dependencies.
368
369
Returns:
370
List of dependency cycles (empty if no cycles)
371
"""
372
373
def format_tree(self, package: str | None = None) -> str:
374
"""
375
Format dependency tree as text.
376
377
Args:
378
package: Root package (default: all top-level packages)
379
380
Returns:
381
Formatted dependency tree string
382
"""
383
```
384
385
### Usage Examples
386
387
#### Basic Dependency Resolution
388
389
```python
390
from pdm.resolver import RLResolver, UvResolver
391
from pdm.models.requirements import Requirement
392
from pdm.models.repositories import PyPIRepository
393
from pdm.environments import PythonEnvironment
394
395
# Setup resolver components
396
repo = PyPIRepository()
397
env = PythonEnvironment("/usr/bin/python3.9")
398
399
# Create resolver (ResolveLib-based)
400
resolver = RLResolver(
401
repository=repo,
402
environment=env,
403
allow_prereleases=False,
404
strategy="reuse-installed"
405
)
406
407
# Define requirements
408
requirements = [
409
Requirement.from_string("requests>=2.25.0"),
410
Requirement.from_string("click>=8.0.0"),
411
Requirement.from_string("rich")
412
]
413
414
# Resolve dependencies
415
try:
416
candidates = resolver.resolve(requirements)
417
print(f"Resolved {len(candidates)} packages:")
418
for name, candidate in candidates.items():
419
print(f" {name} {candidate.version}")
420
except ResolutionError as e:
421
print(f"Resolution failed: {e}")
422
```
423
424
#### UV Resolver for Performance
425
426
```python
427
from pdm.resolver import UvResolver
428
429
# Try UV resolver for better performance
430
uv_resolver = UvResolver(
431
repository=repo,
432
environment=env,
433
allow_prereleases=False
434
)
435
436
# Check if UV is available
437
if uv_resolver.is_available():
438
print("Using UV resolver for faster resolution")
439
candidates = uv_resolver.resolve(requirements)
440
else:
441
print("UV not available, falling back to ResolveLib")
442
candidates = resolver.resolve(requirements)
443
```
444
445
#### Resolution with Groups and Constraints
446
447
```python
448
from pdm.resolver import RLResolver
449
from pdm.models.requirements import Requirement
450
451
resolver = RLResolver(repo, env)
452
453
# Requirements with dependency groups
454
requirements = [
455
Requirement.from_string("django>=4.0"),
456
Requirement.from_string("pytest>=6.0"), # dev group
457
Requirement.from_string("sphinx>=4.0") # docs group
458
]
459
460
# Resolve specific groups
461
candidates = resolver.resolve(
462
requirements=requirements,
463
groups=["default", "dev"], # exclude docs
464
prefer_pinned=True
465
)
466
467
# Get resolution context for analysis
468
context = resolver.get_resolution_context()
469
print(f"Resolved {context.resolved_count} packages")
470
print(f"Active constraints: {context.constraint_count}")
471
472
# Check for conflicts
473
conflicts = context.get_conflicts()
474
if conflicts:
475
print("Resolution conflicts found:")
476
for name, conflicting_reqs in conflicts:
477
print(f" {name}: {conflicting_reqs}")
478
```
479
480
#### Resolution Graph Analysis
481
482
```python
483
from pdm.resolver.graph import ResolutionGraph
484
485
# Create resolution graph from candidates
486
graph = ResolutionGraph(candidates)
487
488
# Analyze dependencies
489
print("Dependency analysis:")
490
for package in candidates:
491
deps = graph.get_dependencies(package)
492
dependents = graph.get_dependents(package)
493
print(f"{package}:")
494
print(f" Dependencies: {deps}")
495
print(f" Dependents: {dependents}")
496
497
# Get installation order
498
install_order = graph.get_install_order()
499
print(f"Installation order: {install_order}")
500
501
# Check for circular dependencies
502
cycles = graph.detect_cycles()
503
if cycles:
504
print("Circular dependencies detected:")
505
for cycle in cycles:
506
print(f" {' -> '.join(cycle + [cycle[0]])}")
507
508
# Display dependency tree
509
tree = graph.format_tree()
510
print("Dependency tree:")
511
print(tree)
512
```
513
514
#### Custom Resolution Strategy
515
516
```python
517
from pdm.resolver import RLResolver, ResolutionStrategy
518
from pdm.models.candidates import Candidate
519
from pdm.models.requirements import Requirement
520
521
class PreferStableStrategy(ResolutionStrategy):
522
"""Custom strategy preferring stable releases"""
523
524
def select_candidate(
525
self,
526
candidates: list[Candidate],
527
requirement: Requirement
528
) -> Candidate:
529
# Filter out pre-releases first
530
stable_candidates = [
531
c for c in candidates
532
if not c.version.is_prerelease
533
]
534
535
if stable_candidates:
536
# Return newest stable version
537
return max(stable_candidates, key=lambda c: c.version)
538
else:
539
# Fall back to newest if no stable versions
540
return max(candidates, key=lambda c: c.version)
541
542
# Use custom strategy
543
resolver = RLResolver(repo, env)
544
resolver.strategy = PreferStableStrategy()
545
546
candidates = resolver.resolve(requirements)
547
```
548
549
#### Parallel Resolution
550
551
```python
552
import asyncio
553
from pdm.resolver import RLResolver, UvResolver
554
555
async def resolve_parallel(
556
requirements_list: list[list[Requirement]]
557
) -> list[dict[str, Candidate]]:
558
"""Resolve multiple requirement sets in parallel"""
559
560
resolvers = [
561
RLResolver(repo, env) for _ in requirements_list
562
]
563
564
async def resolve_single(resolver, requirements):
565
return resolver.resolve(requirements)
566
567
# Resolve all requirement sets concurrently
568
tasks = [
569
resolve_single(resolver, reqs)
570
for resolver, reqs in zip(resolvers, requirements_list)
571
]
572
573
return await asyncio.gather(*tasks)
574
575
# Usage
576
requirement_sets = [
577
[Requirement.from_string("django>=4.0")],
578
[Requirement.from_string("flask>=2.0")],
579
[Requirement.from_string("fastapi>=0.70")]
580
]
581
582
results = asyncio.run(resolve_parallel(requirement_sets))
583
for i, candidates in enumerate(results):
584
print(f"Set {i+1}: {len(candidates)} packages resolved")
585
```