0
# CLI Framework
1
2
Extensible command-line interface supporting custom commands, argument parsing, plugin integration, and comprehensive action implementations for PDM's command system.
3
4
## Capabilities
5
6
### Base Command System
7
8
Foundation classes for implementing PDM CLI commands with consistent argument parsing and execution patterns.
9
10
```python { .api }
11
from argparse import ArgumentParser, Namespace, _SubParsersAction
12
from typing import Any, Sequence
13
14
class BaseCommand:
15
"""
16
Base class for all CLI commands providing consistent interface
17
and integration with PDM's command system.
18
"""
19
20
# Class attributes
21
name: str | None = None
22
description: str | None = None
23
arguments: Sequence[Option] = (verbose_option, global_option, project_option)
24
25
@classmethod
26
def register_to(cls, subparsers: _SubParsersAction, name: str | None = None, **kwargs: Any) -> None:
27
"""
28
Register command to argument parser subcommands.
29
30
Args:
31
subparsers: Subparser for command registration
32
name: Optional command name override
33
**kwargs: Additional arguments for add_parser
34
"""
35
36
def add_arguments(self, parser: ArgumentParser) -> None:
37
"""
38
Add command-specific arguments to parser.
39
40
Args:
41
parser: Argument parser for this command
42
"""
43
44
def handle(self, project: Project, options: Namespace) -> None:
45
"""
46
Execute command with parsed options.
47
48
Args:
49
project: PDM project instance
50
options: Parsed command line options
51
52
Raises:
53
PdmUsageError: Command usage errors
54
PdmException: Command execution errors
55
"""
56
```
57
58
### Core CLI Commands
59
60
Comprehensive set of built-in commands covering all PDM functionality.
61
62
#### Dependency Management Commands
63
64
```python { .api }
65
class AddCommand(BaseCommand):
66
"""Add dependencies to project"""
67
68
def handle(self, project: Project, options: Namespace) -> None:
69
"""
70
Add dependencies with version constraints and groups.
71
72
Options:
73
- packages: Package specifications to add
74
- group: Dependency group name
75
- dev: Add to development dependencies
76
- editable: Install in editable mode
77
- no-sync: Don't sync after adding
78
"""
79
80
class RemoveCommand(BaseCommand):
81
"""Remove dependencies from project"""
82
83
def handle(self, project: Project, options: Namespace) -> None:
84
"""
85
Remove specified packages from project.
86
87
Options:
88
- packages: Package names to remove
89
- group: Dependency group name
90
- dev: Remove from development dependencies
91
- no-sync: Don't sync after removing
92
"""
93
94
class UpdateCommand(BaseCommand):
95
"""Update project dependencies"""
96
97
def handle(self, project: Project, options: Namespace) -> None:
98
"""
99
Update dependencies to latest compatible versions.
100
101
Options:
102
- packages: Specific packages to update (default: all)
103
- group: Update specific dependency group
104
- top: Only update top-level dependencies
105
- dry-run: Show what would be updated
106
"""
107
```
108
109
#### Project Lifecycle Commands
110
111
```python { .api }
112
class InitCommand(BaseCommand):
113
"""Initialize new PDM project"""
114
115
def handle(self, project: Project, options: Namespace) -> None:
116
"""
117
Initialize project with pyproject.toml and configuration.
118
119
Options:
120
- name: Project name
121
- version: Initial version
122
- author: Author information
123
- license: License specification
124
- python: Python version requirement
125
"""
126
127
class InstallCommand(BaseCommand):
128
"""Install project dependencies"""
129
130
def handle(self, project: Project, options: Namespace) -> None:
131
"""
132
Install all project dependencies from lockfile or requirements.
133
134
Options:
135
- group: Install specific dependency group
136
- production: Skip development dependencies
137
- dry-run: Show what would be installed
138
- no-lock: Don't update lockfile
139
"""
140
141
class SyncCommand(BaseCommand):
142
"""Synchronize environment with lockfile"""
143
144
def handle(self, project: Project, options: Namespace) -> None:
145
"""
146
Sync environment to exactly match lockfile specifications.
147
148
Options:
149
- group: Sync specific dependency group
150
- production: Only production dependencies
151
- clean: Remove packages not in lockfile
152
- dry-run: Show sync operations
153
"""
154
```
155
156
#### Build and Publishing Commands
157
158
```python { .api }
159
class BuildCommand(BaseCommand):
160
"""Build distribution packages"""
161
162
def handle(self, project: Project, options: Namespace) -> None:
163
"""
164
Build wheel and/or source distributions.
165
166
Options:
167
- dest: Output directory for distributions
168
- no-sdist: Skip source distribution
169
- no-wheel: Skip wheel distribution
170
- config-settings: Build configuration overrides
171
"""
172
173
class PublishCommand(BaseCommand):
174
"""Publish packages to repository"""
175
176
def handle(self, project: Project, options: Namespace) -> None:
177
"""
178
Upload distributions to package repository.
179
180
Options:
181
- repository: Repository URL or name
182
- username: Authentication username
183
- password: Authentication password
184
- comment: Upload comment
185
- sign: Sign uploads with GPG
186
"""
187
```
188
189
### CLI Actions
190
191
Core implementation functions that handle the actual command logic.
192
193
```python { .api }
194
def do_add(
195
project: Project,
196
requirements: list[str],
197
group: str = "default",
198
dev: bool = False,
199
editable: bool = False,
200
sync: bool = True,
201
**kwargs
202
) -> dict:
203
"""
204
Add dependencies to project.
205
206
Args:
207
project: Project instance
208
requirements: List of requirement specifications
209
group: Dependency group name
210
dev: Add to development dependencies (deprecated)
211
editable: Install packages in editable mode
212
sync: Synchronize environment after adding
213
214
Returns:
215
Dictionary with operation results and statistics
216
"""
217
218
def do_install(
219
project: Project,
220
groups: list[str] | None = None,
221
production: bool = False,
222
check: bool = False,
223
**kwargs
224
) -> None:
225
"""
226
Install project dependencies.
227
228
Args:
229
project: Project instance
230
groups: Specific dependency groups to install
231
production: Skip development dependencies
232
check: Verify installation without installing
233
"""
234
235
def do_lock(
236
project: Project,
237
groups: list[str] | None = None,
238
refresh: bool = False,
239
**kwargs
240
) -> dict:
241
"""
242
Generate project lockfile.
243
244
Args:
245
project: Project instance
246
groups: Dependency groups to include
247
refresh: Refresh all cached data
248
249
Returns:
250
Dictionary with lock operation results
251
"""
252
253
def do_sync(
254
project: Project,
255
groups: list[str] | None = None,
256
production: bool = False,
257
clean: bool = False,
258
**kwargs
259
) -> None:
260
"""
261
Synchronize environment with lockfile.
262
263
Args:
264
project: Project instance
265
groups: Dependency groups to sync
266
production: Only production dependencies
267
clean: Remove packages not in lockfile
268
"""
269
270
def do_update(
271
project: Project,
272
requirements: list[str] | None = None,
273
groups: list[str] | None = None,
274
top: bool = False,
275
**kwargs
276
) -> dict:
277
"""
278
Update project dependencies.
279
280
Args:
281
project: Project instance
282
requirements: Specific packages to update
283
groups: Dependency groups to update
284
top: Only update top-level dependencies
285
286
Returns:
287
Dictionary with update results
288
"""
289
290
def do_remove(
291
project: Project,
292
requirements: list[str],
293
group: str = "default",
294
dev: bool = False,
295
sync: bool = True,
296
**kwargs
297
) -> dict:
298
"""
299
Remove dependencies from project.
300
301
Args:
302
project: Project instance
303
requirements: Package names to remove
304
group: Dependency group name
305
dev: Remove from development dependencies (deprecated)
306
sync: Synchronize environment after removal
307
308
Returns:
309
Dictionary with removal results
310
"""
311
```
312
313
### Argument Parsing Utilities
314
315
Enhanced argument parsing with PDM-specific extensions and error handling.
316
317
```python { .api }
318
class ArgumentParser:
319
"""
320
Enhanced argument parser with PDM-specific functionality.
321
322
Provides better error messages, command suggestions, and
323
integration with PDM's help system.
324
"""
325
326
def add_subparsers(self, **kwargs):
327
"""Add subparser for commands"""
328
329
def parse_args(self, args: list[str] | None = None) -> Namespace:
330
"""Parse command line arguments with error handling"""
331
332
class ErrorArgumentParser(ArgumentParser):
333
"""
334
Argument parser that raises PdmArgumentError instead of exiting.
335
336
Used for testing and programmatic command invocation.
337
"""
338
339
def error(self, message: str) -> None:
340
"""Raise PdmArgumentError instead of system exit"""
341
342
def format_similar_command(command: str, available: list[str]) -> str:
343
"""
344
Format suggestion for similar commands when command not found.
345
346
Args:
347
command: Command that was not found
348
available: List of available commands
349
350
Returns:
351
Formatted suggestion message
352
"""
353
```
354
355
### Usage Examples
356
357
#### Creating Custom Commands
358
359
```python
360
from pdm.cli.commands.base import BaseCommand
361
from pdm.core import Core
362
from argparse import ArgumentParser, Namespace
363
364
class MyCustomCommand(BaseCommand):
365
"""Custom command example"""
366
367
@property
368
def name(self) -> str:
369
return "mycmd"
370
371
@property
372
def description(self) -> str:
373
return "My custom PDM command"
374
375
def add_arguments(self, parser: ArgumentParser) -> None:
376
parser.add_argument(
377
"--my-option",
378
help="Custom option for my command"
379
)
380
parser.add_argument(
381
"target",
382
help="Target for command operation"
383
)
384
385
def handle(self, project, options: Namespace) -> None:
386
print(f"Executing custom command on {options.target}")
387
if options.my_option:
388
print(f"Option value: {options.my_option}")
389
390
# Register command via plugin
391
def my_plugin(core: Core) -> None:
392
core.register_command(MyCustomCommand(), "mycmd")
393
```
394
395
#### Using CLI Actions Programmatically
396
397
```python
398
from pdm.project import Project
399
from pdm.cli.actions import do_add, do_install, do_lock, do_sync
400
401
# Load project
402
project = Project()
403
404
# Add dependencies programmatically
405
result = do_add(
406
project,
407
requirements=["requests>=2.25.0", "click>=8.0"],
408
group="default",
409
sync=False # Don't sync yet
410
)
411
print(f"Added {len(result['added'])} packages")
412
413
# Add dev dependencies
414
do_add(
415
project,
416
requirements=["pytest", "black", "mypy"],
417
group="dev",
418
sync=False
419
)
420
421
# Generate lockfile
422
lock_result = do_lock(project)
423
print(f"Locked {len(lock_result['candidates'])} packages")
424
425
# Install all dependencies
426
do_install(project, production=False)
427
428
# Or sync to exact lockfile state
429
do_sync(project, clean=True)
430
```
431
432
#### Command Option Handling
433
434
```python
435
from pdm.cli.commands.base import BaseCommand
436
from argparse import ArgumentParser, Namespace
437
438
class ExampleCommand(BaseCommand):
439
"""Example showing comprehensive option handling"""
440
441
def add_arguments(self, parser: ArgumentParser) -> None:
442
# Boolean flags
443
parser.add_argument(
444
"--dry-run",
445
action="store_true",
446
help="Show what would be done without executing"
447
)
448
449
# String options with choices
450
parser.add_argument(
451
"--level",
452
choices=["debug", "info", "warning", "error"],
453
default="info",
454
help="Logging level"
455
)
456
457
# Multiple values
458
parser.add_argument(
459
"--exclude",
460
action="append",
461
help="Packages to exclude (can be repeated)"
462
)
463
464
# Positional arguments
465
parser.add_argument(
466
"packages",
467
nargs="*",
468
help="Package names to process"
469
)
470
471
def handle(self, project, options: Namespace) -> None:
472
if options.dry_run:
473
print("DRY RUN - no changes will be made")
474
475
print(f"Log level: {options.level}")
476
477
if options.exclude:
478
print(f"Excluding: {', '.join(options.exclude)}")
479
480
for package in options.packages:
481
print(f"Processing package: {package}")
482
```