Skip to main content

Extension module for argparse

Project description

argparse-boost

Build Status Codecov PyPI - Version PyPI - Python Version PyPI - Downloads

argparse-boost is a library for building CLI applications with automatic argument parsing from dataclasses and environment variable support.

Installation

pip install argparse-boost

How to use

Quick Start: Multi-Command CLI

Build multi-command CLIs (like git, docker, kubectl) with automatic command discovery:

Project structure:

myapp/
└── cli/
    ├── __main__.py
    └── greet.py

myapp/cli/main.py:

from argparse_boost import Config, setup_cli
from myapp import cli


def main() -> None:
    config = Config(
        app_name="myapp",
        env_prefix="MYAPP_",
    )
    setup_cli(
        config=config,
        description="My CLI application",
        commands_package=cli,
    )


if __name__ == "__main__":
    main()

myapp/cli/greet.py:

from dataclasses import dataclass


@dataclass(kw_only=True)
class GreetConfig:
    name: str
    greeting: str = "Hello"


def main(args: GreetConfig) -> None:
    """Greet someone with a custom message."""
    print(f"{args.greeting}, {args.name}!")

Usage:

python -m myapp.cli greet --name World --greeting Hi
# Output: Hi, World!

python -m myapp.cli greet --name Alice
# Output: Hello, Alice!

python -m myapp.cli --help
# Shows available commands

python -m myapp.cli greet --help
# Shows arguments for greet command

The library automatically:

  • Discovers commands in the cli/ directory
  • Parses arguments from the dataclass type hint in main()
  • Generates help text from dataclass fields
  • Supports environment variables with the specified prefix

Writing Commands with Dataclass Auto-Parsing

The easiest way to write a command is to use a dataclass type hint in the main() function. Arguments are automatically parsed from the dataclass fields:

# myapp/cli/deploy.py
from dataclasses import dataclass, field
from typing import Annotated
from argparse_boost import Help


@dataclass(kw_only=True)
class DeployConfig:
    environment: Annotated[str, Help("Target environment (dev/staging/prod)")]
    version: Annotated[str, Help("Version to deploy")]
    dry_run: Annotated[bool, Help("Simulate deployment without making changes")] = False
    services: Annotated[list[str], Help("Services to deploy")] = field(
        default_factory=list
    )


def main(args: DeployConfig) -> None:
    """Deploy application to the specified environment."""
    if args.dry_run:
        print(f"[DRY RUN] Would deploy {args.version} to {args.environment}")
    else:
        print(f"Deploying {args.version} to {args.environment}...")

    if args.services:
        print(f"Services: {', '.join(args.services)}")

Usage:

python -m myapp.cli deploy --environment prod --version 1.2.3
python -m myapp.cli deploy --environment staging --version 1.2.4 --dry-run true
python -m myapp.cli deploy --environment dev --version 1.2.5 --services "api,worker"

This approach gives you:

  • Type-safe argument parsing
  • Automatic validation (required fields, type checking)
  • Auto-generated help text with descriptions
  • Support for complex types (lists, dicts, nested dataclasses)

Writing Commands with Manual Parser Setup

For more control over argument parsing, use setup_parser() and work with argparse.Namespace:

import argparse


def setup_parser(parser: argparse.ArgumentParser) -> None:
    """Configure arguments for this command."""
    parser.add_argument(
        "--environment",
        required=True,
        choices=["dev", "staging", "prod"],
        help="Target environment",
    )
    parser.add_argument(
        "--version",
        required=True,
        help="Version to deploy",
    )
    parser.add_argument(
        "--force",
        action="store_true",
        help="Force deployment even if validation fails",
    )


def main(args: argparse.Namespace) -> None:
    """Deploy application to the specified environment."""
    print(f"Deploying {args.version} to {args.environment}")
    if args.force:
        print("Force mode enabled - skipping validation")

This approach is useful when you need:

  • Custom argument behavior (choices, actions, mutual exclusivity)
  • Integration with existing argparse code
  • Arguments that don't map cleanly to dataclass fields

