Package: arcparse
arcparseDeclare program arguments type-safely.This project builds on top ofargparseby adding type-safety and allowing a more expressive argument parser definition.Disclaimer: This library is young and relatively unstable. Issues are open and pull requests are welcome!Example usagefromarcparseimportarcparser,positional@arcparserclassArgs:name:str=positional()age:inthappy:boolargs=Args.parse("--age 25 --happy Thomas".split())print(f"Hi, my name is{args.name}!")For a complete overview of features seeFeatures.Installation# Using pip$pipinstallarcparseFeaturesRequired and optional argumentsArguments without explicitly assigned argument class are implicitly options (prefixed with--). A non-optional typehint results inrequired=Truefor options. Defaults can be set by directly assigning them. You can useoption()to further customize the argument.@arcparserclassArgs:required:stroptional:str|Nonedefault:str="foo"default_with_help:str=option(default="bar",help="help message")Positional argumentsPositional arguments usepositional(). Optional type-hints usenargs="?"in the background.@arcparserclassArgs:required:str=positional()optional:str|None=positional()FlagsAll arguments type-hinted asboolare flags, they useaction="store_true"in the background. Flags (as well as options) can also define short forms for each argument. They can also disable the long form withshort_only=True.Useno_flag()to easily create a--no-...flag withaction="store_false".Usetri_flag()(or type-hint argument asbool | None) to create a "true" flag and a "false" flag (e.g.--cloneand--no-clone). Passing--clonewill storeTrue, passing--no-clonewill storeFalseand not passing anything will storeNone. Passing both is an error ensured by an implicit mutually exclusive group.@arcparserclassArgs:sync:boolrecurse:bool=no_flag(help="Do not recurse")clone:bool|Nonedebug:bool=flag("-d")# both -d and --debugverbose:bool=flag("-v",short_only=True)# only -vMultiple values per argumentBy type-hinting the argument aslist[...], the argument will usenargs="*"in the background. Passingat_least_one=Trueusesnargs="+"instead. Passingappend=Truetooption()usesaction="append"instead (this is available only foroption()and incompatible withat_least_one).@arcparserclassArgs:option_nargs:list[str]positional_nargs:list[str]=positional()append_option:list[str]=option(append=True)nargs_plus_option:list[str]=option(at_least_one=True)nargs_plus_positional:list[str]=positional(at_least_one=True)Note thatoption(at_least_one=True)will cause the option to be required. If this is not intended, provide a default value.Name overridingPassingname_override=...will cause the provided string to be used instead of the variable name for the argument name. The string will undergo a replacement of_with-and will contain a--prefix if used inoption().This is useful in combination with accepting multiple values withappend=True, because the user will use--value foo --value bar, while the code will useargs.values.@arcparserclassArgs:values:list[str]=option(name_override="value",append=True)Type conversionsAutomatic type conversions are supported. The type-hint type is used to convert the string argument to the desired type. This is NOT done using argparse'stype=...because it was causing issues fordict_option()anddict_positional(). Using aStrEnumsubclass as a type-hint automatically populateschoices, usingLiteralalso populates choices but does not set converter unlikeStrEnum. Using are.Patterntypehint automatically usesre.compileas a converter. A custom type-converter can be used by passingconverter=...to eitheroption()orpositional(). Come common utility converters are defined inconverters.py.Custom converters may be used in combination with multiple values per argument. These converters are calleditemwiseand need to be wrapped initemwise(). This wrapper is used automatically if an argument is typed aslist[...]and no converter is set.fromarcparse.convertersimportsv,csv,sv_dict,itemwisefromenumimportStrEnumfromreimportPatternfromtypingimportLiteral@arcparserclassArgs:classResult(StrEnum):PASS="pass"FAIL="fail"@classmethoddeffrom_int(cls,arg:str)->"Result":number=int(arg)returncls.PASSifnumber==1elsecls.FAILnumber:intresult:Resultliteral:Literal["yes","no"]pattern:Patterncustom:Result=option(converter=Result.from_int)ints:list[int]=option(converter=csv(int))ip_parts:list[int]=option(converter=sv(".",int),name_override="ip")int_overrides:dict[str,int]=option(converter=sv_dict(",","=",value_type=int))# accepts x=1,y=2results:list[Result]=option(converter=itemwise(Result.from_int))dict helpersSometimes creating an argument able to choose a value from a dict by its key is desired.dict_option()anddict_positional()do exactly that. In the following example passing--foo yeswill result in.foobeingTrue.fromarcparseimportdict_optionvalues={"yes":True,"no":False,}@arcparserclassArgs:foo:bool=dict_option(values)Mutually exclusive groupsUsemx_groupto group multiple arguments together in a mutually exclusive group. Each argument has to have a default defined either implicitly through the type (beingboolor a union withNone) or explicitly withdefault.@arcparserclassArgs:group=mx_group()# alternatively use `mx_group=(group := mx_group())` on the next lineflag:bool=flag(mx_group=group)option:str|None=option(mx_group=group)SubparsersType-hinting an argument as a union of classes creates subparsers from them in the background. Assigning fromsubparsers()gives them names as they will be entered from the command-line. Subparsers are required by default. AddingNoneto the union makes the subparsers optional.classFooArgs:arg1:strclassBarArgs:arg2:int=positional()@arcparserclassArgs:action:FooArgs|BarArgs=subparsers("foo","bar")@arcparserclassOptionalSubparsersArgs:action:FooArgs|BarArgs|None=subparsers("foo","bar")Once the arguments are parsed, the different subparsers can be triggered and distinguished like so:python3script.pyfoo--arg1baz
python3script.pybar--arg2123args=Args.parse("foo --arg1 baz".split())ifisinstance(foo:=args.action,FooArgs):print(f"foo{foo.arg1}")elifisinstance(bar:=args.action,BarArgs):print(f"bar{bar.arg2}")CreditsThis project was inspired byswansonk14/typed-argument-parser.Known issuesAnnotationsfrom __future__ import annotationsmakes all annotations strings at runtime. This library relies on class variable annotations's types being actual types.inspect.get_annotations(obj, eval_str=True)is used to evaluate string annotations to types in order to assign converters. If an argument is annotated with a non-builtin type which is defined outside of the argument-defining class body the type can't be found which results inNameErrors. This is avoidable either by only using custom types which have been defined in the argument-defining class body (which is restrictive), or alternatively by not using theannotationsimport which should not be necessary from python 3.13 forward thanks toPEP 649.