Source code for funcargparse

from __future__ import print_function, division
import os
import six
import sys
import inspect
from itertools import chain, groupby
from argparse import ArgumentParser, Namespace
import argparse
from docrep import DocstringProcessor

try:
    from cyordereddict import OrderedDict
except ImportError:
    try:
        from collections import OrderedDict
    except ImportError:
        from ordereddict import OrderedDict


if six.PY2:
    import __builtin__ as builtins
else:
    import builtins


__version__ = '0.2.1'


docstrings = DocstringProcessor()


_on_rtd = os.environ.get('READTHEDOCS', None) == 'True'


[docs]class FuncArgParser(ArgumentParser): """Subclass of an argument parser that get's parts of the information from a given function""" _finalized = False #: The unfinished arguments after the setup unfinished_arguments = {} #: The sections to extract from a function docstring that should be used #: in the epilog of this parser. See also the :meth:`setup_args` method epilog_sections = ['Notes', 'References'] #: The formatter specification for the epilog. This can either be a string #: out of 'header', 'bold', or #: 'rubric' or a callable (i.e. function) that takes two arguments, #: the section title and the section text, and returns a string. #: #: 'heading' #: Use section headers such as:: #: #: Notes #: ----- #: 'bold' #: Just make a bold header for the section, e.g. ``**Notes**`` #: 'rubric' #: Use a rubric rst directive, e.g. ``.. rubric:: Notes`` #: #: .. warning:: #: #: When building a sphinx documentation using the sphinx-argparse #: module, this value should be set to ``'bold'`` or ``'rubric'``! Just #: add this two lines to your conf.py: #: #: .. code-block:: python #: #: import funcargparse #: funcargparse.FuncArgParser.epilog_formatter = 'rubric' epilog_formatter = 'heading' def __init__(self, *args, **kwargs): """ Parameters ---------- ``*args,**kwargs`` Theses arguments are determined by the :class:`argparse.ArgumentParser` base class. Note that by default, we use a :class:`argparse.RawTextHelpFormatter` class for the `formatter_class` keyword, whereas the :class:`argparse.ArgumentParser` uses a :class:`argparse.HelpFormatter` Other Parameters ---------------- epilog_sections: list of str The default sections to use for the epilog (see the :attr:`epilog_sections` attribute). They can also be specified each time the :meth:`setup_args` method is called epilog_formatter: {'header', 'bold', 'rubric'} or function Specify how the epilog sections should be formatted and defaults to the :attr:`epilog_formatter` attribute. This can either be a string out of 'header', 'bold', or 'rubric' or a callable (i.e. function) that takes two arguments, the section title and the section text, and returns a string. 'heading' Use section headers such as:: Notes ----- 'bold' Just make a bold header for the section, e.g. ``**Notes**`` 'rubric' Use a rubric rst directive, e.g. ``.. rubric:: Notes`` """ self._subparsers_action = None kwargs.setdefault('formatter_class', argparse.RawTextHelpFormatter) epilog_sections = kwargs.pop('epilog_sections', None) if epilog_sections is not None: self.epilog_sections = epilog_sections epilog_formatter = kwargs.pop('epilog_formatter', None) if epilog_formatter is not None: self.epilog_formatter = epilog_formatter super(FuncArgParser, self).__init__(*args, **kwargs) self.unfinished_arguments = OrderedDict() self._used_functions = [] self.__currentarg = None self._chain_subparsers = False self._setup_as = None self._epilog_formatters = {'heading': self.format_heading, 'bold': self.format_bold, 'rubric': self.format_rubric}
[docs] @staticmethod def get_param_doc(doc, param): """Get the documentation and datatype for a parameter This function returns the documentation and the argument for a napoleon like structured docstring `doc` Parameters ---------- doc: str The base docstring to use param: str The argument to use Returns ------- str The documentation of the given `param` str The datatype of the given `param`""" arg_doc = docstrings.keep_params_s(doc, [param]) or \ docstrings.keep_types_s(doc, [param]) dtype = None if arg_doc: lines = arg_doc.splitlines() arg_doc = inspect.cleandoc('\n' + '\n'.join(lines[1:])) param_desc = lines[0].split(':', 1) if len(param_desc) > 1: dtype = param_desc[1].strip() return arg_doc, dtype
[docs] @docstrings.get_sectionsf('FuncArgParser.setup_args', sections=['Parameters', 'Returns']) @docstrings.dedent def setup_args(self, func=None, setup_as=None, insert_at=None, interprete=True, epilog_sections=None, overwrite=False, append_epilog=True): """ Add the parameters from the given `func` to the parameter settings Parameters ---------- func: function The function to use. If None, a function will be returned that can be used as a decorator setup_as: str The attribute that shall be assigned to the function in the resulting namespace. If specified, this function will be used when calling the :meth:`parse2func` method insert_at: int The position where the given `func` should be inserted. If None, it will be appended at the end and used when calling the :meth:`parse2func` method interprete: bool If True (default), the docstrings are interpreted and switches and lists are automatically inserted (see the [interpretation-docs]_ epilog_sections: list of str The headers of the sections to extract. If None, the :attr:`epilog_sections` attribute is used overwrite: bool If True, overwrite the existing epilog and the existing description of the parser append_epilog: bool If True, append to the existing epilog Returns ------- function Either the function that can be used as a decorator (if `func` is ``None``), or the given `func` itself. Examples -------- Use this method as a decorator:: >>> @parser.setup_args ... def do_something(a=1): ''' Just an example Parameters ---------- a: int A number to increment by one ''' return a + 1 >>> args = parser.parse_args('-a 2'.split()) Or by specifying the setup_as function:: >>> @parser.setup_args(setup_as='func') ... def do_something(a=1): ''' Just an example Parameters ---------- a: int A number to increment by one ''' return a + 1 >>> args = parser.parse_args('-a 2'.split()) >>> args.func is do_something >>> parser.parse2func('-a 2'.split()) 3 References ---------- .. [interpretation-docs] http://funcargparse.readthedocs.io/en/latest/docstring_interpretation.html) """ def setup(func): # insert the function if insert_at is None: self._used_functions.append(func) else: self._used_functions.insert(insert_at, func) args_dict = self.unfinished_arguments # save the function to use in parse2funcs if setup_as: args_dict[setup_as] = dict( long=setup_as, default=func, help=argparse.SUPPRESS) self._setup_as = setup_as # create arguments args, varargs, varkw, defaults = inspect.getargspec(func) doc = inspect.getdoc(func) full_doc = docstrings.dedents(doc) if doc else '' summary = docstrings.get_full_description(full_doc) if summary: if not self.description or overwrite: self.description = summary full_doc = docstrings._remove_summary(full_doc) self.extract_as_epilog(full_doc, epilog_sections, overwrite, append_epilog) doc = docstrings._get_section(full_doc, 'Parameters') + '\n' doc += docstrings._get_section(full_doc, 'Other Parameters') doc = doc.rstrip() default_min = len(args or []) - len(defaults or []) for i, arg in enumerate(args): if arg == 'self' or arg in args_dict: continue arg_doc, dtype = self.get_param_doc(doc, arg) args_dict[arg] = d = {'dest': arg, 'short': arg.replace('_', '-'), 'long': arg.replace('_', '-')} if arg_doc: d['help'] = arg_doc if i >= default_min: d['default'] = defaults[i - default_min] else: d['positional'] = True if interprete and dtype == 'bool' and 'default' in d: d['action'] = 'store_false' if d['default'] else \ 'store_true' elif interprete and dtype: if dtype.startswith('list of'): d['nargs'] = '+' dtype = dtype[7:].strip() if dtype in ['str', 'string', 'strings']: d['type'] = six.text_type if dtype == 'strings': dtype = 'string' else: try: d['type'] = getattr(builtins, dtype) except AttributeError: try: # maybe the dtype has a final 's' d['type'] = getattr(builtins, dtype[:-1]) dtype = dtype[:-1] except AttributeError: pass d['metavar'] = dtype return func if func is None: return setup else: return setup(func)
[docs] @docstrings.get_sectionsf('FuncArgParser.add_subparsers') @docstrings.dedent def add_subparsers(self, *args, **kwargs): """ Add subparsers to this parser Parameters ---------- ``*args, **kwargs`` As specified by the original :meth:`argparse.ArgumentParser.add_subparsers` method chain: bool Default: False. If True, It is enabled to chain subparsers""" chain = kwargs.pop('chain', None) ret = super(FuncArgParser, self).add_subparsers(*args, **kwargs) if chain: self._chain_subparsers = True self._subparsers_action = ret return ret
[docs] @docstrings.dedent def setup_subparser( self, func=None, setup_as=None, insert_at=None, interprete=True, epilog_sections=None, overwrite=False, append_epilog=True, return_parser=False, name=None, **kwargs): """ Create a subparser with the name of the given function Parameters are the same as for the :meth:`setup_args` function, other parameters are parsed to the :meth:`add_subparsers` method if (and only if) this method has not already been called. Parameters ---------- %(FuncArgParser.setup_args.parameters)s return_parser: bool If True, the create parser is returned instead of the function name: str The name of the created parser. If None, the function name is used and underscores (``'_'``) are replaced by minus (``'-'``) ``**kwargs`` Any other parameter that is passed to the add_parser method that creates the parser Other Parameters ---------------- Returns ------- FuncArgParser or %(FuncArgParser.setup_args.returns)s If return_parser is True, the created subparser is returned Examples -------- Use this method as a decorator:: >>> from funcargparser import FuncArgParser >>> parser = FuncArgParser() >>> @parser.setup_subparser ... def my_func(my_argument=None): ... pass >>> args = parser.parse_args('my-func -my-argument 1'.split()) """ def setup(func): if self._subparsers_action is None: raise RuntimeError( "No subparsers have yet been created! Run the " "add_subparsers method first!") # replace underscore by '-' name2use = name if name2use is None: name2use = func.__name__.replace('_', '-') doc = inspect.getdoc(func) kwargs.setdefault('help', docstrings.get_summary( docstrings.dedents(doc) if doc else '')) parser = self._subparsers_action.add_parser(name2use, **kwargs) parser.setup_args( func, setup_as=setup_as, insert_at=insert_at, interprete=interprete, epilog_sections=epilog_sections, overwrite=overwrite, append_epilog=append_epilog) return func, parser if func is None: return lambda f: setup(f)[0] else: return setup(func)[int(return_parser)]
[docs] @docstrings.get_sectionsf('FuncArgParser.update_arg') @docstrings.dedent def update_arg(self, arg, if_existent=None, **kwargs): """ Update the `add_argument` data for the given parameter Parameters ---------- arg: str The name of the function argument if_existent: bool or None If True, the argument is updated. If None (default), the argument is only updated, if it exists. Otherwise, if False, the given ``**kwargs`` are only used if the argument is not yet existing ``**kwargs`` The keyword arguments any parameter for the :meth:`argparse.ArgumentParser.add_argument` method """ if if_existent or (if_existent is None and arg in self.unfinished_arguments): self.unfinished_arguments[arg].update(kwargs) elif not if_existent and if_existent is not None: self.unfinished_arguments.setdefault(arg, kwargs)
[docs] @docstrings.dedent def update_argf(self, arg, **kwargs): """ Update the arguments as a decorator Parameters --------- %(FuncArgParser.update_arg.parameters)s Examples -------- Use this method as a decorator:: >>> from funcargparser import FuncArgParser >>> parser = FuncArgParser() >>> @parser.update_argf('my_argument', type=int) ... def my_func(my_argument=None): ... pass >>> args = parser.parse_args('my-func -my-argument 1'.split()) >>> isinstance(args.my_argument, int) True See Also -------- update_arg""" return self._as_decorator('update_arg', arg, **kwargs)
def _as_decorator(self, funcname, *args, **kwargs): def func_decorator(func): success = False for parser in self._get_corresponding_parsers(func): getattr(parser, funcname)(*args, **kwargs) success = True if not success: raise ValueError( "Could not figure out to which this %s belongs" % func) return func return func_decorator def _get_corresponding_parsers(self, func): """Get the parser that has been set up by the given `function`""" if func in self._used_functions: yield self if self._subparsers_action is not None: for parser in self._subparsers_action.choices.values(): for sp in parser._get_corresponding_parsers(func): yield sp
[docs] def pop_arg(self, *args, **kwargs): """Delete a previously defined argument from the parser """ return self.unfinished_arguments.pop(*args, **kwargs)
[docs] def pop_argf(self, *args, **kwargs): """Delete a previously defined argument from the parser via decorators Same as :meth:`pop_arg` but it can be used as a decorator""" return self._as_decorator('pop_arg', *args, **kwargs)
[docs] def pop_key(self, arg, key, *args, **kwargs): """Delete a previously defined key for the `add_argument` """ return self.unfinished_arguments[arg].pop(key, *args, **kwargs)
[docs] def pop_keyf(self, *args, **kwargs): """Delete a previously defined key for the `add_argument` Same as :meth:`pop_key` but it can be used as a decorator""" return self._as_decorator('pop_key', *args, **kwargs)
[docs] def create_arguments(self, subparsers=False): """Create and add the arguments Parameters ---------- subparsers: bool If True, the arguments of the subparsers are also created""" ret = [] if not self._finalized: for arg, d in self.unfinished_arguments.items(): try: not_positional = int(not d.pop('positional', False)) short = d.pop('short', None) long_name = d.pop('long', None) if short is None and long_name is None: raise ValueError( "Either a short (-) or a long (--) argument must " "be provided!") if not not_positional: short = arg long_name = None d.pop('dest', None) if short == long_name: long_name = None args = [] if short: args.append('-' * not_positional + short) if long_name: args.append('--' * not_positional + long_name) group = d.pop('group', self) if d.get('action') in ['store_true', 'store_false']: d.pop('metavar', None) ret.append(group.add_argument(*args, **d)) except Exception: print('Error while creating argument %s' % arg) raise else: raise ValueError('Parser has already been finalized!') self._finalized = True if subparsers and self._subparsers_action is not None: for parser in self._subparsers_action.choices.values(): parser.create_arguments(True) return ret
[docs] def append2help(self, arg, s): """Append the given string to the help of argument `arg` Parameters ---------- arg: str The function argument s: str The string to append to the help""" self.unfinished_arguments[arg]['help'] += s
[docs] def append2helpf(self, arg, s): """Append the given string to the help of argument `arg` Parameters ---------- arg: str The function argument s: str The string to append to the help""" return self._as_decorator('append2help', arg, s)
[docs] @staticmethod def format_bold(section, text): """Make a bold formatting for the section header""" return '**%s**\n\n%s' % (section, text)
[docs] @staticmethod def format_rubric(section, text): """Make a bold formatting for the section header""" return '.. rubric:: %s\n\n%s' % (section, text)
[docs] @staticmethod def format_heading(section, text): return '\n'.join([section, '-' * len(section), text])
[docs] def format_epilog_section(self, section, text): """Format a section for the epilog by inserting a format""" try: func = self._epilog_formatters[self.epilog_formatter] except KeyError: if not callable(self.epilog_formatter): raise func = self.epilog_formatter return func(section, text)
[docs] def extract_as_epilog(self, text, sections=None, overwrite=False, append=True): """Extract epilog sections from the a docstring Parameters ---------- text The docstring to use sections: list of str The headers of the sections to extract. If None, the :attr:`epilog_sections` attribute is used overwrite: bool If True, overwrite the existing epilog append: bool If True, append to the existing epilog""" if sections is None: sections = self.epilog_sections if ((not self.epilog or overwrite or append) and sections): epilog_parts = [] for sec in sections: text = docstrings._get_section(text, sec).strip() if text: epilog_parts.append( self.format_epilog_section(sec, text)) if epilog_parts: epilog = '\n\n'.join(epilog_parts) if overwrite or not self.epilog: self.epilog = epilog else: self.epilog += '\n\n' + epilog
[docs] def grouparg(self, arg, my_arg=None, parent_cmds=[]): """ Grouper function for chaining subcommands Parameters ---------- arg: str The current command line argument that is parsed my_arg: str The name of this subparser. If None, this parser is the main parser and has no parent parser parent_cmds: list of str The available commands of the parent parsers Returns ------- str or None The grouping key for the given `arg` or None if the key does not correspond to this parser or this parser is the main parser and does not have seen a subparser yet Notes ----- Quite complicated, there is no real need to deal with this function """ if self._subparsers_action is None: return None commands = self._subparsers_action.choices currentarg = self.__currentarg # the default return value is the current argument we are in or the # name of the subparser itself ret = currentarg or my_arg if currentarg is not None: # if we are already in a sub command, we use the sub parser sp_key = commands[currentarg].grouparg(arg, currentarg, chain( commands, parent_cmds)) if sp_key is None and arg in commands: # if the subparser did not recognize the command, we use the # command the corresponds to this parser or (of this parser # is the parent parser) the current subparser self.__currentarg = currentarg = arg ret = my_arg or currentarg elif sp_key not in commands and arg in parent_cmds: # otherwise, if the subparser recognizes the commmand but it is # not in the known command of this parser, it must be another # command of the subparser and this parser can ignore it ret = None else: # otherwise the command belongs to this subparser (if this one # is not the subparser) or the current subparser ret = my_arg or currentarg elif arg in commands: # if the argument is a valid subparser, we return this one self.__currentarg = arg ret = arg elif arg in parent_cmds: # if the argument is not a valid subparser but in one of our # parents, we return None to signalize that we cannot categorize # it ret = None return ret
[docs] def parse_known_args(self, args=None, namespace=None): if self._chain_subparsers: if args is None: args = sys.argv[1:] choices_d = OrderedDict() remainders = OrderedDict() main_args = [] # get the first argument to make sure that everything works cmd = self.__currentarg = None for i, (cmd, subargs) in enumerate(groupby(args, self.grouparg)): if cmd is None: main_args += list(subargs) else: # replace '-' by underscore ns_cmd = cmd.replace('-', '_') choices_d[ns_cmd], remainders[ns_cmd] = super( FuncArgParser, self).parse_known_args( list(chain(main_args, subargs))) main_ns, remainders[None] = self.__parse_main(main_args) for key, val in vars(main_ns).items(): choices_d[key] = val self.__currentarg = None if '__dummy' in choices_d: del choices_d['__dummy'] return Namespace(**choices_d), list(chain(*remainders.values())) # otherwise, use the default behaviour return super(FuncArgParser, self).parse_known_args(args, namespace)
def __parse_main(self, args): """Parse the main arguments only. This is a work around for python 2.7 because argparse does not allow to parse arguments without subparsers """ if six.PY2: self._subparsers_action.add_parser("__dummy") return super(FuncArgParser, self).parse_known_args( list(args) + ['__dummy']) return super(FuncArgParser, self).parse_known_args(args)
[docs] @docstrings.get_sectionsf('FuncArgParser.update_short') @docstrings.dedent def update_short(self, **kwargs): """ Update the short optional arguments (those with one leading '-') This method updates the short argument name for the specified function arguments as stored in :attr:`unfinished_arguments` Parameters ---------- ``**kwargs`` Keywords must be keys in the :attr:`unfinished_arguments` dictionary (i.e. keywords of the root functions), values the short argument names Examples -------- Setting:: >>> parser.update_short(something='s', something_else='se') is basically the same as:: >>> parser.update_arg('something', short='s') >>> parser.update_arg('something_else', short='se') which in turn is basically comparable to:: >>> parser.add_argument('-s', '--something', ...) >>> parser.add_argument('-se', '--something_else', ...) See Also -------- update_shortf, update_long""" for key, val in six.iteritems(kwargs): self.update_arg(key, short=val)
[docs] @docstrings.dedent def update_shortf(self, **kwargs): """ Update the short optional arguments belonging to a function This method acts exactly like :meth:`update_short` but works as a decorator (see :meth:`update_arg` and :meth:`update_argf`) Parameters ---------- %(FuncArgParser.update_short.parameters)s Returns ------- function The function that can be used as a decorator Examples -------- Use this method as a decorator:: >>> @parser.update_shortf(something='s', something_else='se') ... def do_something(something=None, something_else=None): ... ... See also the examples in :meth:`update_short`. See Also -------- update_short, update_longf """ return self._as_decorator('update_short', **kwargs)
[docs] @docstrings.get_sectionsf('FuncArgParser.update_long') @docstrings.dedent def update_long(self, **kwargs): """ Update the long optional arguments (those with two leading '-') This method updates the short argument name for the specified function arguments as stored in :attr:`unfinished_arguments` Parameters ---------- ``**kwargs`` Keywords must be keys in the :attr:`unfinished_arguments` dictionary (i.e. keywords of the root functions), values the long argument names Examples -------- Setting:: >>> parser.update_long(something='s', something_else='se') is basically the same as:: >>> parser.update_arg('something', long='s') >>> parser.update_arg('something_else', long='se') which in turn is basically comparable to:: >>> parser.add_argument('--s', dest='something', ...) >>> parser.add_argument('--se', dest='something_else', ...) See Also -------- update_short, update_longf""" for key, val in six.iteritems(kwargs): self.update_arg(key, long=val)
[docs] @docstrings.dedent def update_longf(self, **kwargs): """ Update the long optional arguments belonging to a function This method acts exactly like :meth:`update_long` but works as a decorator (see :meth:`update_arg` and :meth:`update_argf`) Parameters ---------- %(FuncArgParser.update_long.parameters)s Returns ------- function The function that can be used as a decorator Examples -------- Use this method as a decorator:: >>> @parser.update_shortf(something='s', something_else='se') ... def do_something(something=None, something_else=None): ... ... See also the examples in :meth:`update_long`. See Also -------- update_short, update_longf """ return self._as_decorator('update_long', **kwargs)
[docs] def parse2func(self, args=None, func=None): """Parse the command line arguments to the setup function This method parses the given command line arguments to the function used in the :meth:`setup_args` method to setup up this parser Parameters ---------- args: list The list of command line arguments func: function An alternative function to use. If None, the last function or the one specified through the `setup_as` parameter in the :meth:`setup_args` is used. Returns ------- object What ever is returned by the called function Note ---- This method does not cover subparsers!""" kws = vars(self.parse_args(args)) if func is None: if self._setup_as: func = kws.pop(self._setup_as) else: func = self._used_functions[-1] return func(**kws)
[docs] def parse_known2func(self, args=None, func=None): """Parse the command line arguments to the setup function This method parses the given command line arguments to the function used in the :meth:`setup_args` method to setup up this parser Parameters ---------- args: list The list of command line arguments func: function or str An alternative function to use. If None, the last function or the one specified through the `setup_as` parameter in the :meth:`setup_args` is used. Returns ------- object What ever is returned by the called function list The remaining command line arguments that could not be interpreted Note ---- This method does not cover subparsers!""" ns, remainder = self.parse_known_args(args) kws = vars(ns) if func is None: if self._setup_as: func = kws.pop(self._setup_as) else: func = self._used_functions[-1] return func(**kws), remainder
[docs] def parse_chained(self, args=None): """ Parse the argument directly to the function used for setup This function parses the command line arguments to the function that has been used for the :meth:`setup_args`. Parameters ---------- args: list The arguments parsed to the :meth:`parse_args` function Returns ------- argparse.Namespace The namespace with mapping from command name to the function return See also -------- parse_known_chained """ kws = vars(self.parse_args(args)) return self._parse2subparser_funcs(kws)
[docs] def parse_known_chained(self, args=None): """ Parse the argument directly to the function used for setup This function parses the command line arguments to the function that has been used for the :meth:`setup_args` method. Parameters ---------- args: list The arguments parsed to the :meth:`parse_args` function Returns ------- argparse.Namespace The namespace with mapping from command name to the function return list The remaining arguments that could not be interpreted See also -------- parse_known """ ns, remainder = self.parse_known_args(args) kws = vars(ns) return self._parse2subparser_funcs(kws), remainder
def _parse2subparser_funcs(self, kws): """ Recursive function to parse arguments to chained parsers """ choices = getattr(self._subparsers_action, 'choices', {}) replaced = {key.replace('-', '_'): key for key in choices} sp_commands = set(replaced).intersection(kws) if not sp_commands: if self._setup_as is not None: func = kws.pop(self._setup_as) else: try: func = self._used_functions[-1] except IndexError: return None return func(**{ key: kws[key] for key in set(kws).difference(choices)}) else: ret = {} for key in sp_commands: ret[key.replace('-', '_')] = \ choices[replaced[key]]._parse2subparser_funcs( vars(kws[key])) return Namespace(**ret)
[docs] def get_subparser(self, name): """ Convenience method to get a certain subparser Parameters ---------- name: str The name of the subparser Returns ------- FuncArgParser The subparsers corresponding to `name` """ if self._subparsers_action is None: raise ValueError("%s has no subparsers defined!" % self) return self._subparsers_action.choices[name]