Source

argh / argh / assembling.py

Full commit
Andy Mikhailenko a1d2580 

Andy Mikhailenko 2ef4c3b 
Andy Mikhailenko a1d2580 








Andy Mikhailenko c02834f 
Andy Mikhailenko a1d2580 


Andy Mikhailenko dd4dd4c 
Andy Mikhailenko 7c0eefd 

Andy Mikhailenko 0eaaa19 
Andy Mikhailenko 159ca85 
Andy Mikhailenko b7f49c9 

Andy Mikhailenko 176ff47 
Andy Mikhailenko dd4dd4c 
Andy Mikhailenko a1d2580 

Andy Mikhailenko 7c0eefd 

















Andy Mikhailenko a1d2580 

Andy Mikhailenko 159ca85 
Andy Mikhailenko b7f49c9 
Andy Mikhailenko 159ca85 

Andy Mikhailenko dd4dd4c 
Andy Mikhailenko 6d067df 
Andy Mikhailenko 159ca85 

Andy Mikhailenko dd4dd4c 


Andy Mikhailenko 6d067df 


Andy Mikhailenko 159ca85 






Andy Mikhailenko 6d067df 
Andy Mikhailenko d9b6024 

Andy Mikhailenko 159ca85 
Andy Mikhailenko 6d067df 

Andy Mikhailenko d9b6024 
Andy Mikhailenko 6d067df 

Andy Mikhailenko d9b6024 

Andy Mikhailenko 6d067df 

Andy Mikhailenko d9b6024 
Andy Mikhailenko 159ca85 
Andy Mikhailenko 6d067df 
Andy Mikhailenko d9b6024 
Andy Mikhailenko 159ca85 
Andy Mikhailenko d9b6024 


Andy Mikhailenko 176ff47 
Andy Mikhailenko 159ca85 
Andy Mikhailenko 915a6aa 



Andy Mikhailenko 159ca85 
Andy Mikhailenko 176ff47 
Andy Mikhailenko 0fdf350 





Andy Mikhailenko 176ff47 
Andy Mikhailenko 0fdf350 
Andy Mikhailenko 4f8b689 


Andy Mikhailenko 9df2dd0 


Andy Mikhailenko 0fdf350 
Andy Mikhailenko 176ff47 
Andy Mikhailenko 9df2dd0 
Andy Mikhailenko 176ff47 

Andy Mikhailenko 0fdf350 
Andy Mikhailenko 4f8b689 


Andy Mikhailenko 0fdf350 
Andy Mikhailenko 9df2dd0 
Andy Mikhailenko 176ff47 

Andy Mikhailenko 9df2dd0 
Andy Mikhailenko 176ff47 
Andy Mikhailenko 0fdf350 

Andy Mikhailenko b7f49c9 
















Andy Mikhailenko dd4dd4c 

Andy Mikhailenko b7f49c9 












Andy Mikhailenko 43f065e 












Andy Mikhailenko a1d2580 


Andy Mikhailenko 1fce951 

Andy Mikhailenko a1d2580 



Andy Mikhailenko 1fce951 
Andy Mikhailenko a1d2580 

Andy Mikhailenko da4394b 







Andy Mikhailenko aeeb3d3 




Andy Mikhailenko a1d2580 




Andy Mikhailenko b7f49c9 

Andy Mikhailenko dd4dd4c 
Andy Mikhailenko c41ac25 
Andy Mikhailenko 159ca85 

Andy Mikhailenko d9b6024 


Andy Mikhailenko c41ac25 
Andy Mikhailenko 43f065e 





Andy Mikhailenko d9b6024 
Andy Mikhailenko c41ac25 
Andy Mikhailenko d9b6024 





Andy Mikhailenko 176ff47 
Andy Mikhailenko 43f065e 
Andy Mikhailenko c41ac25 









Andy Mikhailenko 43f065e 
Andy Mikhailenko c41ac25 



Andy Mikhailenko 43f065e 
Andy Mikhailenko d9b6024 

Andy Mikhailenko c41ac25 
Andy Mikhailenko 159ca85 


Andy Mikhailenko 0fdf350 


Andy Mikhailenko 176ff47 




