Source code for dsargparse

#
# dsargparse.py
#
# Copyright (c) 2016 Junpei Kawamoto
#
# This software is released under the MIT License.
#
# http://opensource.org/licenses/mit-license.php
#
"""dsargparse: docstring based argparse.

dsargparse is a wrapper of argparse library which prepares helps and descriptions
from docstrings. It also sets up functions to be run for each sub command,
and provides a helper function which parses args and run a selected command.
"""
import argparse
import itertools
import inspect
import textwrap

# Load objects defined in argparse.
for name in argparse.__all__:
    if name != "ArgumentParser":
        globals()[name] = getattr(argparse, name)
__all__ = argparse.__all__


_HELP = "help"
_DESCRIPTION = "description"
_FORMAT_CLASS = "formatter_class"

_KEYWORDS_ARGS = ("Args:",)
_KEYWORDS_OTHERS = ("Returns:", "Raises:", "Yields:", "Usage:")
_KEYWORDS = _KEYWORDS_ARGS + _KEYWORDS_OTHERS


def _checker(keywords):
    """Generate a checker which tests a given value not starts with keywords."""
    def _(v):
        """Check a given value matches to keywords."""
        for k in keywords:
            if k in v:
                return False
        return True
    return _


def _parse_doc(doc):
    """Parse a docstring.

    Parse a docstring and extract three components; headline, description,
    and map of arguments to help texts.

    Args:
      doc: docstring.

    Returns:
      a dictionary.
    """
    lines = doc.split("\n")
    descriptions = list(itertools.takewhile(_checker(_KEYWORDS), lines))

    if len(descriptions) < 3:
        description = lines[0]
    else:
        description = "{0}\n\n{1}".format(
            lines[0], textwrap.dedent("\n".join(descriptions[2:])))

    args = list(itertools.takewhile(
        _checker(_KEYWORDS_OTHERS),
        itertools.dropwhile(_checker(_KEYWORDS_ARGS), lines)))
    argmap = {}
    if len(args) > 1:
        for pair in args[1:]:
            kv = [v.strip() for v in pair.split(":")]
            if len(kv) >= 2:
                argmap[kv[0]] = ":".join(kv[1:])

    return dict(headline=descriptions[0], description=description, args=argmap)


class _SubparsersWrapper(object):
    """Wrapper of the action object made by argparse.ArgumentParser.add_subparsers.

    To create an instance, the constructor takes a reference to an instance of
    the action class.
    """
    __slots__ = ("__delegate")

    def __init__(self, delegate):
        self.__delegate = delegate

    def add_parser(self, func=None, name=None, **kwargs):
        """Add parser.

        This method makes a new sub command parser. It takes same arguments
        as add_parser() of the action class made by
        argparse.ArgumentParser.add_subparsers.

        In addition to, it takes one positional argument `func`, which is the
        function implements process of this sub command. The `func` will be used
        to determine the name, help, and description of this sub command. The
        function `func` will also be set as a default value of `cmd` attribute.

        If you want to choose name of this sub command, use keyword argument
        `name`.

        Args:
          func: function implements the process of this command.
          name: name of this command. If not give, the function name is used.

        Returns:
          new ArgumentParser object.

        Raises:
          ValueError: if the given function does not have docstrings.
        """
        if func:
            if not func.__doc__:
                raise ValueError(
                    "No docstrings given in {0}".format(func.__name__))

            info = _parse_doc(func.__doc__)
            if _HELP not in kwargs or not kwargs[_HELP]:
                kwargs[_HELP] = info["headline"]
            if _DESCRIPTION not in kwargs or not kwargs[_DESCRIPTION]:
                kwargs[_DESCRIPTION] = info["description"]
            if _FORMAT_CLASS not in kwargs or not kwargs[_FORMAT_CLASS]:
                kwargs[_FORMAT_CLASS] = argparse.RawTextHelpFormatter

            if not name:
                name = func.__name__ if hasattr(func, "__name__") else func

            res = self.__delegate.add_parser(name, argmap=info["args"], **kwargs)
            res.set_defaults(cmd=func)

        else:
            res = self.__delegate.add_parser(name, **kwargs)

        return res

    def __repr__(self):
        return self.__delegate.__repr__()


[docs]class ArgumentParser(argparse.ArgumentParser): """Customized ArgumentParser. This customized ArgumentParser will add help and description automatically based on docstrings of main module and functions implements processes of each command. It also provides :meth:`parse_and_run` method which helps parsing arguments and executing functions. This class takes same arguments as argparse.ArgumentParser to construct a new instance. Additionally, it has a positional argument ``main``, which takes the main function of the script ``dsargparse`` library called. From the main function, it extracts doctstings to set command descriptions. """ def __init__(self, main=None, argmap=None, *args, **kwargs): if main: if _DESCRIPTION not in kwargs or not kwargs[_DESCRIPTION]: info = _parse_doc(inspect.getmodule(main).__doc__) kwargs[_DESCRIPTION] = info[_DESCRIPTION] if _FORMAT_CLASS not in kwargs or not kwargs[_FORMAT_CLASS]: kwargs[_FORMAT_CLASS] = argparse.RawTextHelpFormatter self.__argmap = argmap if argmap else {} super(ArgumentParser, self).__init__(*args, **kwargs)
[docs] def add_subparsers(self, **kwargs): """Add subparsers. Keyword Args: same keywords arguments as ``argparse.ArgumentParser.add_subparsers``. Returns: an instance of action class which is used to add sub parsers. """ return _SubparsersWrapper( super(ArgumentParser, self).add_subparsers(**kwargs))
[docs] def add_argument(self, *args, **kwargs): """Add an argument. This method adds a new argument to the current parser. The function is same as ``argparse.ArgumentParser.add_argument``. However, this method tries to determine help messages for the adding argument from some docstrings. If the new arguments belong to some sub commands, the docstring of a function implements behavior of the sub command has ``Args:`` section, and defines same name variable, this function sets such definition to the help message. Positional Args: same positional arguments as argparse.ArgumentParser.add_argument. Keyword Args: same keywards arguments as argparse.ArgumentParser.add_argument. """ if _HELP not in kwargs: for name in args: name = name.replace("-", "") if name in self.__argmap: kwargs[_HELP] = self.__argmap[name] break return super(ArgumentParser, self).add_argument(*args, **kwargs)
[docs] def parse_and_run(self, **kwargs): """Parse arguments and run the selected command. Keyword Args: same keywords arguments as ``argparse.ArgumentParser.parse_args``. Returns: any value the selected command returns. It could be ``None``. """ return self._dispatch(**vars(self.parse_args(**kwargs)))
@staticmethod def _dispatch(cmd, **kwargs): """Dispatch parsed arguments to a command to be run. """ return cmd(**kwargs)