Package: argclass
A wrapper around the standardargparsemodule that allows you to describe
argument parsers declaratively.By default, theargparsemodule 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:importloggingimportargclassclassCopyParser(argclass.Parser):recursive:boolpreserve_attributes:boolparser=CopyParser()parser.parse_args(["--recursive","--preserve-attributes"])assertparser.recursiveassertparser.preserve_attributesAs you can see this example shown a basic module usage, when you want specify
argument default and other options you have to useargclass.Argument.Following example useargclass.Argumentand argument groups:fromtypingimportFrozenSetimportloggingimportargclassclassAddressPortGroup(argclass.Group):address:str=argclass.Argument(default="127.0.0.1")port:intclassParser(argclass.Parser):log_level:int=argclass.LogLevelhttp=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.environparser.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)assertparser.http.address=='127.0.0.1'assertparser.rpc.address=='127.0.0.1'assertparser.http.port==8080assertparser.rpc.port==9090Run this script:$ python example.py
INFO:root:Listening http://127.0.0.1:8080
INFO:root:Listening rpc://127.0.0.1:9090Example of--helpoutput:$ 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/#configsSecretsArguments 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 thesecret=Trueparameter,
or use the special default constructorargclass.Secretinstead ofargclass.Argument.importargclassclassHttpAuthentication(argclass.Group):username:str=argclass.Argument()password:str=argclass.Secret()classHttpBearerAuthentication(argclass.Group):token:str=argclass.Argument(secret=True)classParser(argclass.Parser):http_basic=HttpAuthentication()http_bearer=HttpBearerAuthentication()parser=Parser()parser.print_help()Trying to protect data from being written to the logA secret is not actually a string, but a special class inherited
from astr, and all attempts to cast this type to astr(using__str__method) should be fine, and returning the original
value, unless the__str__method call is from aloggingmodule.>>>importlogging>>>fromargclassimportSecretString>>>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 compromisedOf 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!rfor any f-string, for examplef’{value!r}’.ConfigsThe parser objects might be get default values from environment variables or
one of passed configuration files.classAddressPortGroup(argclass.Group):address:str=argclass.Argument(default="127.0.0.1")port:intclassParser(argclass.Parser):spam:strquantity:intlog_level:int=argclass.LogLevelhttp=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=infospam=eggquantity=100user_ids=[1, 2, 3][http]address=127.0.0.1port=8080[rpc]address=127.0.0.1port=9090Enum argumentimportenumimportloggingimportargclassclassLogLevelEnum(enum.IntEnum):debug=logging.DEBUGinfo=logging.INFOwarning=logging.WARNINGerror=logging.ERRORcritical=logging.CRITICALclassParser(argclass.Parser):"""Log level with default"""log_level=argclass.EnumArgument(LogLevelEnum,default="info")classParserLogLevelIsRequired(argclass.Parser):log_level:LogLevelEnumparser=Parser()parser.parse_args([])assertparser.log_level==logging.INFOparser=Parser()parser.parse_args(["--log-level=error"])assertparser.log_level==logging.ERRORparser=ParserLogLevelIsRequired()parser.parse_args(["--log-level=warning"])assertparser.log_level==logging.WARNINGConfig ActionThis library provides base class for writing custom configuration parsers.YAML parserimportyamlimportargclassclassYAMLConfigAction(argclass.ConfigAction):defparse_file(self,file:Path)->Mapping[str,Any]:withfile.open("r")asfp:returnyaml.load_all(fp)classYAMLConfigArgument(argclass.ConfigArgument):action=YAMLConfigActionclassParser(argclass.Parser):config=argclass.Config(required=True,config_class=YAMLConfigArgument,)TOML parserimporttomliimportargclassclassTOMLConfigAction(argclass.ConfigAction):defparse_file(self,file:Path)->Mapping[str,Any]:withfile.open("r")asfp:returntomli.load(fp)classTOMLConfigArgument(argclass.ConfigArgument):action=TOMLConfigActionclassParser(argclass.Parser):config=argclass.Config(required=True,config_class=TOMLConfigArgument,)SubparsersComplex example with subparsers:importloggingfromfunctoolsimportsingledispatchfrompathlibimportPathfromtypingimportOptional,AnyimportargclassclassAddressPortGroup(argclass.Group):address:str=argclass.Argument(default="127.0.0.1")port:intclassCommitCommand(argclass.Parser):comment:str=argclass.Argument()classPushCommand(argclass.Parser):comment:str=argclass.Argument()classParser(argclass.Parser):log_level:int=argclass.LogLevelendpoint=AddressPortGroup(title="Endpoint options",defaults=dict(port=8080))commit:Optional[CommitCommand]=CommitCommand()push:Optional[PushCommand]=PushCommand()@singledispatchdefhandle_subparser(subparser:Any)->None:raiseNotImplementedError(f"Unexpected subparser type{subparser.__class__!r}")@handle_subparser.register(type(None))defhandle_none(_:None)->None:Parser().print_help()exit(2)@handle_subparser.register(CommitCommand)defhandle_commit(subparser:CommitCommand)->None:print("Commit command called",subparser)@handle_subparser.register(PushCommand)defhandle_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 conversionIf the argument has a generic or composite type, then you must explicitly
describe it usingargclass.Argument, while specifying the converter
function withtypeorconverterargument to transform the value
after parsing the arguments.The exception to this rule isOptionalwith a single type. In this case,
an argument without a default value will not be required,
and its value can be None.importargclassfromtypingimportOptional,Uniondefconverter(value:str)->Optional[Union[int,str,bool]]:ifvalue.lower()=="none":returnNoneifvalue.isdigit():returnint(value)ifvalue.lower()in("yes","true","enabled","enable","on"):returnTruereturnFalseclassParser(argclass.Parser):gizmo:Optional[Union[int,str,bool]]=argclass.Argument(converter=converter)optional:Optional[int]parser=Parser()parser.parse_args(["--gizmo=65535"])assertparser.gizmo==65535parser.parse_args(["--gizmo=None"])assertparser.gizmoisNoneparser.parse_args(["--gizmo=on"])assertparser.gizmoisTrueassertparser.optionalisNoneparser.parse_args(["--gizmo=off","--optional=10"])assertparser.gizmoisFalseassertparser.optional==10