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:

import os
from argparse_boost import setup_main


def main() -> None:
    setup_main(
        prog="myapp",
        description="My CLI application",
        env_prefix="MYAPP_",
        package_path=os.path.dirname(__file__),
        prefix="myapp.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 env_for_dataclass, from_dict, field_path_to_env_name


@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
config_data = env_for_dataclass(
    AppConfig,
    name_maker=field_path_to_env_name(env_prefix="APP_"),
)
config = from_dict(config_data, AppConfig)

# Now use your type-safe config
print(f"Connecting to {config.db.host}:{config.db.port}")
print(f"Debug mode: {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,
    from_dict,
    dict_from_args,
    env_for_dataclass,
    field_path_to_env_name,
)


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


parser = BoostedArgumentParser(prog="myapp", env_prefix="APP_")
parser.parse_arguments_from_dataclass(Config)
args = parser.parse_args(["--host", "example.com"])

# Read environment variables
env_data = env_for_dataclass(
    Config,
    name_maker=field_path_to_env_name(env_prefix="APP_"),
)

# Extract CLI arguments
cli_data = dict_from_args(args, Config)

# Merge (CLI overrides ENV)
merged = {**env_data, **cli_data}

# Construct dataclass
config = from_dict(merged, Config)

API Reference

Framework Functions:

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

Configuration Functions:

  • from_dict(data, dataclass_type) - Construct dataclass from flat dict
  • env_for_dataclass(dataclass_type, name_maker) - Read ENV vars to flat dict
  • field_path_to_env_name(env_prefix) - Create ENV name mapper

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
  • arg_option_to_env_name(env_prefix) - Convert CLI option to ENV name

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 Config:
        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 Config:
        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.1.0.tar.gz (37.4 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.1.0-py3-none-any.whl (21.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: argparse_boost-0.1.0.tar.gz
  • Upload date:
  • Size: 37.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","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.1.0.tar.gz
Algorithm Hash digest
SHA256 4ad5764666a947353ad5e602fed17e7dad5c1a888f87224c87607b4e3357009e
MD5 32d1afefbdb48c93f7e2007bbe5077be
BLAKE2b-256 b5360366ac9f0c2d7dffed5156cef70ca050d1c46e529706a5273831ca2c4f1d

See more details on using hashes here.

File details

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

File metadata

  • Download URL: argparse_boost-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a779efb348447174a7ac23091cafc975f18538d1df77bf830ac67cd65a3bf6a6
MD5 4c8499a7b3b274b5fe5e538ec4d84dfd
BLAKE2b-256 6f6ee410a8fc8cc297682e558fd75c50d076582a5af4c5ece344adcd1da40805

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