#!/usr/bin/env python # Copyright (c) 2022, National Diet Library, Japan # # This software is released under the CC BY 4.0. # https://creativecommons.org/licenses/by/4.0/ import argparse import inspect from collections import OrderedDict def get_rec(d, key): def _get_rec(d, ks): if d is None or len(ks) == 0: return d return _get_rec(d.get(ks[0]), ks[1:]) return _get_rec(d, key.split(".")) def get_list_type(type): import re match = re.findall("typing.List\[(.*)\]", str(type)) # noqa: W605 if match: return eval(match[0]) return None def add_argument(parser, fn, **kwargs): assert inspect.isfunction(fn) sig = inspect.signature(fn) params = sig.parameters fn_vars = vars(fn) for k, p in params.items(): kwargs = {} kwargs['help'] = get_rec(fn_vars, 'help.{}'.format(k)) kwargs['choices'] = get_rec(fn_vars, 'choices.{}'.format(k)) kwargs['default'] = p.default type = p.annotation if p.annotation != inspect._empty else None kwargs['type'] = type if kwargs['default'] != inspect._empty: default_str = " (default: {})".format(kwargs['default']) if kwargs['help']: kwargs['help'] += default_str else: kwargs['help'] = default_str list_type = get_list_type(type) if list_type: kwargs['type'] = list_type kwargs['nargs'] = '+' if type is bool: if k.startswith('use') or k.startswith('enable') or k.startswith('disable') or k.startswith('ignore') or k.startswith('naive'): kwargs = {'action': 'store_true', 'help': kwargs['help']} if p.kind == inspect._ParameterKind.POSITIONAL_OR_KEYWORD: if kwargs.get('default', inspect._empty) != inspect._empty or kwargs.get('action', "").startswith('store'): parser.add_argument('--' + k, **kwargs) else: parser.add_argument(k, **kwargs) elif p.kind == inspect._ParameterKind.KEYWORD_ONLY: parser.add_argument('--' + k, **kwargs) def make_argparser(fn, parser=None, **kwargs): if parser is None: parser = argparse.ArgumentParser(**kwargs) if inspect.isfunction(fn): add_argument(parser, fn) parser.set_defaults(handler=fn) elif isinstance(fn, list): subp = parser.add_subparsers() for fn_dict in fn: if inspect.isfunction(fn_dict): fn_dict = {'name': fn_dict.__name__, 'description': fn_dict.__doc__, 'func': fn_dict} p = subp.add_parser( fn_dict['name'], description=fn_dict.get('description', None)) make_argparser(fn_dict['func'], parser=p) elif isinstance(fn, dict): fn = [{'name': name, 'description': val.__doc__, 'func': val} if inspect.isfunction( val) else {'name': name, 'func': val} for name, val in fn.items()] make_argparser(fn, parser=parser) else: assert False, fn return parser def add_params(val_name, help=None, choices=None): def _add_params(fn): if not hasattr(fn, 'help'): fn.help = {} if not hasattr(fn, 'choices'): fn.choices = {} fn.help[val_name] = help fn.choices[val_name] = choices return fn return _add_params def auto_run(*args, **kwargs): parser = make_argparser(*args, **kwargs) args = parser.parse_args() if not hasattr(args, 'handler'): parser.print_help() return handler = args.handler _args = args._get_args() _kwargs = dict(args._get_kwargs()) del(_kwargs['handler']) return handler(*_args, **_kwargs) def argslist_to_dict(argslist): """Convert args list to dictionary. It converts ["KEY1=VAL1,KEY2=VAL2", "KEY3=VAL3"] to {"KEY1": "VAL1", "KEY2": "VAL2", "KEY3": "VAL3"} """ argsdict = OrderedDict() for x in argslist: kvs = x.split(',') for kv in kvs: eq = kv.find('=') k, v = (kv[:eq].strip() if 0 <= eq else '', kv[eq+1:].strip()) argsdict[k] = v return argsdict