# -*- coding: utf-8 -*-# vim: tabstop=4 shiftwidth=4 softtabstop=4## Copyright (C) 2014-2025 GEM Foundation## OpenQuake is free software: you can redistribute it and/or modify it# under the terms of the GNU Affero General Public License as published# by the Free Software Foundation, either version 3 of the License, or# (at your option) any later version.## OpenQuake is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU Affero General Public License for more details.## You should have received a copy of the GNU Affero General Public License# along with OpenQuake. If not, see <http://www.gnu.org/licenses/>."""``openquake.baselib.sap`` is a Simple Argument Parser based on argparsewhich is extremely powerful. Its features are1. zero boilerplate (no decorators)2. supports arbitrarily nested subcommands with an easy sintax3. automatically generates a simple parser from a Python module and a hierarchic parser from a Python package.Here is a minimal example of usage:.. code-block:: python >>> def convert_archive(input_, output=None, inplace=False, *, out='/tmp'): ... "Example" ... print(input_, output, inplace, out) >>> convert_archive.input_ = 'input file or archive' >>> convert_archive.inplace = 'convert inplace' >>> convert_archive.output = 'output archive' >>> convert_archive.out = 'output directory' >>> run(convert_archive, argv=['a.zip', 'b.zip']) a.zip b.zip False /tmp >>> run(convert_archive, argv=['a.zip', '-i', '-o', '/tmp/x']) a.zip None True /tmp/x"""importosimportinspectimportargparseimportimportlibNODEFAULT=object()def_choices(choices):# returns {choice1, ..., choiceN} or the empty stringifchoices:return'{%s}'%', '.join(map(str,choices))return''def_populate(parser,func):# populate the parser# args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annsargspec=inspect.getfullargspec(func)ifargspec.varargs:raiseTypeError('varargs in the signature of %s are not supported'%func)defaults=argspec.defaultsor()nodefaults=len(argspec.args)-len(defaults)alldefaults=(NODEFAULT,)*nodefaults+defaultsargdef=dict(zip(argspec.args,alldefaults))argdef.update(argspec.kwonlydefaultsor{})parser.description=func.__doc__parser.set_defaults(_func=func)parser.aliases={}argdescr=[]# list of pairs (argname, argkind)forarginargspec.args:ifargdef[arg]isFalse:argdescr.append((arg,'flg'))else:argdescr.append((arg,'pos'))forarginargspec.kwonlyargs:argdescr.append((arg,'opt'))abbrevs={'-h'}# already taken abbreviationsforname,kindinargdescr:ifname.endswith('_'):# make it possible use bultins/keywords as argument namesstripped=name.rstrip('_')parser.aliases[stripped]=nameelse:stripped=namedescr=getattr(func,name,'')ifisinstance(descr,str):kw=dict(help=descr)else:# assume a dictionarykw=descr.copy()if(kind!='flg'andkw.get('type')isNoneandnameinfunc.__annotations__):kw.setdefault('type',func.__annotations__[name])abbrev=kw.get('abbrev')choices=kw.get('choices')default=argdef[name]ifkind=='pos':ifdefaultisnotNODEFAULT:kw['default']=defaultkw.setdefault('nargs','?')ifdefaultisnotNone:kw['help']+=' [default: %s]'%repr(default)elifkind=='flg':kw.setdefault('abbrev',abbrevor'-'+name[0])kw['action']='store_true'elifkind=='opt':kw.setdefault('abbrev',abbrevor'-'+name[0])ifdefaultnotin(None,NODEFAULT):kw['default']=defaultkw.setdefault('metavar',_choices(choices)orstr(default))abbrev=kw.pop('abbrev',None)longname='--'+stripped.replace('_','-')ifabbrevandabbrevinabbrevs:# avoid conflicts with previously defined abbreviationsargs=longname,elifabbrev:iflen(abbrev)>2:# no single-letter abbrevargs=longname,abbrevelse:# single-letter abbrevargs=abbrev,longnameabbrevs.add(abbrev)else:# no abbrevargs=stripped,parser.add_argument(*args,**kw)def_rec_populate(parser,funcdict):subparsers=parser.add_subparsers(help='available subcommands; use %s <subcmd> --help'%parser.prog)forname,funcinfuncdict.items():subp=subparsers.add_parser(name,prog=parser.prog+' '+name)ifisinstance(func,dict):# nested subcommand_rec_populate(subp,func)else:# terminal subcommand_populate(subp,func)
[docs]defpkg2dic(pkg):""" :param pkg: a python module or package :returns: a dictionary name -> func_or_dic_of_funcs """ifnothasattr(pkg,'__path__'):# is a module, not a packagereturn{pkg.__name__:pkg.main}dic={}forpathinpkg.__path__:fornameinos.listdir(path):fname=os.path.join(path,name)dotname=pkg.__name__+'.'+nameifos.path.isdir(fname)and'__init__.py'inos.listdir(fname):subdic=pkg2dic(importlib.import_module(dotname))ifsubdic:dic[name]=subdicelifname.endswith('.py')andnamenotin('__init__.py','__main__.py'):mod=importlib.import_module(dotname[:-3])ifhasattr(mod,'main'):dic[name[:-3]]=mod.mainreturndic
[docs]defparser(funcdict,**kw):""" :param funcdict: a function or a nested dictionary of functions :param kw: keyword arguments passed to the underlying ArgumentParser :returns: the ArgumentParser instance """ifisinstance(funcdict,dict):version=funcdict.pop('__version__',None)else:version=getattr(funcdict,'__version__',None)parser=argparse.ArgumentParser(**kw)ifversion:parser.add_argument('-v','--version',action='version',version=version)ifinspect.ismodule(funcdict):# passed a module or packagefuncdict=pkg2dic(funcdict)ifcallable(funcdict):_populate(parser,funcdict)else:_rec_populate(parser,funcdict)returnparser
def_run(parser,argv=None):namespace=parser.parse_args(argv)try:func=namespace.__dict__.pop('_func')exceptKeyError:parser.print_usage()returnifhasattr(parser,'aliases'):# go back from stripped to unstripped namesdic={parser.aliases.get(name,name):valueforname,valueinvars(namespace).items()}else:dic=vars(namespace)returnfunc(**dic)
[docs]defrun(funcdict,argv=None,**parserkw):""" :param funcdict: a function or a nested dictionary of functions :param argv: a list of command-line arguments (if None, use sys.argv[1:]) :param parserkw: arguments accepted by argparse.ArgumentParser """return_run(parser(funcdict,**parserkw),argv)
[docs]defrunline(line,**parserkw):""" Run a command-line. Useful in the tests. """pkg,*args=line.split()returnrun(importlib.import_module(pkg),args,**parserkw)