argclass


Nameargclass JSON
Version 1.1.0 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-10-22 16:32:27
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 imperatively, which is not convenient for type checking and attribute access. Additionally, IDE autocompletion and type hints are not applicable in this case.

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

## Quick Start

<!--- name: test_simple_example --->
```python
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 shows basic module usage. When you want to specify argument defaults and other options, you have to use `argclass.Argument`.

## Subparsers

The following example shows how to use subparsers:

```python
import argclass

class SubCommand(argclass.Parser):
    comment: str

    def __call__(self) -> int:
        endpoint: str = self.__parent__.endpoint
        print("Subcommand called", self, "endpoint", endpoint)
        return 0

class Parser(argclass.Parser):
    endpoint: str
    subcommand = SubCommand()

if __name__ == '__main__':
    parser = Parser()
    parser.parse_args()
    exit(parser())
```

The `__call__` method will be called when the subparser is used. Otherwise, help will be printed.

## Value Conversion

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

The main differences between `type` and `converter` are:

* `type` will be directly passed to the `argparse.ArgumentParser.add_argument` method.
* The `converter` function will be called after parsing the argument.

<!--- name: test_converter_example --->
```python
import uuid
import argclass

def string_uid(value: str) -> uuid.UUID:
    return uuid.uuid5(uuid.NAMESPACE_OID, value)

class Parser(argclass.Parser):
    strid1: uuid.UUID = argclass.Argument(converter=string_uid)
    strid2: uuid.UUID = argclass.Argument(type=string_uid)

parser = Parser()
parser.parse_args(["--strid1=hello", "--strid2=world"])
assert parser.strid1 == uuid.uuid5(uuid.NAMESPACE_OID, 'hello')
assert parser.strid2 == uuid.uuid5(uuid.NAMESPACE_OID, 'world')
```

As you can see, the `string_uid` function is called in both cases, but `converter` is applied after parsing the argument.

The following example shows how `type` is applied to each item in a list when using `nargs`:

<!--- name: test_list_converter_example --->
```python
import argclass

class Parser(argclass.Parser):
    numbers = argclass.Argument(nargs=argclass.Nargs.ONE_OR_MORE, type=int)

parser = Parser()
parser.parse_args(["--numbers", "1", "2", "3"])
assert parser.numbers == [1, 2, 3]
```

`type` will be applied to each item in the list of arguments.

If you want to convert a list of strings to a list of integers and then to a `frozenset`, you can use the following example:

<!--- name: test_list_converter_frozenset_example --->
```python
import argclass

class Parser(argclass.Parser):
    numbers = argclass.Argument(
        nargs=argclass.Nargs.ONE_OR_MORE, type=int, converter=frozenset
    )

parser = Parser()
parser.parse_args(["--numbers", "1", "2", "3"])
assert parser.numbers == frozenset([1, 2, 3])
```

## Configuration Files

Parser objects can get default values from environment variables or from specified configuration files.

<!--- name: test_config_example --->
```python
import logging
from pathlib import Path
from tempfile import TemporaryDirectory
import argclass

class Parser(argclass.Parser):
    log_level: int = argclass.LogLevel
    address: str
    port: int

with TemporaryDirectory() as tmpdir:
    tmp = Path(tmpdir)
    with open(tmp / "config.ini", "w") as fp:
        fp.write(
            "[DEFAULT]\n"
            "log_level=info\n"
            "address=localhost\n"
            "port=8080\n"
        )

    parser = Parser(config_files=[tmp / "config.ini"])
    parser.parse_args([])
    assert parser.log_level == logging.INFO
    assert parser.address == "localhost"
    assert parser.port == 8080
```

When using configuration files, argclass uses Python's `ast.literal_eval` for parsing arguments with `nargs` and 
complex types. This means that in your INI configuration files, you should write values in a syntax that `literal_eval`
can parse for these specific arguments. 

For regular arguments (simple types like strings, integers, booleans), you can write the values as-is.

## Argument Groups

The following example uses `argclass.Argument` and argument groups:

<!-- name: test_argument_groups_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('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
```

Argument groups are sections in the parser configuration. For example, in this case, the configuration file might be:

