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
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='*')
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='*')
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
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
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
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
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
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)
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, })
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)