标准argparse模块的包装器,允许您声明式地描述参数解析器
项目描述
argclass
标准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
明确描述它,同时使用 type
或 converter
参数指定转换函数以在解析参数后转换值。
此规则的例外是具有单个类型的 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 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | edff35d9f0a7fd2f43433a34976c60c2abdfd868c875106f1746dadf2acee24e |
|
MD5 | 9189c542c2dbba6dec1a65a2f1bac5b1 |
|
BLAKE2b-256 | 6a91168fd776db1b7c9758aa353c9258162df4a88173cab804db4825928e213e |