Example #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 = get_arg_spec(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
Example #2
0
def _get_args_from_signature(function):
    if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
        return

    spec = get_arg_spec(function)

    defaults = dict(zip(*[reversed(x) for x in (spec.args,
                                                spec.defaults or [])]))
    defaults.update(getattr(spec, 'kwonlydefaults', None) or {})

    kwonly = getattr(spec, 'kwonlyargs', [])

    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 + kwonly]
    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 + kwonly:
        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 defaults or name in kwonly:
            if name in defaults:
                akwargs.update(default=defaults.get(name))
            else:
                akwargs.update(required=True)
            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('_', '-') if x.startswith('-') else x
                      for x in flags)

        yield dict(option_strings=flags, **akwargs)

    if spec.varargs:
        # *args
        yield dict(option_strings=[spec.varargs], nargs='*')
Example #3
0
def _get_args_from_signature(function):
    if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
        return

    spec = get_arg_spec(function)

    defaults = dict(
        zip(*[reversed(x) for x in (spec.args, spec.defaults or [])]))
    defaults.update(getattr(spec, 'kwonlydefaults', None) or {})

    kwonly = getattr(spec, 'kwonlyargs', [])

    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 + kwonly]
    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 + kwonly:
        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 defaults or name in kwonly:
            if name in defaults:
                akwargs.update(default=defaults.get(name))
            else:
                akwargs.update(required=True)
            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('_', '-') if x.startswith('-') else x for x in flags)

        yield dict(option_strings=flags, **akwargs)

    if spec.varargs:
        # *args
        yield dict(option_strings=[spec.varargs], nargs='*')
Example #4
0
 def __init__(self, function):
     self.function_name = function.__name__
     spec = get_arg_spec(function)
     args_len = len(spec.args)
     defaults = spec.defaults or []
     if len(defaults) < args_len:
         prefix = [_nop for _ in range(args_len - len(defaults))]
         defaults = prefix + list(defaults)
     self.types = defaults
     self.len_types = len(self.types)
     self.has_varargs = spec.varargs is not None
Example #5
0
    def wrapper(func):
        spec = get_arg_spec(func)

        toggleables = getattr(func, ATTR_TOGGLEABLES, [])
        
        for (dest, default) in zip(spec.args[-len(spec.defaults):], spec.defaults):
            if isinstance(default, bool):
                cmd_dest = dest.replace('_', '-')
                toggleables.append(('--' + cmd_dest, inv_prefix))

        setattr(func, ATTR_TOGGLEABLES, toggleables)
        return func
Example #6
0
def _assert_spec(f, **overrides):
    spec = get_arg_spec(f)

    defaults = {
        'args': ['x', 'y'],
        'varargs': None,
        'varkw': None,
        'defaults': (0, ),
        'kwonlyargs': [],
        'annotations': {},
    }

    for k in defaults:
        actual = getattr(spec, k)
        expected = overrides[k] if k in overrides else defaults[k]
        assert actual == expected
Example #7
0
def _assert_spec(f, **overrides):
    spec = get_arg_spec(f)

    defaults = {
        'args': ['x', 'y'],
        'varargs': None,
        'varkw': None,
        'defaults': (0,),
        'kwonlyargs': [],
        'annotations': {},
    }

    for k in defaults:
        actual = getattr(spec, k)
        expected = overrides[k] if k in overrides else defaults[k]
        assert actual == expected
Example #8
0
    def _call():
        # Actually call the function
        if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
            result = function(namespace_obj)
        else:
            # namespace -> dictionary
            _flat_key = lambda key: key.replace('-', '_')
            all_input = dict(
                (_flat_key(k), v) for k, v in vars(namespace_obj).items())

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

            spec = get_arg_spec(function)

            positional = [all_input[k] for k in spec.args]
            kwonly = getattr(spec, 'kwonlyargs', [])
            keywords = dict((k, all_input[k]) for k in kwonly)

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

            # **kwargs
            varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
            if varkw:
                not_kwargs = [DEST_FUNCTION] + spec.args + [spec.varargs
                                                            ] + kwonly
                for k in vars(namespace_obj):
                    if k.startswith('_') or k in not_kwargs:
                        continue
                    keywords[k] = getattr(namespace_obj, k)

            result = 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
    def _call():
        # Actually call the function
        if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
            result = function(namespace_obj)
        else:
            # namespace -> dictionary
            _flat_key = lambda key: key.replace('-', '_')
            all_input = dict((_flat_key(k), v)
                             for k,v in vars(namespace_obj).items())

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

            spec = get_arg_spec(function)

            positional = [all_input[k] for k in spec.args]
            kwonly = getattr(spec, 'kwonlyargs', [])
            keywords = dict((k, all_input[k]) for k in kwonly)

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

            # **kwargs
            varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
            if varkw:
                not_kwargs = [DEST_FUNCTION] + spec.args + [spec.varargs] + kwonly
                for k in vars(namespace_obj):
                    if k.startswith('_') or k in not_kwargs:
                        continue
                    keywords[k] = getattr(namespace_obj, k)

            result = 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
