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
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
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
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
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
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 _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
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
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)