Exemple #1
0
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 in [['arg'], ['args'], ['self', 'arg'], ['self', '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
Exemple #2
0
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 in [['arg'], ['args'], ['self', 'arg'], ['self', '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
Exemple #3
0
def get_arg_spec(function):
    """Returns argument specification for given function.  Omits special
    arguments of instance methods (`self`) and static methods (usually `cls`
    or something like this).
    """
    spec = compat.getargspec(function)
    if inspect.ismethod(function):
        spec = spec._replace(args=spec.args[1:])
    return spec
Exemple #4
0
def get_arg_spec(function):
    """
    Returns argument specification for given function.  Omits special
    arguments of instance methods (`self`) and static methods (usually `cls`
    or something like this).
    """
    spec = compat.getargspec(function)
    if inspect.ismethod(function):
        spec = spec._replace(args=spec.args[1:])
    return spec
Exemple #5
0
def get_arg_names(function):
    """Returns argument names for given function.  Omits special arguments
    of instance methods (`self`) and static methods (usually `cls` or something
    like this).
    """
    spec = compat.getargspec(function)
    names = spec.args

    if not names:
        return []

    if inspect.ismethod(function):
        return names[1:]
    else:
        return names
Exemple #6
0
def get_arg_names(function):
    """Returns argument names for given function.  Omits special arguments
    of instance methods (`self`) and static methods (usually `cls` or something
    like this).
    """
    spec = compat.getargspec(function)
    names = spec.args

    if not names:
        return []

    if inspect.ismethod(function):
        return names[1:]
    else:
        return names
Exemple #7
0
def _get_args_from_signature(function):
    if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
        return

    spec = compat.getargspec(function)
    names = get_arg_names(function)

    kwargs = dict(zip(*[reversed(x) for x in (names, 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 names]
    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 names:
        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='*')
Exemple #8
0
def _get_args_from_signature(function):
    if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
        return

    spec = compat.getargspec(function)
    names = get_arg_names(function)

    kwargs = dict(zip(*[reversed(x) for x in (names, 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 names]
    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 names:
        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 _call():
        # Actually call the function
        if getattr(args.function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
            result = args.function(args)
        else:
            # namespace -> dictionary
            _flat_key = lambda key: key.replace('-', '_')
            all_input = dict((_flat_key(k), v) for k,v in  vars(args).items())

            # filter the namespace variables so that only those expected by the
            # actual function will pass

            spec = compat.getargspec(args.function)
            names = get_arg_names(args.function)

            positional = [all_input[k] for k in names]
            keywords = {}

            # *args
            if spec.varargs:
                positional += getattr(args, spec.varargs)

            # **kwargs
            varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
            if varkw:
                not_kwargs = ['function'] + spec.args + [spec.varargs]
                extra = [k for k in vars(args) if k not in not_kwargs]
                for k in extra:
                    keywords[k] = getattr(args, k)

            result = args.function(*positional, **keywords)

        # Yield the results
        if isinstance(result, (GeneratorType, list, tuple)):
            # yield each line ASAP, convert CommandError message to a line
            for line in result:
                yield line
        else:
            # yield non-empty non-iterable result as a single line
            if result is not None:
                yield result
Exemple #10
0
    def _call():
        # Actually call the function
        if getattr(args.function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
            result = args.function(args)
        else:
            # namespace -> dictionary
            _flat_key = lambda key: key.replace('-', '_')
            all_input = dict((_flat_key(k), v) for k, v in vars(args).items())

            # filter the namespace variables so that only those expected by the
            # actual function will pass

            spec = compat.getargspec(args.function)
            names = get_arg_names(args.function)

            positional = [all_input[k] for k in names]
            keywords = {}

            # *args
            if spec.varargs:
                positional += getattr(args, spec.varargs)

            # **kwargs
            varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
            if varkw:
                not_kwargs = ['function'] + spec.args + [spec.varargs]
                extra = [k for k in vars(args) if k not in not_kwargs]
                for k in extra:
                    keywords[k] = getattr(args, k)

            result = args.function(*positional, **keywords)

        # Yield the results
        if isinstance(result, (GeneratorType, list, tuple)):
            # yield each line ASAP, convert CommandError message to a line
            for line in result:
                yield line
        else:
            # yield non-empty non-iterable result as a single line
            if result is not None:
                yield result
Exemple #11
0
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)
Exemple #12
0
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)