跳转到主要内容

标准argparse模块的包装器,允许您声明式地描述参数解析器

项目描述

argclass

Coverage Actions Latest Version Python Versions License

标准argparse模块的包装器,允许您声明式地描述参数解析器。

默认情况下,argparse模块建议创建命令行解析器,这不是从类型检查和属性访问的角度来看很方便的,当然,在这个情况下,IDE自动完成和类型提示也不适用。

此模块允许您使用类声明命令行解析器。

简单示例

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

如您所见,此示例展示了基本模块的使用,当您想指定参数默认值和其他选项时,您必须使用argclass.Argument

以下示例使用argclass.Argument和参数组

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

运行此脚本

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

--help输出的示例

$ 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.ac.cn/project/argclass/#configs

秘密

反映某些敏感数据、令牌或加密密钥、带密码的URL等参数,在通过环境变量或配置文件传递时,可以在--help的输出中打印。要隐藏默认值,请添加secret=True参数,或使用特殊的默认构造函数argclass.Secret代替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()

尝试保护数据不被写入日志

秘密实际上不是一个字符串,而是一个从 str 继承的特殊类,所有尝试将此类型转换为 str(使用 __str__ 方法)都应该正常,并返回原始值,除非 __str__ 方法的调用来自 logging 模块。

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

当然,这并不是绝对敏感数据保护,但我希望它能帮助防止意外记录此类值。

此 repr 将始终返回占位符,因此最好总是为任何 f-string 添加 !r,例如 f'{value!r}'

配置

解析器对象可能会从环境变量或传递的配置文件之一获取默认值。

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

在这种情况下,将打开每个传递的和现有的配置文件。

根级别参数可能在 [DEFAULT] 部分中描述。

其他参数可能在组特定部分中描述。

因此,上面示例的完整配置文件如下

[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

枚举参数

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

配置操作

此库为编写自定义配置解析器提供基类。

YAML 解析器

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 解析器

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

子解析器

使用子解析器有两种方式:要么将其作为常规函数调用,在这种情况下,子解析器必须实现 __call__ 方法,否则将打印帮助并程序以错误退出。或者,您可以直接查看解析器的 .current_subparser 属性。第二种方法似乎更复杂,但如果你使用标准库中的 singledispatch,它会变得更容易。

使用 __call__

只需为子解析器实现 __call__ 方法并调用即可

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

使用 singledispatch

子解析器的复杂示例

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)

值转换

如果参数具有通用或复合类型,则必须使用 argclass.Argument 明确描述它,同时使用 typeconverter 参数指定转换函数以在解析参数后转换值。

此规则的例外是具有单个类型的 Optional。在这种情况下,没有默认值的参数不是必需的,其值可以是 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

项目详情


下载文件

下载您平台的文件。如果您不确定选择哪一个,请了解更多关于 安装包 的信息。

源分布

argclass-1.0.3.tar.gz (15.5 kB 查看哈希值)

上传时间

构建分布

argclass-1.0.3-py3-none-any.whl (16.6 kB 查看哈希值)

上传时间 Python 3

支持者