Example #10
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, `AssemblingError` 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 = get_arg_spec(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

        # a mapping of "dest" strings to argument declarations.
        #
        # * a "dest" string is a normalized form of argument name, i.e.:
        #
        #     '-f', '--foo' → 'foo'
        #     'foo-bar'     → 'foo_bar'
        #
        # * argument declaration is a dictionary representing an argument;
        #   it is obtained either from _get_args_from_signature() or from
        #   an @arg decorator (as is).
        #
        dests = OrderedDict()

        for argspec in inferred_args:
            dest = _get_parser_param_kwargs(parser, argspec)['dest']
            dests[dest] = argspec

        for declared_kw in declared_args:
            # an argument is declared via decorator
            dest = _get_dest(parser, declared_kw)
            if dest in dests:
                # the argument is already known from function signature
                #
                # now make sure that this declared arg conforms to the function
                # signature and therefore only refines an inferred arg:
                #
                #      @arg('my-foo')    maps to  func(my_foo)
                #      @arg('--my-bar')  maps to  func(my_bar=...)

                # either both arguments are positional or both are optional
                decl_positional = _is_positional(declared_kw['option_strings'])
                infr_positional = _is_positional(dests[dest]['option_strings'])
                if decl_positional != infr_positional:
                    kinds = {True: 'positional', False: 'optional'}
                    raise AssemblingError(
                        '{func}: argument "{dest}" declared as {kind_i} '
                        '(in function signature) and {kind_d} (via decorator)'
                        .format(
                            func=function.__name__,
                            dest=dest,
                            kind_i=kinds[infr_positional],
                            kind_d=kinds[decl_positional],
                        ))

                # merge explicit argument declaration into the inferred one
                # (e.g. `help=...`)
                dests[dest].update(**declared_kw)
            else:
                # the argument is not in function signature
                varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
                if varkw:
                    # function accepts **kwargs; the argument goes into it
                    dests[dest] = declared_kw
                else:
                    # there's no way we can map the argument declaration
                    # to function signature
                    xs = (dests[x]['option_strings'] for x in dests)
                    raise AssemblingError(
                        '{func}: argument {flags} does not fit '
                        'function signature: {sig}'.format(
                            flags=', '.join(declared_kw['option_strings']),
                            func=function.__name__,
                            sig=', '.join('/'.join(x) for x in xs)))

        # pack the modified data back into a list
        inferred_args = dests.values()

    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)
