Skip to main content

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

Project description

https://coveralls.io/repos/github/mosquito/argclass/badge.svg?branch=master Actions Latest Version https://img.shields.io/pypi/pyversions/argclass.svg https://img.shields.io/pypi/l/argclass.svg

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:

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:

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:

$ 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:

$ 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-hypernode.com/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.

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.

>>> 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.

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 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:

[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

Subparsers

Complex example with subparsers:

import logging
from functools import singledispatch
from pathlib import Path
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.

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

argclass-0.11.0.tar.gz (13.5 kB view details)

Uploaded Source

Built Distribution

argclass-0.11.0-py3-none-any.whl (15.3 kB view details)

Uploaded Python 3

File details

Details for the file argclass-0.11.0.tar.gz.

File metadata

  • Download URL: argclass-0.11.0.tar.gz
  • Upload date:
  • Size: 13.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.4.0 CPython/3.10.2 Darwin/22.3.0

File hashes

Hashes for argclass-0.11.0.tar.gz
Algorithm Hash digest
SHA256 933cf68d54133eca9ab02f3b24e4dee5a2ddcbcce1c76fca09b076ebfae277ab
MD5 8dfa25cde2f2bb4d365d540dcf4c77a8
BLAKE2b-256 4628515977af9ac60fa158c3f47796503cfc5e3949ec18803d22e1be680e83f1

See more details on using hashes here.

File details

Details for the file argclass-0.11.0-py3-none-any.whl.

File metadata

  • Download URL: argclass-0.11.0-py3-none-any.whl
  • Upload date:
  • Size: 15.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.4.0 CPython/3.10.2 Darwin/22.3.0

File hashes

Hashes for argclass-0.11.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a6e59cef99b7b6cd052c6af633899813a31785260b2ac50ce89e7f6c0798036f
MD5 939ad2b3cd9e59b9cd32d1e47531e58e
BLAKE2b-256 1c1a46bb9ac8eeb4ba264a27a0f2eaa405993f7cd4da39cfaf0fdfa1c935c606

See more details on using hashes here.

Supported by

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