Package: dataclass-click
dataclass-clickStructure your click arguments.dataclass-click lets you move your user arguments from kwargs to dataclasses, keeping things self-contained.Click is pretty simple to start with, but when your program gets complex, you find yourself repeating a lot of work.
This is particularly true if you have command groups with shared arguments.The idea ofdataclass-clickis to move the@optionand@argumentdecorators off into annotations on dataclasses
and pass dataclass objects instead of kwargs.Simple ExamplefromdataclassesimportdataclassfrompathlibimportPathfromtypingimportAnnotatedimportclickfromdataclass_clickimportargument,dataclass_click,option@dataclassclassConfig:# Auto-inferred type for built-in click typestarget:Annotated[Path,argument()]# Auto-inferred option namesfoo:Annotated[float,option(required=True)]# Automatically map mismatched namesbar:Annotated[int|None,option("--other")]@click.command()@dataclass_click(Config)defmain(config:Config):# All args neatly packaged.print(config.target,config.foo,config.bar)print(list(config.target.iterdir()))Inference FeaturesName inferenceThe pythonic name for options and attributes is copied from the dataclass attribute name.
Options can then be named whatever you want. If you don't specify an option name, it will be inferred for you:fromdataclassesimportdataclassfromdataclass_clickimportoption,argumentfromtypingimportAnnotated@dataclassclassConfig:foo:Annotated[str,argument()]bar:Annotated[str,option()]baz:Annotated[str,option("--bonno")]Usage: main.py [OPTIONS] FOO

Options:
  --bonno TEXT
  --bar TEXT
  --help        Show this message and exit.Type Inference (Extensible)Out of the box...data-class click can infer click parameter type (eg:option(type=click.INT)) from the attribute type.
These types are static so if, likeclick.Path, you need arguments, you can always specify the type yourself like
normal.fromdataclassesimportdataclassfromdatetimeimportdatetimefrompathlibimportPathfromtypingimportAnnotatedfromuuidimportUUIDfromdataclass_clickimportoption@dataclassclassConfig:a:Annotated[str,option()]# click.STRINGb:Annotated[bool,option()]# click.BOOLc:Annotated[int,option()]# click.INTd:Annotated[float,option()]# click.FLOATe:Annotated[UUID,option()]# click.UUIDf:Annotated[datetime,option()]# click.DateTime()g:Annotated[Path,option()]# click.Path(path_type=Path)Extending it...Type inference is extensible so a program can add and even change the default click ParameterTypes associated with a
Python data type.  Eg: if you want to add aDecimal:fromdecimalimportDecimalimportclickimportdataclass_clickclassDecimalParameterType(click.ParamType):name="integer"defconvert(self,value,param,ctx):ifisinstance(value,Decimal):returnvaluetry:returnDecimal(value)exceptValueError:self.fail(f"{value!r}is not a valid decimal number",param,ctx)DECIMAL=DecimalParameterType()# Extend type inference to add Decimaldataclass_click.register_type_inference(Decimal,DECIMAL)Required InferenceTo avoid tricky mismatches between required options and optional attributes, dataclass-click will infer a field is
required if neitherdefault=norrequired=are explicitly set and the attribute type hint is not optional:fromdataclassesimportdataclassfromtypingimportAnnotatedfromdataclass_clickimportoption@dataclassclassConfig:# Infer --foo is required because str is not optionalfoo:Annotated[str,option()]# Infer --bar is not required because str | None is optionalbar:Annotated[str|None,option()]# No inference performed because required= is set.baz:Annotated[str|None,option(required=True)]# Infer not required because default= set.bob:Annotated[str,option(default="no")]# If you want to shoot yourself in the foot, you can!# Not inferred because required=False.# Click will pass None even though the attribute is not optionalbonno:Annotated[str,option(required=False)]Usage: main.py [OPTIONS]

Options:
  --bob TEXT
  --baz TEXT  [required]
  --bar TEXT
  --foo TEXT  [required]
  --bonno
  --help      Show this message and exit.