Example #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 `AssemblingError`.

    .. 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, `AssemblingError` 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:
        _require_support_for_default_command_with_subparsers()

    spec = get_arg_spec(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

        # a mapping of "dest" strings to argument declarations.
        #
        # * a "dest" string is a normalized form of argument name, i.e.:
        #
        #     '-f', '--foo' → 'foo'
        #     'foo-bar'     → 'foo_bar'
        #
        # * argument declaration is a dictionary representing an argument;
        #   it is obtained either from _get_args_from_signature() or from
        #   an @arg decorator (as is).
        #
        dests = OrderedDict()

        for argspec in inferred_args:
            dest = _get_parser_param_kwargs(parser, argspec)['dest']
            dests[dest] = argspec

        for declared_kw in declared_args:
            # an argument is declared via decorator
            dest = _get_dest(parser, declared_kw)
            if dest in dests:
                # the argument is already known from function signature
                #
                # now make sure that this declared arg conforms to the function
                # signature and therefore only refines an inferred arg:
                #
                #      @arg('my-foo')    maps to  func(my_foo)
                #      @arg('--my-bar')  maps to  func(my_bar=...)

                # either both arguments are positional or both are optional
                decl_positional = _is_positional(declared_kw['option_strings'])
                infr_positional = _is_positional(dests[dest]['option_strings'])
                if decl_positional != infr_positional:
                    kinds = {True: 'positional', False: 'optional'}
                    raise AssemblingError(
                        '{func}: argument "{dest}" declared as {kind_i} '
                        '(in function signature) and {kind_d} (via decorator)'.
                        format(
                            func=function.__name__,
                            dest=dest,
                            kind_i=kinds[infr_positional],
                            kind_d=kinds[decl_positional],
                        ))

                # merge explicit argument declaration into the inferred one
                # (e.g. `help=...`)
                dests[dest].update(**declared_kw)
            else:
                # the argument is not in function signature
                varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
                if varkw:
                    # function accepts **kwargs; the argument goes into it
                    dests[dest] = declared_kw
                else:
                    # there's no way we can map the argument declaration
                    # to function signature
                    xs = (dests[x]['option_strings'] for x in dests)
                    raise AssemblingError(
                        '{func}: argument {flags} does not fit '
                        'function signature: {sig}'.format(
                            flags=', '.join(declared_kw['option_strings']),
                            func=function.__name__,
                            sig=', '.join('/'.join(x) for x in xs)))

        # pack the modified data back into a list
        inferred_args = dests.values()

    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()
        if 'help' not in draft:
            draft.update(help=DEFAULT_ARGUMENT_TEMPLATE)
        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(**{
        DEST_FUNCTION: function,
    })
Example #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, `AssemblingError` 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')

    spec = get_arg_spec(function)

    declared_args = getattr(function, ATTR_ARGS, [])
    inferred_args = list(_get_args_from_signature(function))

    toggleables = getattr(function, ATTR_TOGGLEABLES, [])

    if inferred_args and declared_args:
        # We've got a mixture of declared and inferred arguments

        # a mapping of "dest" strings to argument declarations.
        #
        # * a "dest" string is a normalized form of argument name, i.e.:
        #
        #     '-f', '--foo' → 'foo'
        #     'foo-bar'     → 'foo_bar'
        #
        # * argument declaration is a dictionary representing an argument;
        #   it is obtained either from _get_args_from_signature() or from
        #   an @arg decorator (as is).
        #
        dests = OrderedDict()

        for argspec in inferred_args:
            dest = _get_parser_param_kwargs(parser, argspec)['dest']
            dests[dest] = argspec

        for declared_kw in declared_args:
            # an argument is declared via decorator
            dest = _get_dest(parser, declared_kw)
            if dest in dests:
                # the argument is already known from function signature
                #
                # now make sure that this declared arg conforms to the function
                # signature and therefore only refines an inferred arg:
                #
                #      @arg('my-foo')    maps to  func(my_foo)
                #      @arg('--my-bar')  maps to  func(my_bar=...)

                # either both arguments are positional or both are optional
                decl_positional = _is_positional(declared_kw['option_strings'])
                infr_positional = _is_positional(dests[dest]['option_strings'])
                if decl_positional != infr_positional:
                    kinds = {True: 'positional', False: 'optional'}
                    raise AssemblingError(
                        '{func}: argument "{dest}" declared as {kind_i} '
                        '(in function signature) and {kind_d} (via decorator)'
                        .format(
                            func=function.__name__,
                            dest=dest,
                            kind_i=kinds[infr_positional],
                            kind_d=kinds[decl_positional],
                        ))

                # merge explicit argument declaration into the inferred one
                # (e.g. `help=...`)
                dests[dest].update(**declared_kw)
            else:
                # the argument is not in function signature
                varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
                if varkw:
                    # function accepts **kwargs; the argument goes into it
                    dests[dest] = declared_kw
                else:
                    # there's no way we can map the argument declaration
                    # to function signature
                    xs = (dests[x]['option_strings'] for x in dests)
                    raise AssemblingError(
                        '{func}: argument {flags} does not fit '
                        'function signature: {sig}'.format(
                            flags=', '.join(declared_kw['option_strings']),
                            func=function.__name__,
                            sig=', '.join('/'.join(x) for x in xs)))

        # pack the modified data back into a list
        inferred_args = dests.values()

    opt_string_togmap = {}

    for toggleable, inv_prefix in [(t, i) for t,i in toggleables]:
        if toggleable.find('_') >= 0:
            raise AssemblingError("Toggleable destinations cannot contain underscores")

        #for each toggleable, verify there exists a matching destination
        matched_dest = False
        for arg in inferred_args:
            for opt_str in arg['option_strings']:
                if opt_str == toggleable:
                    matched_dest = True
                    opt_string_togmap[opt_str] = (toggleable[2:], inv_prefix)
        if not matched_dest:
            raise AssemblingError("Unrecognized destination for toggleable: {}".format(toggleable))
        
    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()
        if 'help' not in draft:
            draft.update(help=DEFAULT_ARGUMENT_TEMPLATE)

        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:
            if dest_or_opt_strings[-1] in opt_string_togmap:
                # if we're working with a toggleable list of opt_strings, make mutually exclusive
                 # and set to opposite defaults & storing actions
                toggleable, inv_prefix = opt_string_togmap[dest_or_opt_strings[-1]]
                group = parser.add_mutually_exclusive_group()

                draft['action'] = 'store_true'
                draft['dest'] = toggleable.replace('-', '_')

                # XXX unsure about desired behavior in autocompletion of toggleables case
                action = group.add_argument(*dest_or_opt_strings, **draft)

                not_dest_or_opt_strings = tuple(map(lambda x : '--{}-{}'.format(inv_prefix, x.lstrip('-')), 
                                                    dest_or_opt_strings))

                draft['action'] = 'store_false'

                group.add_argument(*not_dest_or_opt_strings, **draft)
            else:
                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)