Async command support: Commands can also be async - the library automatically handles them with asyncio.run():

import asyncio


async def main() -> None:
    """Async command example."""
    await asyncio.sleep(1)

Configuration Management (Without CLI)

You can use argparse-boost for configuration management in your project, completely separate from CLI applications. This is useful for web apps, services, or any Python project that needs type-safe configuration from environment variables:

from dataclasses import dataclass, field
from argparse_boost import construct_dataclass, Config


@dataclass(kw_only=True)
class DatabaseConfig:
    host: str
    port: int = 5432
    user: str = "postgres"
    password: str
    pool_size: int = 10


@dataclass(kw_only=True)
class AppConfig:
    debug: bool = False
    db: DatabaseConfig = field(default_factory=DatabaseConfig)
    allowed_hosts: list[str] = field(default_factory=list)


# Load configuration from environment variables
app_config = construct_dataclass(
    AppConfig,
    config=Config(env_prefix="APP_"),
)

# Now use your type-safe config
print(f"Connecting to {app_config.db.host}:{app_config.db.port}")
print(f"Debug mode: {app_config.debug}")

Environment variables:

APP_DEBUG=true
APP_DB_HOST=localhost
APP_DB_PORT=5433
APP_DB_PASSWORD=secret
APP_DB_POOL_SIZE=20
APP_ALLOWED_HOSTS=example.com,api.example.com

The configuration is loaded with full type conversion:

  • APP_DEBUG=trueconfig.debug = True
  • APP_DB_PORT=5433config.db.port = 5433
  • APP_ALLOWED_HOSTS=a,bconfig.allowed_hosts = ["a", "b"]

This approach gives you:

  • Type-safe configuration with dataclasses
  • Automatic type conversion from environment variables
  • Support for nested configuration (flattened to ENV vars)
  • No CLI overhead - just configuration management

Core Features

Environment Variable Support

Commands automatically read environment variables before parsing CLI arguments. CLI arguments always take priority over environment variables. The env_prefix specified in setup_main() is used for all commands.

Environment variable naming:

  • CLI option --host → ENV var MYAPP_HOST (with prefix from setup_main)
  • CLI option --port → ENV var MYAPP_PORT
  • Multi-word options: --log-levelMYAPP_LOG_LEVEL (dashes become underscores)

Example:

# myapp/cli/connect.py
from dataclasses import dataclass


@dataclass(kw_only=True)
class ConnectConfig:
    host: str
    port: int = 5432
    log_level: str = "INFO"


def main(args: ConnectConfig) -> None:
    """Connect to a server with logging."""
    print(f"Connecting to {args.host}:{args.port}")
    print(f"Log level: {args.log_level}")

Usage:

# Using environment variables only
MYAPP_HOST=localhost MYAPP_LOG_LEVEL=DEBUG python -m myapp.cli connect

# CLI overrides ENV
MYAPP_HOST=localhost python -m myapp.cli connect --host production.example.com --port 443
# Result: host='production.example.com', port=443

# Mix both
MYAPP_LOG_LEVEL=DEBUG python -m myapp.cli connect --host db.example.com
# Result: host='db.example.com', log_level='DEBUG'

Environment variables are displayed in the help output when they are currently set:

MYAPP_HOST=localhost python -m myapp.cli connect --help
# Shows: --host (env: MYAPP_HOST=localhost)

Nested Dataclasses

Nested dataclasses are automatically flattened into CLI arguments with intuitive naming:

# myapp/cli/migrate.py
from dataclasses import dataclass, field


@dataclass(kw_only=True)
class DatabaseConfig:
    host: str
    port: int = 5432
    use_ssl: bool = False


@dataclass(kw_only=True)
class MigrateConfig:
    migration_name: str
    db: DatabaseConfig = field(default_factory=lambda: DatabaseConfig(host="localhost"))


def main(args: MigrateConfig) -> None:
    """Run database migrations."""
    ssl_status = "with SSL" if args.db.use_ssl else "without SSL"
    print(f"Running migration '{args.migration_name}'")
    print(f"Connecting to {args.db.host}:{args.db.port} {ssl_status}")