```ini
[DEFAULT]
log_level=info
user_id=[1, 2, 3]

[http]
port=9001

[rpc]
port=9002
```

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 be based on the following configuration files ['example.ini',
'~/.example.ini', '/etc/example.ini']. Now 1 file has been applied
['example.ini']. The configuration files are INI-formatted files where
configuration groups are INI sections.
See more https://pypi.org/project/argclass/#configs
```

## Secrets

Arguments that contain sensitive data, such as tokens, encryption keys, or 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()
```

### Preventing Secrets from Being Logged

A secret is not actually a string, but a special class inherited from `str`. All attempts to cast this type to a `str` (using the `__str__` method) will return the original value, unless the `__str__` method is called from the `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 absolute sensitive data protection, but it helps prevent accidental logging of these values.

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

## Enum Argument

The library provides a special argument type for working with enumerations. For enum arguments, the `choices` parameter will be generated automatically from the enum names. After parsing the argument, the value will be converted to the enum member.

<!--- name: test_enum_example --->
```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 a base class for writing custom configuration parsers.

`argclass.Config` is a special argument type for parsing configuration files. The optional parameter `config_class` is used to specify the custom configuration parser. By default, it is an INI parser.

### YAML Parser

To parse YAML files, you need to install the `PyYAML` package. Follow code is an implementation of a YAML config 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(fp, Loader=yaml.FullLoader)

class YAMLConfigArgument(argclass.ConfigArgument):
    action = YAMLConfigAction

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

### TOML Parser

To parse TOML files, you need to install the `tomli` package. Follow code is an implementation of a TOML config 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("rb") 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 Advanced Usage

There are two ways to work with subparsers: either by calling the parser as a regular function, in which case the
subparser must implement the `__call__` method (otherwise help will be printed and the program will exit with an
error), or by directly inspecting the `.current_subparser` attribute in the parser. The second method can be 
simplified using `functools.singledispatch`.

### Using `__call__`

Just implement the `__call__` method for subparsers and call the main parser.

```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) -> int:
        endpoint: AddressPortGroup = self.__parent__.endpoint
        print(
            "Commit command called", self,
            "endpoint", endpoint.address, "port", endpoint.port
        )
        return 0

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

    def __call__(self) -> int:
        endpoint: AddressPortGroup = self.__parent__.endpoint
        print(
            "Push command called", self,
            "endpoint", endpoint.address, "port", endpoint.port
        )
        return 0

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

if __name__ == '__main__':
    parser = Parser(
        config_files=["example.ini", "~/.example.ini", "/etc/example.ini"],
        auto_env_var_prefix="EXAMPLE_"
    )
    parser.parse_args()
    exit(parser())
```

### Using `singledispatch`

You can use the `current_subparser` attribute to get the current subparser and then call it. This does not require implementing the `__call__` method.