Andy Mikhailenko 0eaaa19 
Andy Mikhailenko cae8ad2 
Andy Mikhailenko 0eaaa19 


Andy Mikhailenko cae8ad2 
Andy Mikhailenko 146ff3c 
Andy Mikhailenko 176ff47 
Andy Mikhailenko 159ca85 
Andy Mikhailenko a1d2580 














Andy Mikhailenko 1fce951 






Andy Mikhailenko a1d2580 





























Andy Mikhailenko 1fce951 


Andy Mikhailenko a1d2580 













Andy Mikhailenko 1fce951 
Andy Mikhailenko a1d2580 



















Andy Mikhailenko 7c0eefd 













Andy Mikhailenko a1d2580 
# -*- coding: utf-8 -*-
#
#  Copyright (c) 2010—2013 Andrey Mikhaylenko and contributors
#
#  This file is part of Argh.
#
#  Argh is free software under terms of the GNU Lesser
#  General Public License version 3 (LGPLv3) as published by the Free
#  Software Foundation. See the file README for copying conditions.
#
"""
Assembling
~~~~~~~~~~

Functions and classes to properly assemble your commands in a parser.
"""
import sys
import argparse

from argh.completion import COMPLETION_ENABLED
from argh.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME,
                            ATTR_INFER_ARGS_FROM_SIGNATURE,
                            ATTR_EXPECTS_NAMESPACE_OBJECT)
from argh.utils import get_subparsers
from argh import compat


__all__ = ['SUPPORTS_ALIASES', 'set_default_command', 'add_commands']


def _check_support_aliases():
    p = argparse.ArgumentParser()
    s = p.add_subparsers()
    try:
        s.add_parser('x', aliases=[])
    except TypeError:
        return False
    else:
        return True


SUPPORTS_ALIASES = _check_support_aliases()
""" Calculated on load. If `True`, current version of argparse supports
alternative command names (can be set via :func:`~argh.decorators.aliases`).
"""


def _get_args_from_signature(function):
    if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
        return

    spec = compat.getargspec(function)

    kwargs = dict(zip(*[reversed(x) for x in (spec.args, spec.defaults or [])]))

    if sys.version_info < (3,0):
        annotations = {}
    else:
        annotations = dict((k,v) for k,v in function.__annotations__.items()
                           if isinstance(v, str))

    # define the list of conflicting option strings
    # (short forms, i.e. single-character ones)
    chars = [a[0] for a in spec.args]
    char_counts = dict((char, chars.count(char)) for char in set(chars))
    conflicting_opts = tuple(char for char in char_counts
                             if 1 < char_counts[char])

    for name in spec.args:
        flags = []    # name_or_flags
        akwargs = {}  # keyword arguments for add_argument()

        if name in annotations:
            # help message:  func(a : "b")  ->  add_argument("a", help="b")
            akwargs.update(help=annotations.get(name))

        if name in kwargs:
            akwargs.update(default=kwargs.get(name))
            flags = ('-{0}'.format(name[0]), '--{0}'.format(name))
            if name.startswith(conflicting_opts):
                # remove short name
                flags = flags[1:]
        else:
            # positional argument
            flags = (name,)

        # cmd(foo_bar)  ->  add_argument('foo-bar')
        flags = tuple(x.replace('_', '-') for x in flags)

        yield dict(option_strings=flags, **akwargs)

    if spec.varargs:
        # *args
        yield dict(option_strings=[spec.varargs], nargs='*')


def _guess(kwargs):
    """
    Adds types, actions, etc. to given argument specification.
    For example, ``default=3`` implies ``type=int``.

    :param arg: a :class:`argh.utils.Arg` instance
    """
    guessed = {}

    TYPE_AWARE_ACTIONS = 'store', 'append'
    "Parser actions that accept argument 'type'."

    # guess type/action from default value
    value = kwargs.get('default')
    if value is not None:
        if isinstance(value, bool):
            if kwargs.get('action') is None:
                # infer action from default value
                guessed['action'] = 'store_false' if value else 'store_true'
        elif kwargs.get('type') is None:
            # infer type from default value
            # (make sure that action handler supports this keyword)
            if kwargs.get('action', 'store') in TYPE_AWARE_ACTIONS:
                guessed['type'] = type(value)

    # guess type from choices (first item)
    if kwargs.get('choices') and 'type' not in list(guessed) + list(kwargs):
        guessed['type'] = type(kwargs['choices'][0])

    return dict(kwargs, **guessed)