Generated CLI arguments:

  • --migration-nameargs.migration_name
  • --db-hostargs.db.host
  • --db-portargs.db.port
  • --db-use-sslargs.db.use_ssl

Environment variables (with MYAPP prefix from setup_main):

  • MYAPP_MIGRATION_NAME
  • MYAPP_DB_HOST
  • MYAPP_DB_PORT
  • MYAPP_DB_USE_SSL

Usage:

# Using CLI arguments
python -m myapp.cli migrate --migration-name add_users --db-host postgres.local --db-port 5433

# Using environment variables
MYAPP_DB_HOST=postgres.local MYAPP_DB_USE_SSL=true python -m myapp.cli migrate --migration-name add_users

Custom Parsers

Use custom parsing functions for specialized types via Annotated:

# myapp/cli/analyze.py
from dataclasses import dataclass
from typing import Annotated
from argparse_boost import Parser


def parse_percentage(value: str) -> float:
    """Parse percentage string like '75%' to float 0.75"""
    if isinstance(value, float):
        return value
    return float(value.rstrip("%")) / 100


@dataclass(kw_only=True)
class AnalyzeConfig:
    dataset: str
    threshold: Annotated[float, Parser(parse_percentage)] = 0.5
    min_confidence: Annotated[float, Parser(parse_percentage)] = 0.8


def main(args: AnalyzeConfig) -> None:
    """Analyze dataset with custom threshold."""
    print(f"Analyzing {args.dataset}")
    print(f"Threshold: {args.threshold:.2%}")
    print(f"Min confidence: {args.min_confidence:.2%}")

Usage:

python -m myapp.cli analyze --dataset users.csv --threshold 75% --min-confidence 90%
# Output:
# Analyzing users.csv
# Threshold: 75.00%
# Min confidence: 90.00%

Custom parsers are useful for:

  • Parsing duration strings (e.g., "1h30m" → seconds)
  • Parsing file sizes (e.g., "10MB" → bytes)
  • Parsing percentages, ratios, or custom formats
  • Validating and transforming complex inputs

Help Text

Add descriptive help text to fields using Annotated with Help:

# myapp/cli/backup.py
from dataclasses import dataclass
from typing import Annotated
from argparse_boost import Help


@dataclass(kw_only=True)
class BackupConfig:
    source: Annotated[str, Help("Source directory to backup")]
    destination: Annotated[str, Help("Destination directory for backup")]
    timeout: Annotated[int, Help("Backup timeout in seconds")] = 3600
    retries: Annotated[int, Help("Number of retry attempts on failure")] = 3
    compress: Annotated[bool, Help("Compress backup files")] = True


def main(args: BackupConfig) -> None:
    """Create a backup of the source directory."""
    compression = "compressed" if args.compress else "uncompressed"
    print(f"Backing up {args.source} to {args.destination} ({compression})")
    print(f"Timeout: {args.timeout}s, Retries: {args.retries}")

View help:

python -m myapp.cli backup --help
# Shows:
#   --source             Source directory to backup (Required)
#   --destination        Destination directory for backup (Required)
#   --timeout            Backup timeout in seconds (Default: 3600)
#   --retries            Number of retry attempts on failure (Default: 3)
#   --compress           Compress backup files (Default: True)

Help text is automatically enhanced with default values and "Required" indicators for fields without defaults.

Advanced Types

Lists

Lists are parsed from comma-separated values:

# myapp/cli/tag.py
from dataclasses import dataclass, field


@dataclass(kw_only=True)
class TagConfig:
    resource: str
    tags: list[str] = field(default_factory=list)
    allowed_ports: list[int] = field(default_factory=list)


def main(args: TagConfig) -> None:
    """Tag a resource with metadata."""
    print(f"Tagging resource: {args.resource}")
    if args.tags:
        print(f"Tags: {', '.join(args.tags)}")
    if args.allowed_ports:
        print(f"Allowed ports: {', '.join(map(str, args.allowed_ports))}")

Usage:

