# ponderosa: ergonomic subcommand handling built on argparse
![PyPI - Version](https://img.shields.io/pypi/v/ponderosa?link=https%3A%2F%2Fpypi.org%2Fproject%2Fponderosa%2F) ![Tests](https://github.com/camillescott/ponderosa/actions/workflows/pytest.yml/badge.svg) [![codecov](https://codecov.io/github/camillescott/ponderosa/graph/badge.svg?token=XSESR7TKXJ)](https://codecov.io/github/camillescott/ponderosa) <a href="https://github.com/camillescott/ponderosa/blob/latest/LICENSE"><img alt="License: 3-Clause BSD" src="https://img.shields.io/badge/License-BSD%203--Clause-blue.svg"></a> ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ponderosa) ![Static Badge](https://img.shields.io/badge/Platforms-Linux%20%7C%20MacOS%20%7C%20Windows-blue)
Ponderosa extends the Python standard library's [argparse](https://docs.python.org/3/library/argparse.html) 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
```python
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()
```
```console
$ 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:
```python
#!/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:
```console
$ 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:
```python
#!/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:
```console
$ 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:
```python
#!/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:
```console
$ 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)
```
Raw data
{
"_id": null,
"home_page": "https://github.com/camillescott/ponderosa",
"name": "ponderosa",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.12",
"maintainer_email": null,
"keywords": null,
"author": "Camille Scott",
"author_email": "camille@bogg.cc",
"download_url": "https://files.pythonhosted.org/packages/d1/91/9dc52b0401a8b21373a63c645c61595132bfe84e27bb751a565dab392fb7/ponderosa-0.5.1.tar.gz",
"platform": null,
"description": "# ponderosa: ergonomic subcommand handling built on argparse\n\n![PyPI - Version](https://img.shields.io/pypi/v/ponderosa?link=https%3A%2F%2Fpypi.org%2Fproject%2Fponderosa%2F) ![Tests](https://github.com/camillescott/ponderosa/actions/workflows/pytest.yml/badge.svg) [![codecov](https://codecov.io/github/camillescott/ponderosa/graph/badge.svg?token=XSESR7TKXJ)](https://codecov.io/github/camillescott/ponderosa) <a href=\"https://github.com/camillescott/ponderosa/blob/latest/LICENSE\"><img alt=\"License: 3-Clause BSD\" src=\"https://img.shields.io/badge/License-BSD%203--Clause-blue.svg\"></a> ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ponderosa) ![Static Badge](https://img.shields.io/badge/Platforms-Linux%20%7C%20MacOS%20%7C%20Windows-blue)\n\nPonderosa extends the Python standard library's [argparse](https://docs.python.org/3/library/argparse.html) in an effort to make dealing with deeply nested subcommand trees less ponderous. \nI'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.\nPonderosa gets rid of those giant blocks of `add_subparsers` nastiness without entirely reinventing the wheel at the lower level of parsing the arguments themselves.\n\n## Basic Usage\n\n```python\nfrom argparse import Namespace\nfrom ponderosa import ArgParser, CmdTree\n# ArgParser is just Union[argparse.ArgumentParser, argparse._ArgumentGroup]\n\ncommands = CmdTree(description='Ponderosa Basics')\n\n@commands.register('basics', help='Easy as pie \ud83e\udd67')\ndef basics_cmd(args: Namespace):\n print('Ponderosa \ud83c\udf32')\n if args.show:\n commands.print_help()\n\n@basics_cmd.args()\ndef _(parser: ArgParser):\n parser.add_argument('--show', action='store_true', default=False)\n\n@commands.register('basics', 'deeply', 'nested', help='A deeply nested command')\ndef deeply_nested_cmd(args: Namespace):\n print(f'Deeply nested command! Args: {args}')\n\n@commands.register('basics', 'deeply', 'also-nested', help='Another deeply nested command')\ndef deeply_nested_cmd(args: Namespace):\n print(f'Another deeply nested command! Args: {args}')\n\n@deeply_nested_cmd.args()\ndef _(parser: ArgParser):\n parser.add_argument('--deep', action='store_true', default=False)\n\nif __name__ == '__main__':\n commands.run()\n```\n\n```console\n$ python examples/basics.py basics --show\nPonderosa \ud83c\udf32\nusage: basics.py [-h] {basics} ...\n\nSubcommands:\n basics: Easy as pie \ud83e\udd67\n deeply: \n nested: A deeply nested command\n also-nested: Another deeply nested command\n\n$ python examples/basics.py basics deeply nested -h\nusage: basics.py basics deeply nested [-h] [--deep]\n\noptions:\n -h, --help show this help message and exit\n --deep\n```\n\n\n## Registering Subcommands\n\n\n\n\n## Add Postprocessors\n\nSometimes you want to add some postprocessing to your arguments that can only be done after parsing has already\noccurred - for example, validating one of your arguments might depend on opening a database connection.\nYou can register postprocessors on your argument groups to handle this:\n\n```python\n#!/usr/bin/env python3\n\nfrom argparse import Namespace\nfrom ponderosa import arggroup, ArgParser, CmdTree\n\ncommands = CmdTree()\n\n@arggroup('Foobar')\ndef foobar_args(group: ArgParser):\n group.add_argument('--foo', type=str)\n group.add_argument('--bar', type=int)\n \n@foobar_args.apply()\n@commands.register('foobar')\ndef foobar_cmd(args: Namespace) -> int:\n print(f'Handling subcommand with args: {args}')\n return 0\n \n@foobar_args.postprocessor()\ndef foobar_postprocessor(args: Namespace):\n print(f'Postprocessing args: {args}')\n\nif __name__ == '__main__': \n commands.run()\n```\n\nRunning the example gives, roughly:\n\n```console\n$ python examples/postprocessor.py foobar --bar 1 --foo bar \nPostprocessing args: Namespace(func=<function foobar_cmd at 0x7bc1ba0b1800>, foo='bar', bar=1)\nHandling subcommand with args: Namespace(func=<function foobar_cmd at 0x7bc1ba0b1800>, foo='bar', bar=1)\n```\n\nWe can of course register multiple postprocessors, and do so on the result of a `SubCmd.args`.\nBy default, the postprocessors will be executed in the order they are registered:\n\n```python\n#!/usr/bin/env python3\n\nfrom argparse import Namespace\nfrom ponderosa import ArgParser, CmdTree\n\ncommands = CmdTree()\n\n@commands.register('foobar')\ndef foobar_cmd(args: Namespace) -> int:\n print(f'Handling subcommand with args: {args}')\n return 0\n\n@foobar_cmd.args()\ndef foobar_args(group: ArgParser):\n group.add_argument('--foo', type=str)\n group.add_argument('--bar', type=int)\n \n@foobar_args.postprocessor()\ndef _(args: Namespace):\n print(f'First postprocessor: {args}')\n args.calculated = args.bar * 2\n\n@foobar_args.postprocessor()\ndef _(args: Namespace):\n print(f'Second postprocessor: {args}')\n\nif __name__ == '__main__': \n commands.run()\n```\n\nWhich gives:\n\n```console\n$ python examples/multi_postprocessor.py foobar --foo bar --bar 1\nSubCmd.args.wrapper: foobar\nFirst postprocessor: Namespace(func=<function foobar_cmd at 0x751415cb1a80>, foo='bar', bar=1)\nSecond postprocessor: Namespace(func=<function foobar_cmd at 0x751415cb1a80>, foo='bar', bar=1, calculated=2)\nHandling subcommand with args: Namespace(func=<function foobar_cmd at 0x751415cb1a80>, foo='bar', bar=1, calculated=2)\n```\n\nYou can also provide a priority to your postprocessors if registration order is insufficient:\n\n```python\n#!/usr/bin/env python3\n\nfrom argparse import Namespace\nfrom ponderosa import ArgParser, CmdTree\n\ncommands = CmdTree()\n\n@commands.register('foobar')\ndef foobar_cmd(args: Namespace) -> int:\n print(f'Handling subcommand with args: {args}')\n return 0\n\n@foobar_cmd.args()\ndef foobar_args(group: ArgParser):\n group.add_argument('--foo', type=str)\n group.add_argument('--bar', type=int)\n\n@foobar_args.postprocessor()\ndef _(args: Namespace):\n print(f'Low priority: {args}')\n\n# Usually, this function would run second, as it was defined second.\n# It will run first due to its priority score.\n@foobar_args.postprocessor(priority=100)\ndef _(args: Namespace):\n print(f'High priority: {args}')\n args.calculated = args.bar * 2\n\nif __name__ == '__main__': \n commands.run()\n```\n\nThis time, we get:\n\n```console\n$ python examples/priority_postprocessors.py foobar --bar 2 \nHigh priority: Namespace(func=<function foobar_cmd at 0x7693e57b5bc0>, foo=None, bar=2)\nLow priority: Namespace(func=<function foobar_cmd at 0x7693e57b5bc0>, foo=None, bar=2, calculated=4)\nHandling subcommand with args: Namespace(func=<function foobar_cmd at 0x7693e57b5bc0>, foo=None, bar=2, calculated=4)\n```",
"bugtrack_url": null,
"license": "BSD-3-Clause",
"summary": null,
"version": "0.5.1",
"project_urls": {
"Homepage": "https://github.com/camillescott/ponderosa"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "ca4e1d6f84e92330abb015bac0aaea2c1c525557e447aa274ec7f6a08b782d26",
"md5": "6abbd8a94bcf3ba76e24fd71cadc256c",
"sha256": "4b9bfb0bcc6d8cfe548a59df7d2f6fa0de3e9a938707b883291cad1c334920f1"
},
"downloads": -1,
"filename": "ponderosa-0.5.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "6abbd8a94bcf3ba76e24fd71cadc256c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.12",
"size": 8884,
"upload_time": "2024-11-05T02:52:28",
"upload_time_iso_8601": "2024-11-05T02:52:28.485933Z",
"url": "https://files.pythonhosted.org/packages/ca/4e/1d6f84e92330abb015bac0aaea2c1c525557e447aa274ec7f6a08b782d26/ponderosa-0.5.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "d1919dc52b0401a8b21373a63c645c61595132bfe84e27bb751a565dab392fb7",
"md5": "7eb405b1d5600794f49adbc3074e8440",
"sha256": "f7db0da29d6cf03f03bd8e788f02aae1c58b5540c6dbceeea7c401c076d8a8d0"
},
"downloads": -1,
"filename": "ponderosa-0.5.1.tar.gz",
"has_sig": false,
"md5_digest": "7eb405b1d5600794f49adbc3074e8440",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.12",
"size": 10251,
"upload_time": "2024-11-05T02:52:29",
"upload_time_iso_8601": "2024-11-05T02:52:29.801767Z",
"url": "https://files.pythonhosted.org/packages/d1/91/9dc52b0401a8b21373a63c645c61595132bfe84e27bb751a565dab392fb7/ponderosa-0.5.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-11-05 02:52:29",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "camillescott",
"github_project": "ponderosa",
"github_fetch_exception": true,
"lcname": "ponderosa"
}