argclass


Nameargclass JSON
Version 1.0.3 PyPI version JSON
download
home_pagehttps://github.com/mosquito/argclass
SummaryA wrapper around the standard argparse module that allows you to describe argument parsers declaratively
upload_time2024-04-17 10:32:43
maintainerNone
docs_urlNone
authorDmitry Orlov
requires_python<4.0,>=3.8
licenseApache 2
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            # argclass

![Coverage](https://coveralls.io/repos/github/mosquito/argclass/badge.svg?branch=master) [![Actions](https://github.com/mosquito/argclass/workflows/tests/badge.svg)](https://github.com/mosquito/argclass/actions?query=workflow%3Atests) [![Latest Version](https://img.shields.io/pypi/v/argclass.svg)](https://pypi.python.org/pypi/argclass/) [![Python Versions](https://img.shields.io/pypi/pyversions/argclass.svg)](https://pypi.python.org/pypi/argclass/) [![License](https://img.shields.io/pypi/l/argclass.svg)](https://pypi.python.org/pypi/argclass/)

A wrapper around the standard ``argparse`` module that allows you to describe
argument parsers declaratively.

By default, the ``argparse`` module suggests creating parsers imperative,
which is not convenient from the point of view of type checking and
access to attributes, of course, IDE autocompletion and type hints not
applicable in this case.

This module allows you to declare command-line parsers with classes.

Simple example:

<!--- name: test_simple_example --->
```python
import logging
import argclass

class CopyParser(argclass.Parser):
    recursive: bool
    preserve_attributes: bool

parser = CopyParser()
parser.parse_args(["--recursive", "--preserve-attributes"])
assert parser.recursive
assert parser.preserve_attributes
```
As you can see this example shown a basic module usage, when you want specify
argument default and other options you have to use ``argclass.Argument``.

Following example use ``argclass.Argument`` and argument groups:

<!-- name: test_example -->
```python

from typing import FrozenSet
import logging
import argclass

class AddressPortGroup(argclass.Group):
    address: str = argclass.Argument(default="127.0.0.1")
    port: int

class Parser(argclass.Parser):
    log_level: int = argclass.LogLevel
    http = AddressPortGroup(title="HTTP options", defaults=dict(port=8080))
    rpc = AddressPortGroup(title="RPC options", defaults=dict(port=9090))
    user_id: FrozenSet[int] = argclass.Argument(
        nargs="*", type=int, converter=frozenset
    )

parser = Parser(
    config_files=[".example.ini", "~/.example.ini", "/etc/example.ini"],
    auto_env_var_prefix="EXAMPLE_"
)
parser.parse_args([])

# Remove all used environment variables from os.environ
parser.sanitize_env()

logging.basicConfig(level=parser.log_level)
logging.info('Listening http://%s:%d', parser.http.address, parser.http.port)
logging.info(f'Listening rpc://%s:%d', parser.rpc.address, parser.rpc.port)

assert parser.http.address == '127.0.0.1'
assert parser.rpc.address == '127.0.0.1'

assert parser.http.port == 8080
assert parser.rpc.port == 9090
```

Run this script:

```shell
$ python example.py
INFO:root:Listening http://127.0.0.1:8080
INFO:root:Listening rpc://127.0.0.1:9090
```

Example of ``--help`` output:

```shell
$ python example.py --help
usage: example.py [-h] [--log-level {debug,info,warning,error,critical}]
                 [--http-address HTTP_ADDRESS] [--http-port HTTP_PORT]
                 [--rpc-address RPC_ADDRESS] [--rpc-port RPC_PORT]

optional arguments:
  -h, --help            show this help message and exit
  --log-level {debug,info,warning,error,critical}
                        (default: info) [ENV: EXAMPLE_LOG_LEVEL]

HTTP options:
  --http-address HTTP_ADDRESS
                        (default: 127.0.0.1) [ENV: EXAMPLE_HTTP_ADDRESS]
  --http-port HTTP_PORT
                        (default: 8080) [ENV: EXAMPLE_HTTP_PORT]

RPC options:
  --rpc-address RPC_ADDRESS
                        (default: 127.0.0.1) [ENV: EXAMPLE_RPC_ADDRESS]
  --rpc-port RPC_PORT   (default: 9090) [ENV: EXAMPLE_RPC_PORT]

Default values will based on following configuration files ['example.ini',
'~/.example.ini', '/etc/example.ini']. Now 1 files has been applied
['example.ini']. The configuration files is INI-formatted files where
configuration groups is INI sections.
See more https://pypi.org/project/argclass/#configs
```

## Secrets

Arguments reflecting some sensitive data, tokens or encryption keys,
urls with passwords, when passed through environment variables or a
configuration file, can be printed in the output of `--help`.
To hide defaults, add the `secret=True` parameter,
or use the special default constructor `argclass.Secret` instead of
`argclass.Argument`.

```python
import argclass

class HttpAuthentication(argclass.Group):
    username: str = argclass.Argument()
    password: str = argclass.Secret()

class HttpBearerAuthentication(argclass.Group):
    token: str = argclass.Argument(secret=True)

class Parser(argclass.Parser):
    http_basic = HttpAuthentication()
    http_bearer = HttpBearerAuthentication()

parser = Parser()
parser.print_help()
```

### Trying to protect data from being written to the log

A secret is not actually a string, but a special class inherited
from a `str`, and all attempts to cast this type to a `str`
(using `__str__` method) should be fine, and returning the original
value, unless the `__str__` method call is from a `logging` module.

```python
>>> import logging
>>> from argclass import SecretString
>>> logging.basicConfig(level=logging.INFO)
>>> s = SecretString("my-secret-password")
>>> logging.info(s)          # __str__ will be called from logging
>>> logging.info(f"s=%s", s) # __str__ will be called from logging too
>>> logging.info(f"{s!r}")   # repr is safe
>>> logging.info(f"{s}")     # the password will be compromised
```

Of course this is not a absolute sensitive data protection,
but I hope it helps against accidental logging of this kind of values.

The repr for this will always give placeholder, so it is better to always
add `!r` for any f-string, for example `f'{value!r}'`.


## Configs

The parser objects might be get default values from environment variables or
one of passed configuration files.

```python
import argclass

class AddressPortGroup(argclass.Group):
    address: str = argclass.Argument(default="127.0.0.1")
    port: int


class Parser(argclass.Parser):
    spam: str
    quantity: int
    log_level: int = argclass.LogLevel
    http = AddressPortGroup(title="HTTP options")
    rpc = AddressPortGroup(title="RPC options")
    user_ids = argclass.Argument(
        type=int, converter=frozenset, nargs=argclass.Nargs.ONE_OR_MORE
    )


# Trying to parse all passed configuration files
# and break after first success.
parser = Parser(
    config_files=[".example.ini", "~/.example.ini", "/etc/example.ini"],
)
parser.parse_args()
```

In this case each passed and existent configuration file will be opened.

The root level arguments might be described in the ``[DEFAULT]`` section.

Other arguments might be described in group specific sections.

So the full example of config file for above example is:

```ini
[DEFAULT]
log_level=info
spam=egg
quantity=100
user_ids=[1, 2, 3]

[http]
address=127.0.0.1
port=8080

[rpc]
address=127.0.0.1
port=9090
```

## Enum argument

<!-- name: test_enum_argument -->
```python

import enum
import logging
import argclass

class LogLevelEnum(enum.IntEnum):
    debug = logging.DEBUG
    info = logging.INFO
    warning = logging.WARNING
    error = logging.ERROR
    critical = logging.CRITICAL


class Parser(argclass.Parser):
    """Log level with default"""
    log_level = argclass.EnumArgument(LogLevelEnum, default="info")


class ParserLogLevelIsRequired(argclass.Parser):
    log_level: LogLevelEnum

parser = Parser()
parser.parse_args([])
assert parser.log_level == logging.INFO

parser = Parser()
parser.parse_args(["--log-level=error"])
assert parser.log_level == logging.ERROR

parser = ParserLogLevelIsRequired()
parser.parse_args(["--log-level=warning"])
assert parser.log_level == logging.WARNING
```

## Config Action

This library provides base class for writing custom configuration parsers.


### YAML parser

```python
from typing import Mapping, Any
from pathlib import Path

import argclass
import yaml


class YAMLConfigAction(argclass.ConfigAction):
    def parse_file(self, file: Path) -> Mapping[str, Any]:
        with file.open("r") as fp:
            return yaml.load_all(fp)


class YAMLConfigArgument(argclass.ConfigArgument):
    action = YAMLConfigAction


class Parser(argclass.Parser):
    config = argclass.Config(
        required=True,
        config_class=YAMLConfigArgument,
    )
```

### TOML parser

```python
import tomli
import argclass
from pathlib import Path
from typing import Mapping, Any


class TOMLConfigAction(argclass.ConfigAction):
    def parse_file(self, file: Path) -> Mapping[str, Any]:
        with file.open("r") as fp:
            return tomli.load(fp)

class TOMLConfigArgument(argclass.ConfigArgument):
    action = TOMLConfigAction


class Parser(argclass.Parser):
    config = argclass.Config(
        required=True,
        config_class=TOMLConfigArgument,
    )
```

## Subparsers

There are two ways to work with subparsers: either by calling the parser as a regular function, and in this case, 
the subparser must implement the `__call__` method, otherwise help will be printed and the program will exit with
an error. Or you can directly look at the `.current_subparser` attribute in the parser. The second method seems 
more complicated, but it becomes less difficult if you use singledispatch from the standard library.

### Using `__call__`

Just implement `__call__` method for subparsers and call

```python
from typing import Optional

import argclass


class AddressPortGroup(argclass.Group):
    address: str = "127.0.0.1"
    port: int = 8080


class CommitCommand(argclass.Parser):
    comment: str = argclass.Argument()

    def __call__(self):
        endpoint: AddressPortGroup = self.__parent__.endpoint
        print(
            "Commit command called", self, 
            "endpoint", endpoint.address, "port", endpoint.port
        )


class PushCommand(argclass.Parser):
    comment: str = argclass.Argument()

    def __call__(self):
        endpoint: AddressPortGroup = self.__parent__.endpoint
        print(
            "Push command called", self, 
            "endpoint", endpoint.address, "port", endpoint.port
        )


class Parser(argclass.Parser):
    log_level: int = argclass.LogLevel
    endpoint = AddressPortGroup(title="Endpoint options")
    commit: Optional[CommitCommand] = CommitCommand()
    push: Optional[PushCommand] = PushCommand()


parser = Parser(
    config_files=["example.ini", "~/.example.ini", "/etc/example.ini"],
    auto_env_var_prefix="EXAMPLE_"
)
parser.parse_args()
parser()
```

### Using singledispatch

Complex example with subparsers:

```python
from functools import singledispatch
from typing import Optional, Any

import argclass


class AddressPortGroup(argclass.Group):
    address: str = argclass.Argument(default="127.0.0.1")
    port: int


class CommitCommand(argclass.Parser):
    comment: str = argclass.Argument()


class PushCommand(argclass.Parser):
    comment: str = argclass.Argument()


class Parser(argclass.Parser):
    log_level: int = argclass.LogLevel
    endpoint = AddressPortGroup(
        title="Endpoint options",
        defaults=dict(port=8080)
    )
    commit: Optional[CommitCommand] = CommitCommand()
    push: Optional[PushCommand] = PushCommand()


@singledispatch
def handle_subparser(subparser: Any) -> None:
    raise NotImplementedError(
        f"Unexpected subparser type {subparser.__class__!r}"
    )


@handle_subparser.register(type(None))
def handle_none(_: None) -> None:
    Parser().print_help()
    exit(2)


@handle_subparser.register(CommitCommand)
def handle_commit(subparser: CommitCommand) -> None:
    print("Commit command called", subparser)


@handle_subparser.register(PushCommand)
def handle_push(subparser: PushCommand) -> None:
    print("Push command called", subparser)


parser = Parser(
    config_files=["example.ini", "~/.example.ini", "/etc/example.ini"],
    auto_env_var_prefix="EXAMPLE_"
)
parser.parse_args()
handle_subparser(parser.current_subparser)
```

## Value conversion

If the argument has a generic or composite type, then you must explicitly
describe it using ``argclass.Argument``, while specifying the converter
function with ``type`` or ``converter`` argument to transform the value
after parsing the arguments.

The exception to this rule is `Optional` with a single type. In this case,
an argument without a default value will not be required,
and its value can be None.

<!-- name: test_converter -->
```python
import argclass
from typing import Optional, Union

def converter(value: str) -> Optional[Union[int, str, bool]]:
    if value.lower() == "none":
        return None
    if value.isdigit():
        return int(value)
    if value.lower() in ("yes", "true", "enabled", "enable", "on"):
        return True
    return False


class Parser(argclass.Parser):
    gizmo: Optional[Union[int, str, bool]] = argclass.Argument(
        converter=converter
    )
    optional: Optional[int]


parser = Parser()

parser.parse_args(["--gizmo=65535"])
assert parser.gizmo == 65535

parser.parse_args(["--gizmo=None"])
assert parser.gizmo is None

parser.parse_args(["--gizmo=on"])
assert parser.gizmo is True
assert parser.optional is None

parser.parse_args(["--gizmo=off", "--optional=10"])
assert parser.gizmo is False
assert parser.optional == 10
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/mosquito/argclass",
    "name": "argclass",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": "Dmitry Orlov",
    "author_email": "me@mosquito.su",
    "download_url": "https://files.pythonhosted.org/packages/6a/91/168fd776db1b7c9758aa353c9258162df4a88173cab804db4825928e213e/argclass-1.0.3.tar.gz",
    "platform": null,
    "description": "# argclass\n\n![Coverage](https://coveralls.io/repos/github/mosquito/argclass/badge.svg?branch=master) [![Actions](https://github.com/mosquito/argclass/workflows/tests/badge.svg)](https://github.com/mosquito/argclass/actions?query=workflow%3Atests) [![Latest Version](https://img.shields.io/pypi/v/argclass.svg)](https://pypi.python.org/pypi/argclass/) [![Python Versions](https://img.shields.io/pypi/pyversions/argclass.svg)](https://pypi.python.org/pypi/argclass/) [![License](https://img.shields.io/pypi/l/argclass.svg)](https://pypi.python.org/pypi/argclass/)\n\nA wrapper around the standard ``argparse`` module that allows you to describe\nargument parsers declaratively.\n\nBy default, the ``argparse`` module suggests creating parsers imperative,\nwhich is not convenient from the point of view of type checking and\naccess to attributes, of course, IDE autocompletion and type hints not\napplicable in this case.\n\nThis module allows you to declare command-line parsers with classes.\n\nSimple example:\n\n<!--- name: test_simple_example --->\n```python\nimport logging\nimport argclass\n\nclass CopyParser(argclass.Parser):\n    recursive: bool\n    preserve_attributes: bool\n\nparser = CopyParser()\nparser.parse_args([\"--recursive\", \"--preserve-attributes\"])\nassert parser.recursive\nassert parser.preserve_attributes\n```\nAs you can see this example shown a basic module usage, when you want specify\nargument default and other options you have to use ``argclass.Argument``.\n\nFollowing example use ``argclass.Argument`` and argument groups:\n\n<!-- name: test_example -->\n```python\n\nfrom typing import FrozenSet\nimport logging\nimport argclass\n\nclass AddressPortGroup(argclass.Group):\n    address: str = argclass.Argument(default=\"127.0.0.1\")\n    port: int\n\nclass Parser(argclass.Parser):\n    log_level: int = argclass.LogLevel\n    http = AddressPortGroup(title=\"HTTP options\", defaults=dict(port=8080))\n    rpc = AddressPortGroup(title=\"RPC options\", defaults=dict(port=9090))\n    user_id: FrozenSet[int] = argclass.Argument(\n        nargs=\"*\", type=int, converter=frozenset\n    )\n\nparser = Parser(\n    config_files=[\".example.ini\", \"~/.example.ini\", \"/etc/example.ini\"],\n    auto_env_var_prefix=\"EXAMPLE_\"\n)\nparser.parse_args([])\n\n# Remove all used environment variables from os.environ\nparser.sanitize_env()\n\nlogging.basicConfig(level=parser.log_level)\nlogging.info('Listening http://%s:%d', parser.http.address, parser.http.port)\nlogging.info(f'Listening rpc://%s:%d', parser.rpc.address, parser.rpc.port)\n\nassert parser.http.address == '127.0.0.1'\nassert parser.rpc.address == '127.0.0.1'\n\nassert parser.http.port == 8080\nassert parser.rpc.port == 9090\n```\n\nRun this script:\n\n```shell\n$ python example.py\nINFO:root:Listening http://127.0.0.1:8080\nINFO:root:Listening rpc://127.0.0.1:9090\n```\n\nExample of ``--help`` output:\n\n```shell\n$ python example.py --help\nusage: example.py [-h] [--log-level {debug,info,warning,error,critical}]\n                 [--http-address HTTP_ADDRESS] [--http-port HTTP_PORT]\n                 [--rpc-address RPC_ADDRESS] [--rpc-port RPC_PORT]\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --log-level {debug,info,warning,error,critical}\n                        (default: info) [ENV: EXAMPLE_LOG_LEVEL]\n\nHTTP options:\n  --http-address HTTP_ADDRESS\n                        (default: 127.0.0.1) [ENV: EXAMPLE_HTTP_ADDRESS]\n  --http-port HTTP_PORT\n                        (default: 8080) [ENV: EXAMPLE_HTTP_PORT]\n\nRPC options:\n  --rpc-address RPC_ADDRESS\n                        (default: 127.0.0.1) [ENV: EXAMPLE_RPC_ADDRESS]\n  --rpc-port RPC_PORT   (default: 9090) [ENV: EXAMPLE_RPC_PORT]\n\nDefault values will based on following configuration files ['example.ini',\n'~/.example.ini', '/etc/example.ini']. Now 1 files has been applied\n['example.ini']. The configuration files is INI-formatted files where\nconfiguration groups is INI sections.\nSee more https://pypi.org/project/argclass/#configs\n```\n\n## Secrets\n\nArguments reflecting some sensitive data, tokens or encryption keys,\nurls with passwords, when passed through environment variables or a\nconfiguration file, can be printed in the output of `--help`.\nTo hide defaults, add the `secret=True` parameter,\nor use the special default constructor `argclass.Secret` instead of\n`argclass.Argument`.\n\n```python\nimport argclass\n\nclass HttpAuthentication(argclass.Group):\n    username: str = argclass.Argument()\n    password: str = argclass.Secret()\n\nclass HttpBearerAuthentication(argclass.Group):\n    token: str = argclass.Argument(secret=True)\n\nclass Parser(argclass.Parser):\n    http_basic = HttpAuthentication()\n    http_bearer = HttpBearerAuthentication()\n\nparser = Parser()\nparser.print_help()\n```\n\n### Trying to protect data from being written to the log\n\nA secret is not actually a string, but a special class inherited\nfrom a `str`, and all attempts to cast this type to a `str`\n(using `__str__` method) should be fine, and returning the original\nvalue, unless the `__str__` method call is from a `logging` module.\n\n```python\n>>> import logging\n>>> from argclass import SecretString\n>>> logging.basicConfig(level=logging.INFO)\n>>> s = SecretString(\"my-secret-password\")\n>>> logging.info(s)          # __str__ will be called from logging\n>>> logging.info(f\"s=%s\", s) # __str__ will be called from logging too\n>>> logging.info(f\"{s!r}\")   # repr is safe\n>>> logging.info(f\"{s}\")     # the password will be compromised\n```\n\nOf course this is not a absolute sensitive data protection,\nbut I hope it helps against accidental logging of this kind of values.\n\nThe repr for this will always give placeholder, so it is better to always\nadd `!r` for any f-string, for example `f'{value!r}'`.\n\n\n## Configs\n\nThe parser objects might be get default values from environment variables or\none of passed configuration files.\n\n```python\nimport argclass\n\nclass AddressPortGroup(argclass.Group):\n    address: str = argclass.Argument(default=\"127.0.0.1\")\n    port: int\n\n\nclass Parser(argclass.Parser):\n    spam: str\n    quantity: int\n    log_level: int = argclass.LogLevel\n    http = AddressPortGroup(title=\"HTTP options\")\n    rpc = AddressPortGroup(title=\"RPC options\")\n    user_ids = argclass.Argument(\n        type=int, converter=frozenset, nargs=argclass.Nargs.ONE_OR_MORE\n    )\n\n\n# Trying to parse all passed configuration files\n# and break after first success.\nparser = Parser(\n    config_files=[\".example.ini\", \"~/.example.ini\", \"/etc/example.ini\"],\n)\nparser.parse_args()\n```\n\nIn this case each passed and existent configuration file will be opened.\n\nThe root level arguments might be described in the ``[DEFAULT]`` section.\n\nOther arguments might be described in group specific sections.\n\nSo the full example of config file for above example is:\n\n```ini\n[DEFAULT]\nlog_level=info\nspam=egg\nquantity=100\nuser_ids=[1, 2, 3]\n\n[http]\naddress=127.0.0.1\nport=8080\n\n[rpc]\naddress=127.0.0.1\nport=9090\n```\n\n## Enum argument\n\n<!-- name: test_enum_argument -->\n```python\n\nimport enum\nimport logging\nimport argclass\n\nclass LogLevelEnum(enum.IntEnum):\n    debug = logging.DEBUG\n    info = logging.INFO\n    warning = logging.WARNING\n    error = logging.ERROR\n    critical = logging.CRITICAL\n\n\nclass Parser(argclass.Parser):\n    \"\"\"Log level with default\"\"\"\n    log_level = argclass.EnumArgument(LogLevelEnum, default=\"info\")\n\n\nclass ParserLogLevelIsRequired(argclass.Parser):\n    log_level: LogLevelEnum\n\nparser = Parser()\nparser.parse_args([])\nassert parser.log_level == logging.INFO\n\nparser = Parser()\nparser.parse_args([\"--log-level=error\"])\nassert parser.log_level == logging.ERROR\n\nparser = ParserLogLevelIsRequired()\nparser.parse_args([\"--log-level=warning\"])\nassert parser.log_level == logging.WARNING\n```\n\n## Config Action\n\nThis library provides base class for writing custom configuration parsers.\n\n\n### YAML parser\n\n```python\nfrom typing import Mapping, Any\nfrom pathlib import Path\n\nimport argclass\nimport yaml\n\n\nclass YAMLConfigAction(argclass.ConfigAction):\n    def parse_file(self, file: Path) -> Mapping[str, Any]:\n        with file.open(\"r\") as fp:\n            return yaml.load_all(fp)\n\n\nclass YAMLConfigArgument(argclass.ConfigArgument):\n    action = YAMLConfigAction\n\n\nclass Parser(argclass.Parser):\n    config = argclass.Config(\n        required=True,\n        config_class=YAMLConfigArgument,\n    )\n```\n\n### TOML parser\n\n```python\nimport tomli\nimport argclass\nfrom pathlib import Path\nfrom typing import Mapping, Any\n\n\nclass TOMLConfigAction(argclass.ConfigAction):\n    def parse_file(self, file: Path) -> Mapping[str, Any]:\n        with file.open(\"r\") as fp:\n            return tomli.load(fp)\n\nclass TOMLConfigArgument(argclass.ConfigArgument):\n    action = TOMLConfigAction\n\n\nclass Parser(argclass.Parser):\n    config = argclass.Config(\n        required=True,\n        config_class=TOMLConfigArgument,\n    )\n```\n\n## Subparsers\n\nThere are two ways to work with subparsers: either by calling the parser as a regular function, and in this case, \nthe subparser must implement the `__call__` method, otherwise help will be printed and the program will exit with\nan error. Or you can directly look at the `.current_subparser` attribute in the parser. The second method seems \nmore complicated, but it becomes less difficult if you use singledispatch from the standard library.\n\n### Using `__call__`\n\nJust implement `__call__` method for subparsers and call\n\n```python\nfrom typing import Optional\n\nimport argclass\n\n\nclass AddressPortGroup(argclass.Group):\n    address: str = \"127.0.0.1\"\n    port: int = 8080\n\n\nclass CommitCommand(argclass.Parser):\n    comment: str = argclass.Argument()\n\n    def __call__(self):\n        endpoint: AddressPortGroup = self.__parent__.endpoint\n        print(\n            \"Commit command called\", self, \n            \"endpoint\", endpoint.address, \"port\", endpoint.port\n        )\n\n\nclass PushCommand(argclass.Parser):\n    comment: str = argclass.Argument()\n\n    def __call__(self):\n        endpoint: AddressPortGroup = self.__parent__.endpoint\n        print(\n            \"Push command called\", self, \n            \"endpoint\", endpoint.address, \"port\", endpoint.port\n        )\n\n\nclass Parser(argclass.Parser):\n    log_level: int = argclass.LogLevel\n    endpoint = AddressPortGroup(title=\"Endpoint options\")\n    commit: Optional[CommitCommand] = CommitCommand()\n    push: Optional[PushCommand] = PushCommand()\n\n\nparser = Parser(\n    config_files=[\"example.ini\", \"~/.example.ini\", \"/etc/example.ini\"],\n    auto_env_var_prefix=\"EXAMPLE_\"\n)\nparser.parse_args()\nparser()\n```\n\n### Using singledispatch\n\nComplex example with subparsers:\n\n```python\nfrom functools import singledispatch\nfrom typing import Optional, Any\n\nimport argclass\n\n\nclass AddressPortGroup(argclass.Group):\n    address: str = argclass.Argument(default=\"127.0.0.1\")\n    port: int\n\n\nclass CommitCommand(argclass.Parser):\n    comment: str = argclass.Argument()\n\n\nclass PushCommand(argclass.Parser):\n    comment: str = argclass.Argument()\n\n\nclass Parser(argclass.Parser):\n    log_level: int = argclass.LogLevel\n    endpoint = AddressPortGroup(\n        title=\"Endpoint options\",\n        defaults=dict(port=8080)\n    )\n    commit: Optional[CommitCommand] = CommitCommand()\n    push: Optional[PushCommand] = PushCommand()\n\n\n@singledispatch\ndef handle_subparser(subparser: Any) -> None:\n    raise NotImplementedError(\n        f\"Unexpected subparser type {subparser.__class__!r}\"\n    )\n\n\n@handle_subparser.register(type(None))\ndef handle_none(_: None) -> None:\n    Parser().print_help()\n    exit(2)\n\n\n@handle_subparser.register(CommitCommand)\ndef handle_commit(subparser: CommitCommand) -> None:\n    print(\"Commit command called\", subparser)\n\n\n@handle_subparser.register(PushCommand)\ndef handle_push(subparser: PushCommand) -> None:\n    print(\"Push command called\", subparser)\n\n\nparser = Parser(\n    config_files=[\"example.ini\", \"~/.example.ini\", \"/etc/example.ini\"],\n    auto_env_var_prefix=\"EXAMPLE_\"\n)\nparser.parse_args()\nhandle_subparser(parser.current_subparser)\n```\n\n## Value conversion\n\nIf the argument has a generic or composite type, then you must explicitly\ndescribe it using ``argclass.Argument``, while specifying the converter\nfunction with ``type`` or ``converter`` argument to transform the value\nafter parsing the arguments.\n\nThe exception to this rule is `Optional` with a single type. In this case,\nan argument without a default value will not be required,\nand its value can be None.\n\n<!-- name: test_converter -->\n```python\nimport argclass\nfrom typing import Optional, Union\n\ndef converter(value: str) -> Optional[Union[int, str, bool]]:\n    if value.lower() == \"none\":\n        return None\n    if value.isdigit():\n        return int(value)\n    if value.lower() in (\"yes\", \"true\", \"enabled\", \"enable\", \"on\"):\n        return True\n    return False\n\n\nclass Parser(argclass.Parser):\n    gizmo: Optional[Union[int, str, bool]] = argclass.Argument(\n        converter=converter\n    )\n    optional: Optional[int]\n\n\nparser = Parser()\n\nparser.parse_args([\"--gizmo=65535\"])\nassert parser.gizmo == 65535\n\nparser.parse_args([\"--gizmo=None\"])\nassert parser.gizmo is None\n\nparser.parse_args([\"--gizmo=on\"])\nassert parser.gizmo is True\nassert parser.optional is None\n\nparser.parse_args([\"--gizmo=off\", \"--optional=10\"])\nassert parser.gizmo is False\nassert parser.optional == 10\n```\n",
    "bugtrack_url": null,
    "license": "Apache 2",
    "summary": "A wrapper around the standard argparse module that allows you to describe argument parsers declaratively",
    "version": "1.0.3",
    "project_urls": {
        "Documentation": "https://github.com/mosquito/argclass/blob/master/README.md",
        "Homepage": "https://github.com/mosquito/argclass",
        "Source": "https://github.com/mosquito/argclass",
        "Tracker": "https://github.com/mosquito/argclass/issues"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "30637d2ad89f94fdc9e6b5e2cebd1176b6602eae8df8c12fe0ca5c493d4cbac3",
                "md5": "fb1e91fb22526ffcdf08ac5c642f68d0",
                "sha256": "ce8615556c1fdf76675f627e6347036d10121b4a594ffeff558d4ea29be96bc2"
            },
            "downloads": -1,
            "filename": "argclass-1.0.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "fb1e91fb22526ffcdf08ac5c642f68d0",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.8",
            "size": 16628,
            "upload_time": "2024-04-17T10:32:39",
            "upload_time_iso_8601": "2024-04-17T10:32:39.341879Z",
            "url": "https://files.pythonhosted.org/packages/30/63/7d2ad89f94fdc9e6b5e2cebd1176b6602eae8df8c12fe0ca5c493d4cbac3/argclass-1.0.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "6a91168fd776db1b7c9758aa353c9258162df4a88173cab804db4825928e213e",
                "md5": "9189c542c2dbba6dec1a65a2f1bac5b1",
                "sha256": "edff35d9f0a7fd2f43433a34976c60c2abdfd868c875106f1746dadf2acee24e"
            },
            "downloads": -1,
            "filename": "argclass-1.0.3.tar.gz",
            "has_sig": false,
            "md5_digest": "9189c542c2dbba6dec1a65a2f1bac5b1",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.8",
            "size": 15531,
            "upload_time": "2024-04-17T10:32:43",
            "upload_time_iso_8601": "2024-04-17T10:32:43.668971Z",
            "url": "https://files.pythonhosted.org/packages/6a/91/168fd776db1b7c9758aa353c9258162df4a88173cab804db4825928e213e/argclass-1.0.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-17 10:32:43",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "mosquito",
    "github_project": "argclass",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "lcname": "argclass"
}
        
Elapsed time: 0.26766s