Source code for openquake.commonlib.sap

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2014-2016 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/>.

"""
Here is a minimal example of usage:

.. code-block:: python

    >>> from openquake.commonlib import sap
    >>> def fun(input, inplace, output=None, out='/tmp'):
    ...     'Example'
    ...     for argname, argvalue in sorted(locals().iteritems()):
    ...         print argname, '=', argvalue

    >>> p = sap.Parser(fun)
    >>> p.arg('input', 'input file or archive')
    >>> p.flg('inplace', 'convert inplace')
    >>> p.arg('output', 'output archive')
    >>> p.opt('out', 'optional output file')

    >>> p.callfunc(['a'])
    inplace = False
    input = a
    out = /tmp
    output = None

    >>> p.callfunc(['a', 'b', '-i', '-o', 'OUT'])
    inplace = True
    input = a
    out = OUT
    output = b

Parsers can be composed too.
"""

import sys
import inspect
import argparse
from collections import OrderedDict


NODEFAULT = object()


[docs]def get_parentparser(parser, description=None, help=True): """ :param parser: :class:`argparse.ArgumentParser` instance or None :param description: string used to build a new parser if parser is None :param help: flag used to build a new parser if parser is None :returns: if parser is None the new parser; otherwise the `.parentparser` attribute (if set) or the parser itself (if not set) """ if parser is None: return argparse.ArgumentParser( description=description, add_help=help) elif hasattr(parser, 'parentparser'): return parser.parentparser else: return parser
[docs]def str_choices(choices): """Returns {choice1, ..., choiceN} or the empty string""" if choices: return '{%s}' % ', '.join(choices) return ''
[docs]class Parser(object): """ A simple way to define command processors based on argparse. Each parser is associated to a function and parsers can be composed together, by dispatching on a given name (if not given, the function name is used). """ def __init__(self, func, name=None, parentparser=None, help=True): self.func = func self.name = name or func.__name__ args, self.varargs, varkw, defaults = inspect.getargspec(func) assert self.varargs is None, self.varargs defaults = defaults or () nodefaults = len(args) - len(defaults) alldefaults = (NODEFAULT,) * nodefaults + defaults self.argdict = OrderedDict(zip(args, alldefaults)) self.description = descr = func.__doc__ if func.__doc__ else None self.parentparser = get_parentparser(parentparser, descr, help) self.names = set() self.all_arguments = [] self._group = self.parentparser self._argno = 0 # used in the NameError check in the _add method self.checked = False # used in the check_arguments method
[docs] def group(self, descr): """Added a new group of arguments with the given description""" self._group = self.parentparser.add_argument_group(descr)
def _add(self, name, *args, **kw): """ Add an argument to the underlying parser and grow the list .all_arguments and the set .names """ argname = list(self.argdict)[self._argno] if argname != name: raise NameError( 'Setting argument %s, but it should be %s' % (name, argname)) self._group.add_argument(*args, **kw) self.all_arguments.append((args, kw)) self.names.add(name) self._argno += 1
[docs] def arg(self, name, help, type=None, choices=None, metavar=None, nargs=None): """Describe a positional argument""" kw = dict(help=help, type=type, choices=choices, metavar=metavar, nargs=nargs) default = self.argdict[name] if default is not NODEFAULT: kw['nargs'] = nargs or '?' kw['default'] = default kw['help'] = kw['help'] + ' [default: %s]' % repr(default) self._add(name, name, **kw)
[docs] def opt(self, name, help, abbrev=None, type=None, choices=None, metavar=None, nargs=None): """Describe an option""" kw = dict(help=help, type=type, choices=choices, metavar=metavar, nargs=nargs) default = self.argdict[name] if default is not NODEFAULT: kw['default'] = default kw['metavar'] = metavar or str_choices(choices) or str(default) abbrev = abbrev or '-' + name[0] abbrevs = set(args[0] for args, kw in self.all_arguments) longname = '--' + name.replace('_', '-') if abbrev == '-h' or abbrev in abbrevs: # avoid conflicts with predefined abbreviations self._add(name, longname, **kw) else: self._add(name, abbrev, longname, **kw)
[docs] def flg(self, name, help, abbrev=None): """Describe a flag""" abbrev = abbrev or '-' + name[0] longname = '--' + name.replace('_', '-') self._add(name, abbrev, longname, action='store_true', help=help)
[docs] def check_arguments(self): """Make sure all arguments have a specification""" for name, default in self.argdict.items(): if name not in self.names and default is NODEFAULT: raise NameError('Missing argparse specification for %r' % name)
[docs] def callfunc(self, argv=None): """ Parse the argv list and extract a dictionary of arguments which is then passed to the function underlying the Parser. """ if not self.checked: self.check_arguments() self.checked = True namespace = self.parentparser.parse_args(argv or sys.argv[1:]) return self.func(**vars(namespace))
[docs] def help(self): """ Return the help message as a string """ return self.parentparser.format_help()
[docs]def compose(parsers, name='main', description=None, prog=None, version=None): """ Collects together different arguments parsers and builds a single Parser dispatching to the subparsers depending on the first argument, i.e. the name of the subparser to invoke. :param parsers: a list of Parser instances :param name: the name of the composed parser :param description: description of the composed parser :param prog: name of the script printed in the usage message :param version: version of the script printed with --version """ assert len(parsers) >= 1, parsers parentparser = argparse.ArgumentParser( description=description, add_help=False) parentparser.add_argument( '--version', '-v', action='version', version=version) subparsers = parentparser.add_subparsers( help='available subcommands; use %s help <cmd>' % prog, prog=prog) def gethelp(cmd=None): if cmd is None: print(parentparser.format_help()) return subp = subparsers._name_parser_map.get(cmd) if subp is None: print('No help for unknown command %r' % cmd) else: print(subp.format_help()) help_parser = Parser(gethelp, 'help', help=False) progname = '%s ' % prog if prog else '' help_parser.arg('cmd', progname + 'subcommand') for p in parsers + [help_parser]: subp = subparsers.add_parser(p.name, description=p.description) for args, kw in p.all_arguments: subp.add_argument(*args, **kw) subp.set_defaults(_func=p.func) def main(**kw): func = kw.pop('_func') return func(**kw) main.__name__ = name return Parser(main, name, parentparser)