import inspect, os, argparse, logging, sys, asyncio
[docs]class CLI:
"""object designed for swift module CLI configuration"""
def __init__(self, desc, logs, log_obj=None):
"""init top-level parser"""
# define the name of the cli application as the file name of the module which is importing this class
self.name = os.path.basename(inspect.stack()[-1].filename)[:-3]
# define root parser
self.parser = argparse.ArgumentParser(prog=self.name, description=desc)
# add commands subparser
self.subparsers = self.parser.add_subparsers(title="commands", dest="command")
self.func_dict = {} # init empty func dict
self.log_obj = log_obj # store copy of the log object for logging compatibility
self.input = None
if log_obj:
self.log = log_obj.loggers # extract functional logger list
# toggles logs for cli commands
if not logs and log_obj:
for logger in vars(self.log).values(): # iterates through all loggers
logger.setLevel(
logging.CRITICAL + 1
) # sets logs to 1 above critical i.e. 51
[docs] def add_funcs(self, func_dict):
"""add registered functions to the cli"""
self.func_dict = func_dict # assign function dictionary property
# iterate through registered functions
for func_name, items in func_dict.items():
names = items['names'] # collect arg names
types = items['types'] # collect types of arg
arg_types = [types.get(name, None) for name in names]
defaults = items['defaults'] # collect default args
# init arg help and arg description
ahelp = f"execute {func_name} function"
# collect command description
signature = inspect.signature(func_dict[func_name]['func'])
# collect names and params for a given function
params = []
for name, param in signature.parameters.items():
# check if function contains annotations
if param.annotation != inspect.Parameter.empty:
# if default arg exists display in docs
if param.default != inspect.Parameter.empty:
params.append(
f"{name}: {param.annotation.__name__} = {param.default!r}"
)
else:
params.append(f"{name}: {param.annotation.__name__}")
else:
if param.default != inspect.Parameter.empty:
params.append(f"{name} = {param.default!r}")
else:
params.append(f"{name}")
# define return type if exists for docs
if "return" in types:
adesc = f"{func_dict[func_name]['func'].__name__}({', '.join(params)}) -> {str(types['return'].__name__)}"
else:
adesc = f"{func_dict[func_name]['func'].__name__}({', '.join(params)})"
# define help string
if items['desc'] is not None:
ahelp = items['desc']
# init sub parser
subp = self.subparsers.add_parser(
func_name,
help=ahelp,
description=adesc,
argument_default=argparse.SUPPRESS,
add_help=False,
)
# create abbreviations for named short name
abbrevs = set()
for name, atype in zip(names, arg_types):
# if arg is contains a default define a short name
if name in defaults:
# default abbreviation is the first 2 characters
short_name = name[:2]
# if space is taken define short name as just the list character
if short_name in abbrevs:
short_name = name[-1]
abbrevs.add(short_name)
# if there exists a short name with the same first and
# last chars do not define one
try:
subp.add_argument(
f"-{short_name}",
f"--{name}",
metavar=str(atype) if atype is not None else None,
type=atype,
default=defaults[name],
help=f"default: {defaults[name]}",
)
except argparse.ArgumentError:
subp.add_argument(
f"--{name}",
metavar=str(atype) if atype is not None else None,
type=atype,
default=defaults[name],
help=f"default: {defaults[name]}",
)
else:
# if variadic allow any number of args
if items['variadic']:
subp.add_argument(
name, nargs='*', type=atype, help=str(atype) if atype is not None else None
)
else:
subp.add_argument(
name, type=atype, help=str(atype) if atype is not None else None
)
# overide help & place at end of options
subp.add_argument(
"-h", "--help", action="help", help="Show this help message and exit."
)
[docs] def parse(self):
"""initialize parsing args"""
self.input = self.parser.parse_args()
# if command in input namespace
if self.input.command:
# retrieve function and arg names for given command
func_meta = self.func_dict[self.input.command]
args = []
kwargs = {}
# if variadic define args and kwargs
if func_meta['variadic']:
func = func_meta['func']
try:
for arg in vars(self.input)['*args']:
if '=' in arg:
k,v = arg.split('=')
kwargs[k] = v
else:
args.append(arg)
except KeyError:
# pass because args & kwargs are already defined empty
pass
else:
# unpack just the args and function
func, arg_names = (
func_meta['func'],
func_meta['names'],
)
# collect args from input namespace
args = [getattr(self.input, arg) for arg in arg_names]
# run function with given args and collect any returns
if asyncio.iscoroutinefunction(func):
returned = asyncio.run(func(*args, **kwargs))
else:
returned = func(*args, **kwargs)
# print return if not None
if returned:
print(returned)
# exit the interpreter so the entire script is not run
sys.exit()