def _fix_compat_issue29(function):
    #
    # TODO: remove before 1.0 release (will break backwards compatibility)
    #
    if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
        # a modern decorator is used, no compatibility issues
        return function

    if getattr(function, ATTR_INFER_ARGS_FROM_SIGNATURE, False):
        # wrapped in outdated decorator but it implies modern behaviour
        return function

    # Okay, now we've got either a modern-style function (plain signature)
    # or an old-style function which implicitly expects a namespace object.
    # It's very likely that in the latter case the function accepts one and
    # only argument named "args".  If so, we simply wrap this function in
    # @expects_obj and issue a warning.
    spec = compat.getargspec(function)

    if spec.args == ['args']:
        # this is it -- a classic old-style function, goddamnit.
        # no checking *args and **kwargs because they are unlikely to matter.
        import warnings
        warnings.warn('Function {0} is very likely to be old-style, i.e. '
                      'implicitly expects a namespace object.  This behaviour '
                      'is deprecated.  Wrap it in @expects_obj decorator or '
                      'convert to plain signature.'.format(function.__name__),
                      DeprecationWarning)
        setattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, True)
    return function


def _get_dest(parser, argspec):
    argspec = argspec.copy()    # parser methods modify source data
    args = argspec['option_strings']
    assert args

    if 1 < len(args) or args[0][0].startswith(tuple(parser.prefix_chars)):
        kwargs = parser._get_optional_kwargs(*args, **argspec)
    else:
        kwargs = parser._get_positional_kwargs(*args, **argspec)

    return kwargs['dest']


def set_default_command(parser, function):
    """ Sets default command (i.e. a function) for given parser.

    If `parser.description` is empty and the function has a docstring,
    it is used as the description.

    .. note::

       An attempt to set default command to a parser which already has
       subparsers (e.g. added with :func:`~argh.assembling.add_commands`)
       results in a `RuntimeError`.

    .. note::

       If there are both explicitly declared arguments (e.g. via
       :func:`~argh.decorators.arg`) and ones inferred from the function
       signature (e.g. via :func:`~argh.decorators.command`), declared ones
       will be merged into inferred ones. If an argument does not conform
       function signature, `ValueError` is raised.

    .. note::

       If the parser was created with ``add_help=True`` (which is by default),
       option name ``-h`` is silently removed from any argument.

    """
    if parser._subparsers:
        raise RuntimeError('Cannot set default command to a parser with '
                           'existing subparsers')

    function = _fix_compat_issue29(function)

    spec = compat.getargspec(function)

    declared_args = getattr(function, ATTR_ARGS, [])
    inferred_args = list(_get_args_from_signature(function))
    if inferred_args and declared_args:
        # We've got a mixture of declared and inferred arguments

        # FIXME this is an ugly hack for preserving order
        dests = []
        inferred_dict = {}
        for x in inferred_args:
            dest = _get_dest(parser, x)
            dests.append(dest)
            inferred_dict[dest] = x

        for kw in declared_args:
            # 1) make sure that this declared arg conforms to the function
            #    signature and therefore only refines an inferred arg:
            #
            #      @arg('foo')    maps to  func(foo)
            #      @arg('--bar')  maps to  func(bar=...)
            #

            dest = _get_dest(parser, kw)
            if dest in inferred_dict:
                # 2) merge declared args into inferred ones (e.g. help=...)
                inferred_dict[dest].update(**kw)
            else:
                varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
                if varkw:
                    # function accepts **kwargs
                    dests.append(dest)
                    inferred_dict[dest] = kw
                else:
                    xs = (inferred_dict[x]['option_strings'] for x in dests)
                    raise ValueError('{func}: argument {flags} does not fit '
                                     'function signature: {sig}'.format(
                                        flags=', '.join(kw['option_strings']),
                                        func=function.__name__,
                                        sig=', '.join('/'.join(x) for x in xs)))

        # pack the modified data back into a list
        inferred_args = [inferred_dict[x] for x in dests]

    command_args = inferred_args or declared_args

    # add types, actions, etc. (e.g. default=3 implies type=int)
    command_args = [_guess(x) for x in command_args]

    for draft in command_args:
        draft = draft.copy()
        dest_or_opt_strings = draft.pop('option_strings')
        if parser.add_help and '-h' in dest_or_opt_strings:
            dest_or_opt_strings = [x for x in dest_or_opt_strings if x != '-h']
        completer = draft.pop('completer', None)
        try:
            action = parser.add_argument(*dest_or_opt_strings, **draft)
            if COMPLETION_ENABLED and completer:
                action.completer = completer
        except Exception as e:
            raise type(e)('{func}: cannot add arg {args}: {msg}'.format(
                args='/'.join(dest_or_opt_strings), func=function.__name__, msg=e))

    if function.__doc__ and not parser.description:
        parser.description = function.__doc__
    parser.set_defaults(function=function)


