A modern Python package and dependency manager supporting the latest PEP standards
—
Extensible command-line interface supporting custom commands, argument parsing, plugin integration, and comprehensive action implementations for PDM's command system.
Foundation classes for implementing PDM CLI commands with consistent argument parsing and execution patterns.
from argparse import ArgumentParser, Namespace, _SubParsersAction
from typing import Any, Sequence
class BaseCommand:
"""
Base class for all CLI commands providing consistent interface
and integration with PDM's command system.
"""
# Class attributes
name: str | None = None
description: str | None = None
arguments: Sequence[Option] = (verbose_option, global_option, project_option)
@classmethod
def register_to(cls, subparsers: _SubParsersAction, name: str | None = None, **kwargs: Any) -> None:
"""
Register command to argument parser subcommands.
Args:
subparsers: Subparser for command registration
name: Optional command name override
**kwargs: Additional arguments for add_parser
"""
def add_arguments(self, parser: ArgumentParser) -> None:
"""
Add command-specific arguments to parser.
Args:
parser: Argument parser for this command
"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Execute command with parsed options.
Args:
project: PDM project instance
options: Parsed command line options
Raises:
PdmUsageError: Command usage errors
PdmException: Command execution errors
"""Comprehensive set of built-in commands covering all PDM functionality.
class AddCommand(BaseCommand):
"""Add dependencies to project"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Add dependencies with version constraints and groups.
Options:
- packages: Package specifications to add
- group: Dependency group name
- dev: Add to development dependencies
- editable: Install in editable mode
- no-sync: Don't sync after adding
"""
class RemoveCommand(BaseCommand):
"""Remove dependencies from project"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Remove specified packages from project.
Options:
- packages: Package names to remove
- group: Dependency group name
- dev: Remove from development dependencies
- no-sync: Don't sync after removing
"""
class UpdateCommand(BaseCommand):
"""Update project dependencies"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Update dependencies to latest compatible versions.
Options:
- packages: Specific packages to update (default: all)
- group: Update specific dependency group
- top: Only update top-level dependencies
- dry-run: Show what would be updated
"""class InitCommand(BaseCommand):
"""Initialize new PDM project"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Initialize project with pyproject.toml and configuration.
Options:
- name: Project name
- version: Initial version
- author: Author information
- license: License specification
- python: Python version requirement
"""
class InstallCommand(BaseCommand):
"""Install project dependencies"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Install all project dependencies from lockfile or requirements.
Options:
- group: Install specific dependency group
- production: Skip development dependencies
- dry-run: Show what would be installed
- no-lock: Don't update lockfile
"""
class SyncCommand(BaseCommand):
"""Synchronize environment with lockfile"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Sync environment to exactly match lockfile specifications.
Options:
- group: Sync specific dependency group
- production: Only production dependencies
- clean: Remove packages not in lockfile
- dry-run: Show sync operations
"""class BuildCommand(BaseCommand):
"""Build distribution packages"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Build wheel and/or source distributions.
Options:
- dest: Output directory for distributions
- no-sdist: Skip source distribution
- no-wheel: Skip wheel distribution
- config-settings: Build configuration overrides
"""
class PublishCommand(BaseCommand):
"""Publish packages to repository"""
def handle(self, project: Project, options: Namespace) -> None:
"""
Upload distributions to package repository.
Options:
- repository: Repository URL or name
- username: Authentication username
- password: Authentication password
- comment: Upload comment
- sign: Sign uploads with GPG
"""Core implementation functions that handle the actual command logic.
def do_add(
project: Project,
requirements: list[str],
group: str = "default",
dev: bool = False,
editable: bool = False,
sync: bool = True,
**kwargs
) -> dict:
"""
Add dependencies to project.
Args:
project: Project instance
requirements: List of requirement specifications
group: Dependency group name
dev: Add to development dependencies (deprecated)
editable: Install packages in editable mode
sync: Synchronize environment after adding
Returns:
Dictionary with operation results and statistics
"""
def do_install(
project: Project,
groups: list[str] | None = None,
production: bool = False,
check: bool = False,
**kwargs
) -> None:
"""
Install project dependencies.
Args:
project: Project instance
groups: Specific dependency groups to install
production: Skip development dependencies
check: Verify installation without installing
"""
def do_lock(
project: Project,
groups: list[str] | None = None,
refresh: bool = False,
**kwargs
) -> dict:
"""
Generate project lockfile.
Args:
project: Project instance
groups: Dependency groups to include
refresh: Refresh all cached data
Returns:
Dictionary with lock operation results
"""
def do_sync(
project: Project,
groups: list[str] | None = None,
production: bool = False,
clean: bool = False,
**kwargs
) -> None:
"""
Synchronize environment with lockfile.
Args:
project: Project instance
groups: Dependency groups to sync
production: Only production dependencies
clean: Remove packages not in lockfile
"""
def do_update(
project: Project,
requirements: list[str] | None = None,
groups: list[str] | None = None,
top: bool = False,
**kwargs
) -> dict:
"""
Update project dependencies.
Args:
project: Project instance
requirements: Specific packages to update
groups: Dependency groups to update
top: Only update top-level dependencies
Returns:
Dictionary with update results
"""
def do_remove(
project: Project,
requirements: list[str],
group: str = "default",
dev: bool = False,
sync: bool = True,
**kwargs
) -> dict:
"""
Remove dependencies from project.
Args:
project: Project instance
requirements: Package names to remove
group: Dependency group name
dev: Remove from development dependencies (deprecated)
sync: Synchronize environment after removal
Returns:
Dictionary with removal results
"""Enhanced argument parsing with PDM-specific extensions and error handling.
class ArgumentParser:
"""
Enhanced argument parser with PDM-specific functionality.
Provides better error messages, command suggestions, and
integration with PDM's help system.
"""
def add_subparsers(self, **kwargs):
"""Add subparser for commands"""
def parse_args(self, args: list[str] | None = None) -> Namespace:
"""Parse command line arguments with error handling"""
class ErrorArgumentParser(ArgumentParser):
"""
Argument parser that raises PdmArgumentError instead of exiting.
Used for testing and programmatic command invocation.
"""
def error(self, message: str) -> None:
"""Raise PdmArgumentError instead of system exit"""
def format_similar_command(command: str, available: list[str]) -> str:
"""
Format suggestion for similar commands when command not found.
Args:
command: Command that was not found
available: List of available commands
Returns:
Formatted suggestion message
"""from pdm.cli.commands.base import BaseCommand
from pdm.core import Core
from argparse import ArgumentParser, Namespace
class MyCustomCommand(BaseCommand):
"""Custom command example"""
@property
def name(self) -> str:
return "mycmd"
@property
def description(self) -> str:
return "My custom PDM command"
def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument(
"--my-option",
help="Custom option for my command"
)
parser.add_argument(
"target",
help="Target for command operation"
)
def handle(self, project, options: Namespace) -> None:
print(f"Executing custom command on {options.target}")
if options.my_option:
print(f"Option value: {options.my_option}")
# Register command via plugin
def my_plugin(core: Core) -> None:
core.register_command(MyCustomCommand(), "mycmd")from pdm.project import Project
from pdm.cli.actions import do_add, do_install, do_lock, do_sync
# Load project
project = Project()
# Add dependencies programmatically
result = do_add(
project,
requirements=["requests>=2.25.0", "click>=8.0"],
group="default",
sync=False # Don't sync yet
)
print(f"Added {len(result['added'])} packages")
# Add dev dependencies
do_add(
project,
requirements=["pytest", "black", "mypy"],
group="dev",
sync=False
)
# Generate lockfile
lock_result = do_lock(project)
print(f"Locked {len(lock_result['candidates'])} packages")
# Install all dependencies
do_install(project, production=False)
# Or sync to exact lockfile state
do_sync(project, clean=True)from pdm.cli.commands.base import BaseCommand
from argparse import ArgumentParser, Namespace
class ExampleCommand(BaseCommand):
"""Example showing comprehensive option handling"""
def add_arguments(self, parser: ArgumentParser) -> None:
# Boolean flags
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without executing"
)
# String options with choices
parser.add_argument(
"--level",
choices=["debug", "info", "warning", "error"],
default="info",
help="Logging level"
)
# Multiple values
parser.add_argument(
"--exclude",
action="append",
help="Packages to exclude (can be repeated)"
)
# Positional arguments
parser.add_argument(
"packages",
nargs="*",
help="Package names to process"
)
def handle(self, project, options: Namespace) -> None:
if options.dry_run:
print("DRY RUN - no changes will be made")
print(f"Log level: {options.level}")
if options.exclude:
print(f"Excluding: {', '.join(options.exclude)}")
for package in options.packages:
print(f"Processing package: {package}")Install with Tessl CLI
npx tessl i tessl/pypi-pdm