# Using CLI arguments
python -m myapp.cli tag --resource server-01 --tags "web,api,prod" --allowed-ports "80,443,8080"
# Output:
# Tagging resource: server-01
# Tags: web, api, prod
# Allowed ports: 80, 443, 8080

# Using environment variables
MYAPP_RESOURCE=server-02 MYAPP_TAGS="database,backup" python -m myapp.cli tag

Dictionaries

Dictionaries are parsed from comma-separated key-value pairs using : or =:

# myapp/cli/configure.py
from dataclasses import dataclass, field


@dataclass(kw_only=True)
class ConfigureConfig:
    service: str
    limits: dict[str, int] = field(default_factory=dict)
    settings: dict[str, str] = field(default_factory=dict)


def main(args: ConfigureConfig) -> None:
    """Configure service with limits and settings."""
    print(f"Configuring {args.service}")
    if args.limits:
        for key, value in args.limits.items():
            print(f"  Limit {key}: {value}")
    if args.settings:
        for key, value in args.settings.items():
            print(f"  Setting {key}: {value}")

Usage:

# Using colon separator
python -m myapp.cli configure --service api --limits "daily:100,monthly:3000"

# Using equals separator
python -m myapp.cli configure --service worker --settings "timeout=30,retries=3"

Optional Types

Optional fields are supported via T | None or Optional[T]:

# myapp/cli/process.py
from dataclasses import dataclass


@dataclass(kw_only=True)
class ProcessConfig:
    input_file: str
    output_file: str | None = None
    max_retries: int | None = 3
    format: str | None = None


def main(args: ProcessConfig) -> None:
    """Process a file with optional parameters."""
    print(f"Processing {args.input_file}")
    if args.output_file:
        print(f"Output to: {args.output_file}")
    else:
        print("Output to: stdout")

    print(f"Max retries: {args.max_retries}")
    if args.format:
        print(f"Format: {args.format}")

Usage:

# Only required field
python -m myapp.cli process --input-file data.csv
# Output: stdout, max_retries: 3, format: None

# With optional fields
python -m myapp.cli process --input-file data.csv --output-file result.json --format json

Programmatic Usage

For more control, use the low-level API:

from dataclasses import dataclass
from argparse_boost import (
    BoostedArgumentParser,
    construct_dataclass,
    Config,
    dict_from_args,
)


@dataclass(kw_only=True)
class Settings:
    host: str
    port: int = 5432


# Option 1: Simple approach - automatic ENV loading
settings = construct_dataclass(
    Settings,
    config=Config(env_prefix="APP_"),
)

# Option 2: Manual approach with BoostedArgumentParser
parser = BoostedArgumentParser(prog="myapp", env_prefix="APP_")
parser.parse_arguments_from_dataclass(Settings)
args = parser.parse_args(["--host", "example.com"])

# Extract CLI arguments and construct with overrides
cli_data = dict_from_args(args, Settings)
settings = construct_dataclass(
    Settings,
    override=cli_data,
    config=Config(env_prefix="APP_"),
)

API Reference

Framework Functions:

  • setup_main() - Entry point for multi-command CLI applications with automatic command discovery

Configuration Functions:

  • construct_dataclass(dc_type, override, *, config) - Construct dataclass from environment and overrides
  • env_for_dataclass(fields, config) - Read ENV vars for specific fields (advanced use)
  • Config - Configuration class with env_prefix, loaders, and other options

Parser Classes:

  • BoostedArgumentParser - Extended ArgumentParser with ENV variable support
  • DefaultsHelpFormatter - Help formatter showing defaults and required indicators

Dataclass Parsing:

  • dict_from_args(args, dataclass_type) - Extract CLI args to flat dict

Annotations:

  • Parser(func) - Custom parsing function for Annotated fields
  • Help(text) - Custom help text for Annotated fields

Exceptions:

  • CliSetupError - Base exception for CLI setup errors
  • FieldNameConflictError - Raised when flattened field names collide
  • UnsupportedFieldTypeError - Raised for unsupported field types

Supported Types

