Package: click-inspect
click-inspectAdd options to click commands based on inspecting functionsInstallationpip install click-inspectUsageSuppose an application containing an API function for which we would like to expose a command line interface. That function expects one or two arguments with internal data types and a bunch of configuration options. For example:defdisplay_data(data:List[Tuple[int,int]],*,size:int,symbol:str='x',empty:str=' ')->str:"""Display the given data points in a 2D ASCII grid.Args:data (list of (int, int)): The data points as x- and y-tuples.size (int): Size of the grid in both dimensions.symbol (str): Symbol for displaying data points.empty (str): Symbol for displaying empty space.Returns:str: The string containing the grid."""grid=[[empty]*sizefor_inrange(size)]forx,yindata:grid[y][x]=symboltop=bottom=('+',*'-'*size,'+')grid=(top,*(('|',*row,'|')forrowingrid),bottom)return'\n'.join(map(''.join,grid))Here the type of the first argument,data, is an internal aspect of the application, but the remaining arguments are generic options.Now we want to create aclickinterface for using this function from the command line. Specifically we want it to work on JSON files of the following format:{"data":[[1,1],[2,4],[3,3]]}So the only thing our command needs to do is to read the JSON file and convert the content in a way that it is compatible with whatdisplay_dataexpects:importjsonimportclick@click.command()@click.argument('file')defdisplay(file):withopen(file)asfh:data=json.load(fh)['data']data=[[int(x)forxinrow]forrowindata]print(display_data(data))if__name__=='__main__':display()Then we can run the program in the following way:$ python example.py test.json
+-----+
|     |
| x   |
|     |
|   x |
|  x  |
+-----+Now this only uses the default configuration of thedisplay_datafunction and we also want to expose these optional parameters to the command line interface. We can do so by adding a few options:@click.command()@click.argument('file')@click.option('--size',default=5,help='Size of the grid in both dimensions.')@click.option('--symbol',default='x',help='Symbol for displaying data points.')@click.option('--empty',default=' ',help='Symbol for displaying empty space.')defdisplay(file,size,symbol,empty):withopen(file)asfh:data=json.load(fh)['data']data=[[int(x)forxinrow]forrowindata]print(display_data(data,size=size,symbol=symbol,empty=empty))But that's a lot of code duplication. We duplicated the parameter names, the default values and the help text from the docstring.
Also if we decide to add a new parameter todisplay_datawe need to update the command as well.This is whereclick-inspectcomes in handy. Using theadd_options_fromdecorator we can simply add all optional parameters fromdisplay_datato thedisplaycommand without code duplication:fromclick_inspectimportadd_options_from@click.command()@click.argument('file')@add_options_from(display_data)defdisplay(file,**kwargs):withopen(file)asfh:data=json.load(fh)['data']data=[[int(x)forxinrow]forrowindata]print(display_data(data,**kwargs))CustomizationTheadd_options_fromdecorator supports various keyword parameters which can be used for
customizing the way options are created from parameters. Please refer to the docstring
ofadd_options_fromfor more information. In the following some possibilities are shown:# This adds only the `size` and `empty` parameters as options:@add_options_from(display_data,include={'size','empty'})# This has a similar effect by excluding the `symbol` parameter:@add_options_from(display_data,exclude={'symbol'})# This specifies custom option-names for some of the parameters:@add_options_from(display_data,names={'size':['-s','--size'],'empty':['-e','--empty']})# This overrides the default value for the `symbol` parameter:@add_options_from(display_data,custom={'symbol':{'default':'#'}})Boolean flagsBoolean flags are supported via thebooltype hint. The default behavior is to create an on-/off-option
as described in theclick docs.
If this is undesired, it can be customized by using thenameskeyword parameter ofadd_options_from:foo:bool=True# translates toclick.option('--foo/--no-foo',default=True)# Use the following to create an on-/off-option:add_options_from(my_func,names={'foo':['--foo']})# translates toclick.option('--foo',is_flag=True,default=True)Lists and tuplesclick-inspectalso supports sequences as type hints (e.g.list[int]ortuple[int, str]).
Specifically those type hints are translated as follows:foo:Sequence[int]foo:List[int]# translates toclick.option('--foo',multiple=True,type=int)# -------------------------------------------------- #foo:Tuple[int,str]# translates toclick.option('--foo',type=(int,str))These type hints are also supported as part of the docstring:"""Args:foo (list of int): Equivalent to List[int].foo ((int, str)): Equivalent to Tuple[int, str]."""Unionsclick-inspectalso supportstyping.Unionby simply selecting the first option as the type.
SoUnion[int, str]is equivalent toint.(1)Unions are also supported as part of the docstring viaint or str.Docstring stylesclick-inspectsupports inspectingreST-styledocstrings, as well asGoogle-andNumpy-styledocstrings viasphinx.ext.napoleon.(1) If the Union is part of a generic type, it is not guaranteed that the first option is the same one that is displayed in the Union literal. This is because generic types cache their__getitem__methods. For that reasonList[Union[int, str]] is List[Union[str, int]]and the selected type would beintin both cases since that's the one that got cached.