```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)

if __name__ == '__main__':
    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 with Optional and Union Types

If an argument has a generic or composite type, you must explicitly describe it using `argclass.Argument`, specifying 
the converter function with `type` or `converter` to transform the value after parsing. 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_optional_union_example --->
```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/0c/40/463b9ccbffaab7793f9e894a0bece2cd7e99323b8ee17a8c46d918efcd9e/argclass-1.1.0.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 argument parsers declaratively.\n\nBy default, the `argparse` module suggests creating parsers imperatively, which is not convenient for type checking and attribute access. Additionally, IDE autocompletion and type hints are not applicable in this case.\n\nThis module allows you to declare command-line parsers using classes.\n\n## Quick Start\n\n<!--- name: test_simple_example --->\n```python\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```\n\nAs you can see, this example shows basic module usage. When you want to specify argument defaults and other options, you have to use `argclass.Argument`.\n\n## Subparsers\n\nThe following example shows how to use subparsers:\n\n```python\nimport argclass\n\nclass SubCommand(argclass.Parser):\n    comment: str\n\n    def __call__(self) -> int:\n        endpoint: str = self.__parent__.endpoint\n        print(\"Subcommand called\", self, \"endpoint\", endpoint)\n        return 0\n\nclass Parser(argclass.Parser):\n    endpoint: str\n    subcommand = SubCommand()\n\nif __name__ == '__main__':\n    parser = Parser()\n    parser.parse_args()\n    exit(parser())\n```\n\nThe `__call__` method will be called when the subparser is used. Otherwise, help will be printed.\n\n## Value Conversion\n\nIf an argument has a generic or composite type, you must explicitly describe it using `argclass.Argument`, specifying a converter function with the `type` or `converter` argument to transform the value after parsing.\n\nThe main differences between `type` and `converter` are:\n\n* `type` will be directly passed to the `argparse.ArgumentParser.add_argument` method.\n* The `converter` function will be called after parsing the argument.\n\n<!--- name: test_converter_example --->\n```python\nimport uuid\nimport argclass\n\ndef string_uid(value: str) -> uuid.UUID:\n    return uuid.uuid5(uuid.NAMESPACE_OID, value)\n\nclass Parser(argclass.Parser):\n    strid1: uuid.UUID = argclass.Argument(converter=string_uid)\n    strid2: uuid.UUID = argclass.Argument(type=string_uid)\n\nparser = Parser()\nparser.parse_args([\"--strid1=hello\", \"--strid2=world\"])\nassert parser.strid1 == uuid.uuid5(uuid.NAMESPACE_OID, 'hello')\nassert parser.strid2 == uuid.uuid5(uuid.NAMESPACE_OID, 'world')\n```\n\nAs you can see, the `string_uid` function is called in both cases, but `converter` is applied after parsing the argument.\n\nThe following example shows how `type` is applied to each item in a list when using `nargs`:\n\n<!--- name: test_list_converter_example --->\n```python\nimport argclass\n\nclass Parser(argclass.Parser):\n    numbers = argclass.Argument(nargs=argclass.Nargs.ONE_OR_MORE, type=int)\n\nparser = Parser()\nparser.parse_args([\"--numbers\", \"1\", \"2\", \"3\"])\nassert parser.numbers == [1, 2, 3]\n```\n\n`type` will be applied to each item in the list of arguments.\n\nIf you want to convert a list of strings to a list of integers and then to a `frozenset`, you can use the following example:\n\n<!--- name: test_list_converter_frozenset_example --->\n```python\nimport argclass\n\nclass Parser(argclass.Parser):\n    numbers = argclass.Argument(\n        nargs=argclass.Nargs.ONE_OR_MORE, type=int, converter=frozenset\n    )\n\nparser = Parser()\nparser.parse_args([\"--numbers\", \"1\", \"2\", \"3\"])\nassert parser.numbers == frozenset([1, 2, 3])\n```\n\n## Configuration Files\n\nParser objects can get default values from environment variables or from specified configuration files.\n\n<!--- name: test_config_example --->\n```python\nimport logging\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nimport argclass\n\nclass Parser(argclass.Parser):\n    log_level: int = argclass.LogLevel\n    address: str\n    port: int\n\nwith TemporaryDirectory() as tmpdir:\n    tmp = Path(tmpdir)\n    with open(tmp / \"config.ini\", \"w\") as fp:\n        fp.write(\n            \"[DEFAULT]\\n\"\n            \"log_level=info\\n\"\n            \"address=localhost\\n\"\n            \"port=8080\\n\"\n        )\n\n    parser = Parser(config_files=[tmp / \"config.ini\"])\n    parser.parse_args([])\n    assert parser.log_level == logging.INFO\n    assert parser.address == \"localhost\"\n    assert parser.port == 8080\n```\n\nWhen using configuration files, argclass uses Python's `ast.literal_eval` for parsing arguments with `nargs` and \ncomplex types. This means that in your INI configuration files, you should write values in a syntax that `literal_eval`\ncan parse for these specific arguments. \n\nFor regular arguments (simple types like strings, integers, booleans), you can write the values as-is.\n\n## Argument Groups\n\nThe following example uses `argclass.Argument` and argument groups:\n\n<!-- name: test_argument_groups_example -->\n```python\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('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\nArgument groups are sections in the parser configuration. For example, in this case, the configuration file might be:\n\n```ini\n[DEFAULT]\nlog_level=info\nuser_id=[1, 2, 3]\n\n[http]\nport=9001\n\n[rpc]\nport=9002\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 be based on the following configuration files ['example.ini',\n'~/.example.ini', '/etc/example.ini']. Now 1 file has been applied\n['example.ini']. The configuration files are INI-formatted files where\nconfiguration groups are INI sections.\nSee more https://pypi.org/project/argclass/#configs\n```\n\n## Secrets\n\nArguments that contain sensitive data, such as tokens, encryption keys, or 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`.\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### Preventing Secrets from Being Logged\n\nA secret is not actually a string, but a special class inherited from `str`. All attempts to cast this type to a `str` (using the `__str__` method) will return the original value, unless the `__str__` method is called from the `logging` module.\n\n```python\nimport logging\nfrom argclass import SecretString\n\nlogging.basicConfig(level=logging.INFO)\ns = SecretString(\"my-secret-password\")\nlogging.info(s)          # __str__ will be called from logging\nlogging.info(f\"s=%s\", s) # __str__ will be called from logging too\nlogging.info(f\"{s!r}\")   # repr is safe\nlogging.info(f\"{s}\")     # the password will be compromised\n```\n\nOf course, this is not absolute sensitive data protection, but it helps prevent accidental logging of these values.\n\nThe `repr` for this will always give a placeholder, so it is better to always add `!r` to any f-string, for example `f'{value!r}'`.\n\n## Enum Argument\n\nThe library provides a special argument type for working with enumerations. For enum arguments, the `choices` parameter will be generated automatically from the enum names. After parsing the argument, the value will be converted to the enum member.\n\n<!--- name: test_enum_example --->\n```python\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\nclass Parser(argclass.Parser):\n    \"\"\"Log level with default\"\"\"\n    log_level = argclass.EnumArgument(LogLevelEnum, default=\"info\")\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 a base class for writing custom configuration parsers.\n\n`argclass.Config` is a special argument type for parsing configuration files. The optional parameter `config_class` is used to specify the custom configuration parser. By default, it is an INI parser.\n\n### YAML Parser\n\nTo parse YAML files, you need to install the `PyYAML` package. Follow code is an implementation of a YAML config parser.\n\n```python\nfrom typing import Mapping, Any\nfrom pathlib import Path\nimport argclass\nimport yaml\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(fp, Loader=yaml.FullLoader)\n\nclass YAMLConfigArgument(argclass.ConfigArgument):\n    action = YAMLConfigAction\n\nclass Parser(argclass.Parser):\n    config = argclass.Config(\n        required=True,\n        config_class=YAMLConfigArgument,\n    )\n```\n\n### TOML Parser\n\nTo parse TOML files, you need to install the `tomli` package. Follow code is an implementation of a TOML config parser.\n\n```python\nimport tomli\nimport argclass\nfrom pathlib import Path\nfrom typing import Mapping, Any\n\nclass TOMLConfigAction(argclass.ConfigAction):\n    def parse_file(self, file: Path) -> Mapping[str, Any]:\n        with file.open(\"rb\") as fp:\n            return tomli.load(fp)\n\nclass TOMLConfigArgument(argclass.ConfigArgument):\n    action = TOMLConfigAction\n\nclass Parser(argclass.Parser):\n    config = argclass.Config(\n        required=True,\n        config_class=TOMLConfigArgument,\n    )\n```\n\n## Subparsers Advanced Usage\n\nThere are two ways to work with subparsers: either by calling the parser as a regular function, in which case the\nsubparser must implement the `__call__` method (otherwise help will be printed and the program will exit with an\nerror), or by directly inspecting the `.current_subparser` attribute in the parser. The second method can be \nsimplified using `functools.singledispatch`.\n\n### Using `__call__`\n\nJust implement the `__call__` method for subparsers and call the main parser.\n\n```python\nfrom typing import Optional\nimport argclass\n\nclass AddressPortGroup(argclass.Group):\n    address: str = \"127.0.0.1\"\n    port: int = 8080\n\nclass CommitCommand(argclass.Parser):\n    comment: str = argclass.Argument()\n\n    def __call__(self) -> int:\n        endpoint: AddressPortGroup = self.__parent__.endpoint\n        print(\n            \"Commit command called\", self,\n            \"endpoint\", endpoint.address, \"port\", endpoint.port\n        )\n        return 0\n\nclass PushCommand(argclass.Parser):\n    comment: str = argclass.Argument()\n\n    def __call__(self) -> int:\n        endpoint: AddressPortGroup = self.__parent__.endpoint\n        print(\n            \"Push command called\", self,\n            \"endpoint\", endpoint.address, \"port\", endpoint.port\n        )\n        return 0\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\nif __name__ == '__main__':\n    parser = Parser(\n        config_files=[\"example.ini\", \"~/.example.ini\", \"/etc/example.ini\"],\n        auto_env_var_prefix=\"EXAMPLE_\"\n    )\n    parser.parse_args()\n    exit(parser())\n```\n\n### Using `singledispatch`\n\nYou can use the `current_subparser` attribute to get the current subparser and then call it. This does not require implementing the `__call__` method.\n\n```python\nfrom functools import singledispatch\nfrom typing import Optional, Any\nimport argclass\n\nclass AddressPortGroup(argclass.Group):\n    address: str = argclass.Argument(default=\"127.0.0.1\")\n    port: int\n\nclass CommitCommand(argclass.Parser):\n    comment: str = argclass.Argument()\n\nclass PushCommand(argclass.Parser):\n    comment: str = argclass.Argument()\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@singledispatch\ndef handle_subparser(subparser: Any) -> None:\n    raise NotImplementedError(\n        f\"Unexpected subparser type {subparser.__class__!r}\"\n    )\n\n@handle_subparser.register(type(None))\ndef handle_none(_: None) -> None:\n    Parser().print_help()\n    exit(2)\n\n@handle_subparser.register(CommitCommand)\ndef handle_commit(subparser: CommitCommand) -> None:\n    print(\"Commit command called\", subparser)\n\n@handle_subparser.register(PushCommand)\ndef handle_push(subparser: PushCommand) -> None:\n    print(\"Push command called\", subparser)\n\nif __name__ == '__main__':\n    parser = Parser(\n        config_files=[\"example.ini\", \"~/.example.ini\", \"/etc/example.ini\"],\n        auto_env_var_prefix=\"EXAMPLE_\"\n    )\n    parser.parse_args()\n    handle_subparser(parser.current_subparser)\n```\n\n## Value Conversion with Optional and Union Types\n\nIf an argument has a generic or composite type, you must explicitly describe it using `argclass.Argument`, specifying \nthe converter function with `type` or `converter` to transform the value after parsing. The exception to this rule \nis `Optional` with a single type. In this case, an argument without a default value will not be required, and \nits value can be `None`.\n\n<!--- name: test_optional_union_example --->\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\nclass Parser(argclass.Parser):\n    gizmo: Optional[Union[int, str, bool]] = argclass.Argument(\n        converter=converter\n    )\n    optional: Optional[int]\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.1.0",
    "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": "430813cd5d3e75dddd6a6869cbbf0ecd1e5ff9aada4bb3ccfe670d6c6e8825ea",
                "md5": "55b76e9d64c48df6516c44ebba9e0c7a",
                "sha256": "6169f275670125851edec25387d87a23504ba55fc0576d65de343cbd7a75b67b"
            },
            "downloads": -1,
            "filename": "argclass-1.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "55b76e9d64c48df6516c44ebba9e0c7a",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.8",
            "size": 18295,
            "upload_time": "2024-10-22T16:32:25",
            "upload_time_iso_8601": "2024-10-22T16:32:25.507480Z",
            "url": "https://files.pythonhosted.org/packages/43/08/13cd5d3e75dddd6a6869cbbf0ecd1e5ff9aada4bb3ccfe670d6c6e8825ea/argclass-1.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0c40463b9ccbffaab7793f9e894a0bece2cd7e99323b8ee17a8c46d918efcd9e",
                "md5": "ec7dccb0c7b906560e8aedac25bf530e",
                "sha256": "655964e13f1fc1b6a8837cdd69abe6345230afbd3316bc5f78ab0fcb5623238e"
            },
            "downloads": -1,
            "filename": "argclass-1.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "ec7dccb0c7b906560e8aedac25bf530e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.8",
            "size": 21469,
            "upload_time": "2024-10-22T16:32:27",
            "upload_time_iso_8601": "2024-10-22T16:32:27.572220Z",
            "url": "https://files.pythonhosted.org/packages/0c/40/463b9ccbffaab7793f9e894a0bece2cd7e99323b8ee17a8c46d918efcd9e/argclass-1.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-10-22 16:32:27",
    "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.41758s