def add_commands(parser, functions, namespace=None, title=None,
                 description=None, help=None):
    """Adds given functions as commands to given parser.

    :param parser:

        an :class:`argparse.ArgumentParser` instance.

    :param functions:

        a list of functions. A subparser is created for each of them.
        If the function is decorated with :func:`~argh.decorators.arg`, the
        arguments are passed to :class:`argparse.ArgumentParser.add_argument`.
        See also :func:`~argh.dispatching.dispatch` for requirements
        concerning function signatures. The command name is inferred from the
        function name. Note that the underscores in the name are replaced with
        hyphens, i.e. function name "foo_bar" becomes command name "foo-bar".

    :param namespace:

        an optional string representing the group of commands. For example, if
        a command named "hello" is added without the namespace, it will be
        available as "prog.py hello"; if the namespace if specified as "greet",
        then the command will be accessible as "prog.py greet hello". The
        namespace itself is not callable, so "prog.py greet" will fail and only
        display a help message.

    Help message for a namespace can be also tuned with these params (provided
    that you specify the `namespace`):

    :param title:

        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as `title`.

    :param description:

        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as
        `description`.

    :param help:

        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as `help`.

    .. note::

        This function modifies the parser object. Generally side effects are
        bad practice but we don't seem to have any choice as ArgumentParser is
        pretty opaque.
        You may prefer :class:`~argh.helpers.ArghParser.add_commands` for a bit
        more predictable API.

    .. admonition:: Design flaw

        This function peeks into the parser object using its internal API.
        Unfortunately the public API does not allow to *get* the subparsers, it
        only lets you *add* them, and do that *once*. So you would have to toss
        the subparsers object around to add something later. That said, I doubt
        that argparse will change a lot in the future as it's already pretty
        stable. If some implementation details would change and break `argh`,
        we'll simply add a workaround a keep it compatibile.

    .. note::

       An attempt to add commands to a parser which already has a default
       function (e.g. added with :func:`~argh.assembling.set_default_command`)
       results in a `RuntimeError`.

    """
    if 'function' in parser._defaults:
        raise RuntimeError('Cannot add commands to a single-command parser')

    subparsers = get_subparsers(parser, create=True)

    if namespace:
        # make a namespace placeholder and register the commands within it
        subsubparser = subparsers.add_parser(namespace, help=title)
        subparsers = subsubparser.add_subparsers(title=title,
                                                 description=description,
                                                 help=help)
    else:
        assert not any([title, description, help]), (
            'Arguments "title", "description" or "extra_help" only make sense '
            'if provided along with a namespace.')

    for func in functions:
        # use explicitly defined name; if none, use function name (a_b → a-b)
        cmd_name = getattr(func, ATTR_NAME,
                           func.__name__.replace('_','-'))
        parser_kwargs = {}

        # add command help from function's docstring
        parser_kwargs['help'] = func.__doc__

        # try adding aliases for command name
        if SUPPORTS_ALIASES:
            parser_kwargs['aliases'] = getattr(func, ATTR_ALIASES, [])

        # create and set up the parser for this command
        command_parser = subparsers.add_parser(cmd_name, **parser_kwargs)
        set_default_command(command_parser, func)