Skip to main content

No project description provided

Project description

ponderosa: ergonomic subcommand handling built on argparse

PyPI - Version Tests codecov License: 3-Clause BSD PyPI - Python Version Static Badge

Ponderosa extends the Python standard library's argparse in an effort to make dealing with deeply nested subcommand trees less ponderous. I've tried out many different command line parsing libraries over the years, but none of them have quite scratched the itch for this use case. Ponderosa gets rid of those giant blocks of add_subparsers nastiness without entirely reinventing the wheel at the lower level of parsing the arguments themselves.

Basic Usage

from argparse import Namespace
from ponderosa import ArgParser, CmdTree
# ArgParser is just Union[argparse.ArgumentParser, argparse._ArgumentGroup]

commands = CmdTree(description='Ponderosa Basics')

@commands.register('basics', help='Easy as pie 🥧')
def basics_cmd(args: Namespace):
    print('Ponderosa 🌲')
    if args.show:
        commands.print_help()

@basics_cmd.args()
def _(parser: ArgParser):
    parser.add_argument('--show', action='store_true', default=False)

@commands.register('basics', 'deeply', 'nested', help='A deeply nested command')
def deeply_nested_cmd(args: Namespace):
    print(f'Deeply nested command! Args: {args}')

@commands.register('basics', 'deeply', 'also-nested', help='Another deeply nested command')
def deeply_nested_cmd(args: Namespace):
    print(f'Another deeply nested command! Args: {args}')

@deeply_nested_cmd.args()
def _(parser: ArgParser):
    parser.add_argument('--deep', action='store_true', default=False)

if __name__ == '__main__':
    commands.run()
$ python examples/basics.py basics --show
Ponderosa 🌲
usage: basics.py [-h] {basics} ...

Subcommands:
  basics: Easy as pie 🥧
    deeply: 
      nested: A deeply nested command
      also-nested: Another deeply nested command

$ python examples/basics.py basics deeply nested -h
usage: basics.py basics deeply nested [-h] [--deep]

options:
  -h, --help  show this help message and exit
  --deep

Registering Subcommands

Add Postprocessors

Sometimes you want to add some postprocessing to your arguments that can only be done after parsing has already occurred - for example, validating one of your arguments might depend on opening a database connection. You can register postprocessors on your argument groups to handle this:

#!/usr/bin/env python3

from argparse import Namespace
from ponderosa import arggroup, ArgParser, CmdTree

commands = CmdTree()

@arggroup('Foobar')
def foobar_args(group: ArgParser):
    group.add_argument('--foo', type=str)
    group.add_argument('--bar', type=int)
    
@foobar_args.apply()
@commands.register('foobar')
def foobar_cmd(args: Namespace) -> int:
    print(f'Handling subcommand with args: {args}')
    return 0
    
@foobar_args.postprocessor()
def foobar_postprocessor(args: Namespace):
    print(f'Postprocessing args: {args}')

if __name__ == '__main__':    
    commands.run()

Running the example gives, roughly:

$ python examples/postprocessor.py foobar --bar 1 --foo bar      
Postprocessing args: Namespace(func=<function foobar_cmd at 0x7bc1ba0b1800>, foo='bar', bar=1)
Handling subcommand with args: Namespace(func=<function foobar_cmd at 0x7bc1ba0b1800>, foo='bar', bar=1)

We can of course register multiple postprocessors, and do so on the result of a SubCmd.args. By default, the postprocessors will be executed in the order they are registered:

#!/usr/bin/env python3

from argparse import Namespace
from ponderosa import ArgParser, CmdTree

commands = CmdTree()

@commands.register('foobar')
def foobar_cmd(args: Namespace) -> int:
    print(f'Handling subcommand with args: {args}')
    return 0

@foobar_cmd.args()
def foobar_args(group: ArgParser):
    group.add_argument('--foo', type=str)
    group.add_argument('--bar', type=int)
    
@foobar_args.postprocessor()
def _(args: Namespace):
    print(f'First postprocessor: {args}')
    args.calculated = args.bar * 2

@foobar_args.postprocessor()
def _(args: Namespace):
    print(f'Second postprocessor: {args}')

if __name__ == '__main__':    
    commands.run()

Which gives:

$ python examples/multi_postprocessor.py foobar --foo bar --bar 1
SubCmd.args.wrapper: foobar
First postprocessor: Namespace(func=<function foobar_cmd at 0x751415cb1a80>, foo='bar', bar=1)
Second postprocessor: Namespace(func=<function foobar_cmd at 0x751415cb1a80>, foo='bar', bar=1, calculated=2)
Handling subcommand with args: Namespace(func=<function foobar_cmd at 0x751415cb1a80>, foo='bar', bar=1, calculated=2)

You can also provide a priority to your postprocessors if registration order is insufficient:

#!/usr/bin/env python3

from argparse import Namespace
from ponderosa import ArgParser, CmdTree

commands = CmdTree()

@commands.register('foobar')
def foobar_cmd(args: Namespace) -> int:
    print(f'Handling subcommand with args: {args}')
    return 0

@foobar_cmd.args()
def foobar_args(group: ArgParser):
    group.add_argument('--foo', type=str)
    group.add_argument('--bar', type=int)

@foobar_args.postprocessor()
def _(args: Namespace):
    print(f'Low priority: {args}')

# Usually, this function would run second, as it was defined second.
# It will run first due to its priority score.
@foobar_args.postprocessor(priority=100)
def _(args: Namespace):
    print(f'High priority: {args}')
    args.calculated = args.bar * 2

if __name__ == '__main__':    
    commands.run()

This time, we get:

$ python examples/priority_postprocessors.py foobar --bar 2 
High priority: Namespace(func=<function foobar_cmd at 0x7693e57b5bc0>, foo=None, bar=2)
Low priority: Namespace(func=<function foobar_cmd at 0x7693e57b5bc0>, foo=None, bar=2, calculated=4)
Handling subcommand with args: Namespace(func=<function foobar_cmd at 0x7693e57b5bc0>, foo=None, bar=2, calculated=4)

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

ponderosa-0.5.1.tar.gz (10.3 kB view details)

Uploaded Source

Built Distribution

ponderosa-0.5.1-py3-none-any.whl (8.9 kB view details)

Uploaded Python 3

File details

Details for the file ponderosa-0.5.1.tar.gz.

File metadata

  • Download URL: ponderosa-0.5.1.tar.gz
  • Upload date:
  • Size: 10.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.4 CPython/3.12.1 Linux/6.5.0-1025-azure

File hashes

Hashes for ponderosa-0.5.1.tar.gz
Algorithm Hash digest
SHA256 f7db0da29d6cf03f03bd8e788f02aae1c58b5540c6dbceeea7c401c076d8a8d0
MD5 7eb405b1d5600794f49adbc3074e8440
BLAKE2b-256 d1919dc52b0401a8b21373a63c645c61595132bfe84e27bb751a565dab392fb7

See more details on using hashes here.

File details

Details for the file ponderosa-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: ponderosa-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 8.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.4 CPython/3.12.1 Linux/6.5.0-1025-azure

File hashes

Hashes for ponderosa-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4b9bfb0bcc6d8cfe548a59df7d2f6fa0de3e9a938707b883291cad1c334920f1
MD5 6abbd8a94bcf3ba76e24fd71cadc256c
BLAKE2b-256 ca4e1d6f84e92330abb015bac0aaea2c1c525557e447aa274ec7f6a08b782d26

See more details on using hashes here.

Supported by

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