cappa


Namecappa JSON
Version 0.18.1 PyPI version JSON
download
home_pagehttps://github.com/dancardin/cappa
SummaryDeclarative CLI argument parser.
upload_time2024-04-11 13:27:59
maintainerNone
docs_urlNone
authorDanCardin
requires_python<4,>=3.8
licenseApache-2.0
keywords cli parser argparse click typer
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Cappa

[![Actions Status](https://github.com/DanCardin/cappa/actions/workflows/test.yml/badge.svg)](https://github.com/dancardin/cappa/actions)
[![Coverage Status](https://coveralls.io/repos/github/DanCardin/cappa/badge.svg?branch=main)](https://coveralls.io/github/DanCardin/cappa?branch=main)
[![Documentation Status](https://readthedocs.org/projects/cappa/badge/?version=latest)](https://cappa.readthedocs.io/en/latest/?badge=latest)

- [Full documentation here](https://cappa.readthedocs.io/en/latest/).
- [Comparison vs existing libraries.](https://cappa.readthedocs.io/en/latest/comparison.html).
- [Annotation inference details](https://cappa.readthedocs.io/en/latest/annotation.html)
- ["invoke" (click-like) details](https://cappa.readthedocs.io/en/latest/invoke.html)

Cappa is a declarative command line parsing library, taking much of its
inspiration from the "Derive" API from the
[Clap](https://docs.rs/clap/latest/clap/_derive/index.html) written in Rust.

```python
from dataclasses import dataclass, field
import cappa
from typing import Literal
from typing_extensions import Annotated


@dataclass
class Example:
    positional_arg: str = "optional"
    boolean_flag: bool = False
    single_option: Annotated[int | None, cappa.Arg(short=True, help="A number")] = None
    multiple_option: Annotated[
        list[Literal["one", "two", "three"]],
        cappa.Arg(long=True, help="Pick one!"),
    ] = field(default_factory=list)


args: Example = cappa.parse(Example, backend=cappa.backend)
print(args)
```

Produces the following CLI:

![help text](./docs/source/_static/example.svg)

In this way, you can turn any dataclass-like object (with some additional
annotations, depending on what you're looking for) into a CLI.

You'll note that `cappa.parse` returns an instance of the class. This API should
feel very familiar to `argparse`, except that you get the fully typed dataclass
instance back instead of a raw `Namespace`.

## Invoke

["invoke" documentation](https://cappa.readthedocs.io/en/latest/invoke.html)

The "invoke" API is meant to feel more like the experience you get when using
`click` or `typer`. You can take the same dataclass, but register a function to
be called on successful parsing of the command.

```python
from dataclasses import dataclass
import cappa
from typing_extensions import Annotated

def function(example: Example):
    print(example)

@cappa.command(invoke=function)
class Example:  # identical to original class
    positional_arg: str
    boolean_flag: bool
    single_option: Annotated[int | None, cappa.Arg(long=True)]
    multiple_option: Annotated[list[str], cappa.Arg(short=True)]


cappa.invoke(Example)
```

(Note the lack of the dataclass decorator. You can optionally omit or include
it, and it will be automatically inferred).

Alternatively you can make your dataclass callable, as a shorthand for an
explcit invoke function:

```python
@dataclass
class Example:
    ...   # identical to original class

    def __call__(self):
       print(self)
```

Note `invoke=function` can either be a reference to some callable, or a string
module-reference to a function (which will get lazily imported and invoked).

With a single top-level command, the click-like API isn't particularly valuable
by comparison. Click's command-centric API is primarily useful when composing a
number of nested subcommands.

## Subcommands

The useful aspect of click's functional composability is that you can define
some number of subcommands functions under a parent command, whichever
subcommand the function targets will be invoked.

```python
import click

@click.group('example')
def example():
    ...

@example.command("print")
@click.option('--loudly', is_flag=True)
def print_cmd(loudly):
    if loudly:
      print("PRINTING!")
    else:
      print("printing!")

@example.command("fail")
@click.option('--code', type: int)
def fail_cmd(code):
    raise click.Exit(code=code)

# Called like:
# /example.py print
# /example.py fail
```

Whereas with argparse, you'd have had to manually match and call the funcitons
yourself. This API does all of the hard parts of deciding which function to
call.

Similarly, you can achieve the same thing with cappa.

```python
from __future__ import annotations
from dataclasses import dataclass
import cappa

@dataclass
class Example:
    cmd: cappa.Subcommands[Print | Fail]


def print_cmd(print: Print):
    if print.loudly:
        print("PRINTING!")
    else:
        print("printing!")

@cappa.command(invoke=print_cmd)
class Print:
    loudly: bool

@dataclass
class Fail:
    code: int

    def __call__(self):  # again, __call__ is shorthand for the above explicit `invoke=` form.
        raise cappa.Exit(code=code)

cappa.invoke(Example)
```

## Function-based Commands

Purely functions-based can only be used for certain kinds of CLI interfaces.
However, they **can** reduce the ceremony required to define a given CLI
command.

```python
import cappa
from typing_extensions import Annotated

def function(foo: int, bar: bool, option: Annotated[str, cappa.Arg(long=True)] = "opt"):
    ...


cappa.invoke(function)
```

Such a CLI is exactly equivalent to a CLI defined as a dataclass with the
function's arguments as the dataclass's fields.

There are various downsides to using functions. Given that there is no class to
reference, any feature which relies on being able to name the type will be
impossible to use. For example, subcommands cannot be naturally defined as
functions (since there is no type with which to reference the subcommand).


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/dancardin/cappa",
    "name": "cappa",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4,>=3.8",
    "maintainer_email": null,
    "keywords": "CLI, parser, argparse, click, typer",
    "author": "DanCardin",
    "author_email": "ddcardin@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/6b/4e/3827027b38f205255e027ef6944f43b69d2e2406800df2932c7df879ad83/cappa-0.18.1.tar.gz",
    "platform": null,
    "description": "# Cappa\n\n[![Actions Status](https://github.com/DanCardin/cappa/actions/workflows/test.yml/badge.svg)](https://github.com/dancardin/cappa/actions)\n[![Coverage Status](https://coveralls.io/repos/github/DanCardin/cappa/badge.svg?branch=main)](https://coveralls.io/github/DanCardin/cappa?branch=main)\n[![Documentation Status](https://readthedocs.org/projects/cappa/badge/?version=latest)](https://cappa.readthedocs.io/en/latest/?badge=latest)\n\n- [Full documentation here](https://cappa.readthedocs.io/en/latest/).\n- [Comparison vs existing libraries.](https://cappa.readthedocs.io/en/latest/comparison.html).\n- [Annotation inference details](https://cappa.readthedocs.io/en/latest/annotation.html)\n- [\"invoke\" (click-like) details](https://cappa.readthedocs.io/en/latest/invoke.html)\n\nCappa is a declarative command line parsing library, taking much of its\ninspiration from the \"Derive\" API from the\n[Clap](https://docs.rs/clap/latest/clap/_derive/index.html) written in Rust.\n\n```python\nfrom dataclasses import dataclass, field\nimport cappa\nfrom typing import Literal\nfrom typing_extensions import Annotated\n\n\n@dataclass\nclass Example:\n    positional_arg: str = \"optional\"\n    boolean_flag: bool = False\n    single_option: Annotated[int | None, cappa.Arg(short=True, help=\"A number\")] = None\n    multiple_option: Annotated[\n        list[Literal[\"one\", \"two\", \"three\"]],\n        cappa.Arg(long=True, help=\"Pick one!\"),\n    ] = field(default_factory=list)\n\n\nargs: Example = cappa.parse(Example, backend=cappa.backend)\nprint(args)\n```\n\nProduces the following CLI:\n\n![help text](./docs/source/_static/example.svg)\n\nIn this way, you can turn any dataclass-like object (with some additional\nannotations, depending on what you're looking for) into a CLI.\n\nYou'll note that `cappa.parse` returns an instance of the class. This API should\nfeel very familiar to `argparse`, except that you get the fully typed dataclass\ninstance back instead of a raw `Namespace`.\n\n## Invoke\n\n[\"invoke\" documentation](https://cappa.readthedocs.io/en/latest/invoke.html)\n\nThe \"invoke\" API is meant to feel more like the experience you get when using\n`click` or `typer`. You can take the same dataclass, but register a function to\nbe called on successful parsing of the command.\n\n```python\nfrom dataclasses import dataclass\nimport cappa\nfrom typing_extensions import Annotated\n\ndef function(example: Example):\n    print(example)\n\n@cappa.command(invoke=function)\nclass Example:  # identical to original class\n    positional_arg: str\n    boolean_flag: bool\n    single_option: Annotated[int | None, cappa.Arg(long=True)]\n    multiple_option: Annotated[list[str], cappa.Arg(short=True)]\n\n\ncappa.invoke(Example)\n```\n\n(Note the lack of the dataclass decorator. You can optionally omit or include\nit, and it will be automatically inferred).\n\nAlternatively you can make your dataclass callable, as a shorthand for an\nexplcit invoke function:\n\n```python\n@dataclass\nclass Example:\n    ...   # identical to original class\n\n    def __call__(self):\n       print(self)\n```\n\nNote `invoke=function` can either be a reference to some callable, or a string\nmodule-reference to a function (which will get lazily imported and invoked).\n\nWith a single top-level command, the click-like API isn't particularly valuable\nby comparison. Click's command-centric API is primarily useful when composing a\nnumber of nested subcommands.\n\n## Subcommands\n\nThe useful aspect of click's functional composability is that you can define\nsome number of subcommands functions under a parent command, whichever\nsubcommand the function targets will be invoked.\n\n```python\nimport click\n\n@click.group('example')\ndef example():\n    ...\n\n@example.command(\"print\")\n@click.option('--loudly', is_flag=True)\ndef print_cmd(loudly):\n    if loudly:\n      print(\"PRINTING!\")\n    else:\n      print(\"printing!\")\n\n@example.command(\"fail\")\n@click.option('--code', type: int)\ndef fail_cmd(code):\n    raise click.Exit(code=code)\n\n# Called like:\n# /example.py print\n# /example.py fail\n```\n\nWhereas with argparse, you'd have had to manually match and call the funcitons\nyourself. This API does all of the hard parts of deciding which function to\ncall.\n\nSimilarly, you can achieve the same thing with cappa.\n\n```python\nfrom __future__ import annotations\nfrom dataclasses import dataclass\nimport cappa\n\n@dataclass\nclass Example:\n    cmd: cappa.Subcommands[Print | Fail]\n\n\ndef print_cmd(print: Print):\n    if print.loudly:\n        print(\"PRINTING!\")\n    else:\n        print(\"printing!\")\n\n@cappa.command(invoke=print_cmd)\nclass Print:\n    loudly: bool\n\n@dataclass\nclass Fail:\n    code: int\n\n    def __call__(self):  # again, __call__ is shorthand for the above explicit `invoke=` form.\n        raise cappa.Exit(code=code)\n\ncappa.invoke(Example)\n```\n\n## Function-based Commands\n\nPurely functions-based can only be used for certain kinds of CLI interfaces.\nHowever, they **can** reduce the ceremony required to define a given CLI\ncommand.\n\n```python\nimport cappa\nfrom typing_extensions import Annotated\n\ndef function(foo: int, bar: bool, option: Annotated[str, cappa.Arg(long=True)] = \"opt\"):\n    ...\n\n\ncappa.invoke(function)\n```\n\nSuch a CLI is exactly equivalent to a CLI defined as a dataclass with the\nfunction's arguments as the dataclass's fields.\n\nThere are various downsides to using functions. Given that there is no class to\nreference, any feature which relies on being able to name the type will be\nimpossible to use. For example, subcommands cannot be naturally defined as\nfunctions (since there is no type with which to reference the subcommand).\n\n",
    "bugtrack_url": null,
    "license": "Apache-2.0",
    "summary": "Declarative CLI argument parser.",
    "version": "0.18.1",
    "project_urls": {
        "Homepage": "https://github.com/dancardin/cappa",
        "Repository": "https://github.com/dancardin/cappa"
    },
    "split_keywords": [
        "cli",
        " parser",
        " argparse",
        " click",
        " typer"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "67e3accef047c97a99e62d566afc388032f68710675e6839cc1f805178d8948b",
                "md5": "d96745c2815a7833ffd02075f2539ff5",
                "sha256": "dd20dab6d9cc1f2c65946fb4ac1ce97f17046e61c0f53427d8635fc138da990f"
            },
            "downloads": -1,
            "filename": "cappa-0.18.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d96745c2815a7833ffd02075f2539ff5",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4,>=3.8",
            "size": 55478,
            "upload_time": "2024-04-11T13:27:57",
            "upload_time_iso_8601": "2024-04-11T13:27:57.539205Z",
            "url": "https://files.pythonhosted.org/packages/67/e3/accef047c97a99e62d566afc388032f68710675e6839cc1f805178d8948b/cappa-0.18.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "6b4e3827027b38f205255e027ef6944f43b69d2e2406800df2932c7df879ad83",
                "md5": "d9f28bb19010a57e648d276f41df282b",
                "sha256": "c978e45e6fb514b562f300af7588c2af0e80d77b76520e37e9e0fd311c3718a8"
            },
            "downloads": -1,
            "filename": "cappa-0.18.1.tar.gz",
            "has_sig": false,
            "md5_digest": "d9f28bb19010a57e648d276f41df282b",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4,>=3.8",
            "size": 46981,
            "upload_time": "2024-04-11T13:27:59",
            "upload_time_iso_8601": "2024-04-11T13:27:59.180040Z",
            "url": "https://files.pythonhosted.org/packages/6b/4e/3827027b38f205255e027ef6944f43b69d2e2406800df2932c7df879ad83/cappa-0.18.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-11 13:27:59",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "dancardin",
    "github_project": "cappa",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "cappa"
}
        
Elapsed time: 0.23386s