Package: argtyped
argtyped: Command Line Argument Parser, with Typesargtypedis an command line argument parser with that relies on type annotations. It is built onargparse, the command line argument parser library built into
Python. Compared withargparse, this library gives you:More concise and intuitive syntax, less boilerplate code.Type checking and IDE auto-completion for command line arguments.A drop-in replacement forargparsein most cases.Since v0.4.0,argtypedalso supports parsing arguments defined with anattrs-class. SeeAttrs Supportfor more details.InstallationInstall stable release fromPyPI:pipinstallargtypedOr, install the latest commit from GitHub:pipinstallgit+https://github.com/huzecong/argtyped.gitUsageWithargtyped, you can define command line arguments in a syntax similar totyping.NamedTuple. The syntax is intuitive and can
be illustrated with an example:fromtypingimportOptionalfromtyping_extensionsimportLiteral# or directly import from `typing` in Python 3.8+fromargtypedimportArguments,SwitchfromargtypedimportEnum,autoclassLoggingLevels(Enum):Debug=auto()Info=auto()Warning=auto()Error=auto()Critical=auto()classMyArguments(Arguments):model_name:str# required argument of `str` typehidden_size:int=512# `int` argument with default value of 512activation:Literal["relu","tanh","sigmoid"]="relu"# argument with limited choiceslogging_level:LoggingLevels=LoggingLevels.Info# using `Enum` class as choicesuse_dropout:Switch=True# switch argument, enable with "--use-dropout" and disable with "--no-use-dropout"dropout_prob:Optional[float]=0.5# optional argument, "--dropout-prob=none" parses into `None`args=MyArguments()This is equivalent to the following code with Python built-inargparse:importargparsefromenumimportEnumclassLoggingLevels(Enum):Debug="debug"Info="info"Warning="warning"Error="error"Critical="critical"parser=argparse.ArgumentParser()parser.add_argument("--model-name",type=str,required=True)parser.add_argument("--hidden-size",type=int,default=512)parser.add_argument("--activation",choices=["relu","tanh","sigmoid"],default="relu")parser.add_argument("--logging-level",choices=list(LoggingLevels),type=LoggingLevels,default="info")parser.add_argument("--use-dropout",action="store_true",dest="use_dropout",default=True)parser.add_argument("--no-use-dropout",action="store_false",dest="use_dropout")parser.add_argument("--dropout-prob",type=lambdas:Noneifs.lower()=='none'elsefloat(s),default=0.5)args=parser.parse_args()Save the code into a file namedmain.py. Suppose the following arguments are provided:pythonmain.py\--model-nameLSTM\--activationsigmoid\--logging-leveldebug\--no-use-dropout\--dropout-probnoneThen the parsed arguments will be equivalent to the following structure returned byargparse:argparse.Namespace(model_name="LSTM",hidden_size=512,activation="sigmoid",logging_level="debug",use_dropout=False,dropout_prob=None)Arguments can also be pretty-printed:>>> print(args)
<class '__main__.MyArguments'>
╔═════════════════╤══════════════════════════════════╗
║ Arguments       │ Values                           ║
╠═════════════════╪══════════════════════════════════╣
║ model_name      │ 'LSTM'                           ║
║ hidden_size     │ 512                              ║
║ activation      │ 'sigmoid'                        ║
║ logging_level   │ <MyLoggingLevels.Debug: 'debug'> ║
║ use_dropout     │ False                            ║
║ dropout_prob    │ None                             ║
║ label_smoothing │ 0.1                              ║
║ some_true_arg   │ True                             ║
║ some_false_arg  │ False                            ║
╚═════════════════╧══════════════════════════════════╝It is recommended though to use theargs.to_string()method, which gives you control of the table width.Attrs Support (New)The way we define the arguments is very similar to defining adataclassor anattrs-class, so it seems natural to just write an attrs-class, and add parsing support to it.To useargtypedwithattrs, simply define an attrs-class as usual, and have it subclassAttrsArguments. Here's
the same example above, but implemented with attrs-classes, and with some bells and whistles:importattr# note: new style `attrs` syntax is also supportedfromargtypedimportAttrsArgumentsdef_convert_logging_level(s:str)->LoggingLevels:# Custom conversion function that takes the raw string value from the command line.returnLoggingLevels[s.lower()]@attr.s(auto_attribs=True)classMyArguments(AttrsArguments):model_name:str=attr.ib(metadata={"positional":True})# positional argument# Or: `model_name: str = argtyped.positional_arg()`.layer_sizes:List[int]=attr.ib(metadata={"nargs":"+"})# other metadata are treated as `argparse` optionsactivation:Literal["relu","tanh","sigmoid"]="relu"logging_level:LoggingLevels=attr.ib(default=LoggingLevels.Info,converter=_convert_logging_level)use_dropout:Switch=Truedropout_prob:Optional[float]=0.5_activation_fn:Callable[[float],float]=attr.ib(init=False)# `init=False` attributes are not parsed@dropout_prob.validator# validators still work as you would expectdef_dropout_prob_validator(self,attribute,value):ifnot0.0<=value<=1.0:raiseValueError(f"Invalid probability{value}")@_activation_fn.defaultdef_activation_fn(self):return_ACTIVATION_FNS[self.activation]A few things to note here:You can define positional arguments by adding"positional": Trueas metadata. If this feels unnatural, you could
also useargtyped.positional_arg(), which takes the same arguments asattr.ib.You can pass additional options toArgumentParser.add_argumentby listing them as metadata as well. Note that
these options take precedence overargtyped's computed arguments, for example, sequence arguments (List[T]) by
default usesnargs="*", but you could override it with metadata.Attributes with custom converters will not be parsed; its converter will be called with the raw string value from
command line. If the attribute also has a default value, you should make sure that your converter works with both
strings and the default value.init=Falseattributes are not treated as arguments, but they can be useful for storing computed values based on
arguments.The default value logic is the same as normal attrs classes, and thus could be different from non-attrsargtypedclasses. For example, optional arguments are not considered to have an implicit default ofNone, and no type
validation is performed on default values.Here are the (current) differences between an attrs-based arguments class (AttrsArguments) versus the normal arguments
class (Arguments):AttrsArgumentssupports positional arguments and custom options via metadata.AttrsArgumentshandles default values with attrs, so there's no validation of default value types.  This also
means that nullable arguments must have an explicit default value ofNone, otherwise it becomes a required
argument.AttrsArgumentsdoes not supportunderscore=True.AttrsArgumentsdoes not haveto_dict,to_stringmethods.AttrsArgumentsneeds to be called with the factoryparse_argsmethod to parse, whileArgumentsparses command
line arguments on construction.ReferenceTheargtyped.ArgumentsClassTheargtyped.Argumentsclass is main class of the package, from which you should derive your custom class that holds
arguments. Each argument takes the form of a class attribute, with its type annotation and an optional default value.When an instance of your custom class is initialized, the command line arguments are parsed fromsys.argvinto values
with your annotated types. You can also provide the list of strings to parse by passing them as the parameter.The parsed arguments are stored in an object of your custom type. This gives you arguments that can be auto-completed
by the IDE, and type-checked by a static type checker likemypy.The following example illustrates the keypoints:classMyArgs(argtyped.Arguments):# name: type [= default_val]value:int=0args=MyArgs()# equivalent to `parser.parse_args()`args=MyArgs(["--value","123"])# equivalent to `parser.parse_args(["--value", "123"])assertisinstance(args,MyArgs)Arguments.to_dict(self)Convert the set of arguments to a dictionary (OrderedDict).Arguments.to_string(self, width: Optional[int] = None, max_width: Optional[int] = None)Represent the arguments as a table.width: Width of the printed table. Defaults toNone, which fits the table to its contents. An exception is raised
when the table cannot be drawn with the given width.max_width: Maximum width of the printed table. Defaults toNone, meaning no limits. Must beNoneifwidthis
notNone.argtyped.argument_specsReturn a dictionary mapping argument names to their specifications, represented as theargtyped.ArgumentSpectype.
This is useful for programmatically accessing the list of arguments.Argument TypesTo summarize, whatever works forargparseworks here. The following types are supported:Built-in typessuch asint,float,str.Boolean typebool. Accepted values (case-insensitive) forTrueare:y,yes,true,ok; accepted values
forFalseare:n,no,false.Choice typesLiteral[...]. A choice argument is essentially anstrargument with limited
choice of values. The ellipses can be filled with a tuple ofstrs, or an expression that evaluates to a list ofstrs:fromargtypedimportArgumentsfromtyping_extensionsimportLiteralclassMyArgs(Arguments):foo:Literal["debug","info","warning","error"]# 4 choices# argv: ["--foo=debug"] => foo="debug"This is equivalent to thechoiceskeyword inargparse.add_argument.Note:The choice type was previously namedChoices. This is deprecated in favor of theLiteraltypeintroduced in Python 3.8 and back-ported to
3.6 and 3.7 in thetyping_extensionslibrary.Choiceswas removed since version 0.4.0.Enum typesderived fromenum.Enum. It is recommended to useargtyped.Enumwhich uses the instance names as
values:fromargtypedimportEnumclassMyEnum(Enum):Debug=auto()# "debug"Info=auto()# "info"Warning=auto()# "warning"Switch typesSwitch.Switcharguments are likeboolarguments, but they don't take values. Instead, a switch
argumentswitchrequires--switchto enable and--no-switchto disable:fromargtypedimportArguments,SwitchclassMyArgs(Arguments):switch:Switch=Truebool_arg:bool=False# argv: []                                 => flag=True,  bool_arg=False# argv: ["--switch", "--bool-arg=false"]   => flag=True,  bool_arg=False# argv: ["--no-switch", "--bool-arg=true"] => flag=False, bool_arg=True# argv: ["--switch=false"]                 => WRONG# argv: ["--no-bool-arg"]                  => WRONGList typesList[T], whereTis any supported type except switch types. List arguments allow passing multiple
values on the command line following the argument flag, it is equivalent to settingnargs="*"inargparse.Although there is no built-in support for othernargssettings such as"+"(one or more) orN(fixed number),
you can add custom validation logic by overriding the__init__method in yourArgumentssubclass.Optional typesOptional[T], whereTis any supported type except list or switch types. An optional argument
will be filled withNoneif no value is provided. It could also be explicitly set toNoneby usingnoneas value
in the command line:fromargtypedimportArgumentsfromtypingimportOptionalclassMyArgs(Arguments):opt_arg:Optional[int]# implicitly defaults to `None`# argv: []                 => opt_arg=None# argv: ["--opt-arg=1"]    => opt_arg=1# argv: ["--opt-arg=none"] => opt_arg=NoneAny other type that takes a singlestras__init__parameters. It is also theoretically possible to use a function
that takes anstras input, but it's not recommended as it's not type-safe.ComposingArgumentsClassesYou can split your arguments into separateArgumentsclasses and then compose them together by inheritance. A subclass
will have the union of all arguments in its base classes. If the subclass contains an argument with the same name as an
argument in a base class, then the subclass definition takes precedence. For example:classBaseArgs(Arguments):a:int=1b:Switch=TrueclassDerivedArgs(BaseArgs):b:str# args = DerivedArgs([])  # bad; `b` is requiredargs=DerivedArgs(["--b=1234"])Caveat:For simplicity, we do not completely follow theC3 linearization algorithmthat determines the class MRO in Python. Thus, it is a bad idea to have
overridden arguments in cases where there's diamond inheritance.If you don't understand the above, that's fine. Just note that generally, it's a bad idea to have too complicated
inheritance relationships with overridden arguments.Argument Naming StylesBy defaultargtypeduses--kebab-case(with hyphens connecting words), which is the convention for UNIX command line
tools. However, many existing tools use the awkward--snake_case(with underscores connecting words), and sometimes
consistency is preferred over aesthetics. If you want to use underscores, you can do so by settingunderscore=Trueinside the parentheses where you specify base classes, like this:classUnderscoreArgs(Arguments,underscore=True):underscore_arg:intunderscore_switch:Switch=Trueargs=UnderscoreArgs(["--underscore_arg","1","--no_underscore_switch"])The underscore settings only affect arguments defined in the class scope; (non-overridden) inherited arguments are not
affects. Thus, you can mix-and-matchsnake_caseandkebab-casearguments:classMyArgs(UnderscoreArgs):kebab_arg:strclassMyFinalArgs(MyArgs,underscore=True):new_underscore_arg:floatargs=MyArgs(["--underscore_arg","1","--kebab-arg","kebab","--new_underscore_arg","1.0"])NotesAdvancedargparsefeatures such as subparsers, groups, argument lists, and custom actions are not supported.Using switch arguments may result in name clashes: if a switch argument has namearg, there can be no argument with
the nameno_arg.Optional types:Optionalcan be used withLiteral:fromargtypedimportArgumentsfromtypingimportLiteral,OptionalclassMyArgs(Arguments):foo:Optional[Literal["a","b"]]# validbar:Literal["a","b","none"]# also works but is less obviousOptional[str]would parse a value of"none"(case-insensitive) intoNone.List types:List[Optional[T]]is a valid type. For example:fromargtypedimportArgumentsfromtypingimportList,Literal,OptionalclassMyArgs(Arguments):foo:List[Optional[Literal["a","b"]]]=["a",None,"b"]# valid# argv: ["--foo", "a", "b", "none", "a", "b"] => foo=["a", "b", None, "a", "b"]List types cannot be nested inside a list or an optional type. Types such asOptional[List[int]]andList[List[int]]are not accepted.Under the HoodThis is what happens under the hood:When a subclass ofargtyped.Argumentsis constructed, type annotations and class-level attributes (i.e., the
default values) are collected to form argument declarations.After verifying the validity of declared arguments,argtyped.ArgumentSpecare created for each argument and stored
within the subclass as the__arguments__class attribute.When an instance of the subclass is initialized, if it's the first time, an instance ofargparse.ArgumentParseris
created and arguments are registered with the parser. The parser is cached in the subclass as the__parser__attribute.The parser'sparse_argsmethod is invoked with eithersys.argvor strings provided as parameters, returning
parsed arguments.The parsed arguments are assigned toself(the instance of theArgumentssubclass being initialized).TodoSupportaction="append"oraction="extend"forList[T]types.Technically this is not a problem, but there's no elegant way to configure whether this behavior is desired.Throw (suppressible) warnings on using non-type callables as types.Support converting anattrsclass intoArguments.Support forward references in type annotations.MIT LicenseCopyright (c) 2020 Zecong HuPermission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.