Type Example CLI Input ENV Input
int count: int --count 42 APP_COUNT=42
float ratio: float --ratio 3.14 APP_RATIO=3.14
str name: str --name hello APP_NAME=hello
bool debug: bool --debug true APP_DEBUG=yes
list[T] tags: list[str] --tags a,b,c APP_TAGS=a,b,c
dict[K,V] limits: dict[str,int] --limits a:1,b:2 APP_LIMITS=a:1,b:2
T | None port: int | None --port 80 APP_PORT=80
Nested dataclass db: Database --db-host localhost APP_DB_HOST=localhost
Custom Annotated[T, Parser(f)] Custom parsing Custom parsing

Type constraints:

  • list[T] and dict[K,V]: T, K, V must be simple types (int, float, str, bool) or Optional simple types
  • Union is only supported as Optional[T] (i.e., T | None)
  • Generic unions like str | int are not supported

Boolean parsing accepts (case-insensitive):

  • True: true, yes, on, 1
  • False: false, no, off, 0

Tips and Best Practices

  1. Use field(default_factory=...) for mutable defaults:

    from dataclasses import dataclass, field
    
    
    @dataclass(kw_only=True)
    class Settings:
        tags: list[str] = field(default_factory=list)  # Good
        # tags: list[str] = []  # Bad - mutable default
    
  2. Avoid field name conflicts with nested dataclasses:

    # This will raise FieldNameConflictError:
    @dataclass(kw_only=True)
    class Database:
        password: str
    
    
    @dataclass(kw_only=True)
    class Settings:
        db_password: str  # Conflicts with db.password when flattened
        db: Database
    
  3. Use env_prefix to scope environment variables:

    parser = BoostedArgumentParser(prog="myapp", env_prefix="MYAPP_")
    # Prevents conflicts with other apps' ENV vars
    

Development

Quick Start

  1. Clone the repository:
    git clone <repository-url>
    
  2. (?) Copy the example settings file and configure your settings:
    cp settings.example.yaml settings.yaml
    
  3. Build the Docker images:
    docker-compose build
    
  4. Install dependencies:
    make uv args="sync"
    
  5. Start the service:
    docker-compose up
    

Making Changes

  1. List available make commands:

    make help
    
  2. Check code style with:

    make lint
    
  3. Run tests using:

    make test
    
  4. Manage dependencies via uv:

    make uv args="<uv-args>"
    
    • For example: make uv args="add picodi"
  5. For local CI debugging:

    make run-ci
    

Pre-commit Hooks

We use pre-commit for linting and formatting:

  • It runs inside a Docker container by default.
  • Optionally, set up hooks locally:
    pre-commit install
    

Mypy

We use mypy for static type checking.

It is configured for strictly typed code, so you may need to add type hints to your code. But don't be very strict, sometimes it's better to use Any type.

License

MIT

Credits

This project was generated with yakimka/cookiecutter-pyproject.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

argparse_boost-0.2.0.tar.gz (38.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

argparse_boost-0.2.0-py3-none-any.whl (22.6 kB view details)

Uploaded Python 3

File details

Details for the file argparse_boost-0.2.0.tar.gz.

File metadata

  • Download URL: argparse_boost-0.2.0.tar.gz
  • Upload date:
  • Size: 38.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for argparse_boost-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4a145b8dbf05d17fb1974154bef593c4c4faf07b6f817e3bec2e533bf5dfe14a
MD5 2d10efe23d0803cceeafb5986a1096ca
BLAKE2b-256 9115da6cef88a1a381875c32fe37eef39489eb2854b0378001f17e49df6ebbe3

See more details on using hashes here.

File details

Details for the file argparse_boost-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: argparse_boost-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 22.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for argparse_boost-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6daaa7138f435cfe581b22f76e58ff8c0ddd4eb1211874697883ee5a13062fe5
MD5 dc0b6fd4f610aa8c3f302772bea72f21
BLAKE2b-256 259ee83cc201ac695cd163121dd737a0c421ef801ca4e53445fd2b74d0b20f5d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page