API details
# default_exp core
#export
import inspect,functools
import argparse
def test_eq(a,b): assert a==b,a
#export
class Param:
"A parameter in a function used in `anno_parser` or `call_parse`"
def __init__(self, help=None, type=None, opt=True, action=None, nargs=None, const=None,
choices=None, required=None):
self.help,self.type,self.opt,self.action,self.nargs = help,type,opt,action,nargs
self.const,self.choices,self.required = const,choices,required
def set_default(self, d):
if d==inspect.Parameter.empty: self.opt = False
else:
self.default = d
self.help += f" (default: {d})"
@property
def pre(self): return '--' if self.opt else ''
@property
def kwargs(self): return {k:v for k,v in self.__dict__.items() if v is not None and k!='opt'}
Each parameter in your function should have an annotation Param(...)
. You can pass the following when calling Param
: help
,type
,opt
,action
,nargs
,const
,choices
,required
(i.e. it takes the same parameters as argparse.ArgumentParser.add_argument
, plus opt
). Except for opt
, all of these are just passed directly to argparse
, so you have all the power of that module at your disposal. Generally you'll want to pass at least help
(since this is provided as the help string for that parameter) and type
(to ensure that you get the type of data you expect).
opt
is a bool that defines whether a param is optional or required (positional) - but you'll generally not need to set this manually, because fastscript will set it for you automatically based on default values. You should provide a default (after the =
) for any optional parameters. If you don't provide a default for a parameter, then it will be a positional parameter.
p = Param(help="help", type=int)
p.set_default(1)
test_eq(p.kwargs, {'help': 'help (default: 1)', 'type': int, 'default': 1})
#export
def bool_arg(v):
"Use as `type` for `Param` to get `bool` behavior"
if isinstance(v, bool): return v
if v.lower() in ('yes', 'true', 't', 'y', '1'): return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'): return False
else: raise argparse.ArgumentTypeError('Boolean value expected.')
def f(from_name:Param("Get args from prog name instead of argparse", bool_arg)=0,
a:Param("param 1", bool_arg)=1,
b:Param("param 2", str)="test"): ...
#export
def anno_parser(func, prog=None, from_name=False):
"Look at params (annotated with `Param`) in func and return an `ArgumentParser`"
p = argparse.ArgumentParser(description=func.__doc__, prog=prog)
for k,v in inspect.signature(func).parameters.items():
param = func.__annotations__.get(k, Param())
param.set_default(v.default)
p.add_argument(f"{param.pre}{k}", **param.kwargs)
p.add_argument(f"--xtra", help="Parse for additional args", type=str)
return p
This converts a function with parameter annotations of type Param
into an argparse.ArgumentParser
object. Function arguments with a default provided are optional, and other arguments are positional.
def f(required:Param("Required param", int),
a:Param("param 1", bool_arg)=1,
b:Param("param 2", str)="test"):
"my docs"
...
p = anno_parser(f, 'progname')
p.print_help()
usage: progname [-h] [--a A] [--b B] [--xtra XTRA] required my docs positional arguments: required Required param optional arguments: -h, --help show this help message and exit --a A param 1 (default: 1) --b B param 2 (default: test) --xtra XTRA Parse for additional args
#export
def args_from_prog(func, prog):
"Extract args from `prog`"
if '##' in prog: _,prog = prog.split('##', 1)
progsp = prog.split("#")
args = {progsp[i]:progsp[i+1] for i in range(0, len(progsp), 2)}
for k,v in args.items():
t = func.__annotations__.get(k, Param()).type
if t: args[k] = t(v)
return args
Sometimes it's convenient to extract arguments from the actual name of the called program. args_from_prog
will do this, assuming that names and values of the params are separated by a #
. Optionally there can also be a prefix separated by ##
(double underscore).
exp = {'a': False, 'b': 'baa'}
test_eq(args_from_prog(f, 'foo##a#0#b#baa'), exp)
test_eq(args_from_prog(f, 'a#0#b#baa'), exp)
#export
def call_parse(func):
"Decorator to create a simple CLI from `func` using `anno_parser`"
mod = inspect.getmodule(inspect.currentframe().f_back)
if not mod: return func
@functools.wraps(func)
def _f(*args, **kwargs):
mod = inspect.getmodule(inspect.currentframe().f_back)
if not mod: return func(*args, **kwargs)
p = anno_parser(func)
args = p.parse_args()
xtra = getattr(args, 'xtra', None)
if xtra is not None:
if xtra==1: xtra = p.prog
for k,v in args_from_prog(func, xtra).items(): setattr(args,k,v)
del(args.xtra)
func(**args.__dict__)
if mod.__name__=="__main__":
setattr(mod, func.__name__, _f)
return _f()
else: return _f
@call_parse
def test_add(a:Param("param a", int), b:Param("param 1",int)):
return a + b
This is a test to see if call_parse
works as a regular function and also as a command-line interface function.
import os
test_eq(os.system('test_fastscript "Test if this still works"'), 0)
test_eq(test_add(1,2), 3)
This is the main way to use fastscript
; decorate your function with call_parse
, add Param
annotations as shown above, and it can then be used as a script.
#hide
from nbdev.export import notebook2script
notebook2script()
Converted 00_core.ipynb. Converted index.ipynb.