# coding: utf-8 # # Copyright © 2010—2014 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.rst for copying conditions. # """ Dispatching ~~~~~~~~~~~ """ import argparse import sys from types import GeneratorType from argh import compat, io from argh.constants import ( ATTR_WRAPPED_EXCEPTIONS, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, ATTR_EXPECTS_NAMESPACE_OBJECT, PARSER_FORMATTER, DEST_FUNCTION, ) from argh.completion import autocomplete from argh.assembling import add_commands, set_default_command from argh.exceptions import DispatchingError, CommandError from argh.utils import get_arg_spec __all__ = ['dispatch', 'dispatch_command', 'dispatch_commands', 'PARSER_FORMATTER', 'EntryPoint'] class ArghNamespace(argparse.Namespace): """ A namespace object which collects the stack of functions (the :attr:`~argh.constants.DEST_FUNCTION` arguments passed to it via parser's defaults). """ def __init__(self, *args, **kw): super(ArghNamespace, self).__init__(*args, **kw) self._functions_stack = [] def __setattr__(self, k, v): if k == DEST_FUNCTION: # don't register the function under DEST_FUNCTION name. # If `ArgumentParser.parse_known_args()` sees that we already have # such attribute, it skips it. However, it goes from the topmost # parser to subparsers. We need the function mapped to the # subparser. So we fool the `ArgumentParser` and pretend that we # didn't get a DEST_FUNCTION attribute; however, in fact we collect # all its values in a stack. The last item in the stack would be # the function mapped to the innermost parser — the one we need. self._functions_stack.append(v) else: super(ArghNamespace, self).__setattr__(k, v) def get_function(self): return self._functions_stack[-1] def dispatch(parser, argv=None, add_help_command=True, completion=True, pre_call=None, output_file=sys.stdout, errors_file=sys.stderr, raw_output=False, namespace=None, skip_unknown_args=False): """ Parses given list of arguments using given parser, calls the relevant function and prints the result. The target function should expect one positional argument: the :class:`argparse.Namespace` object. However, if the function is decorated with :func:`~argh.decorators.plain_signature`, the positional and named arguments from the namespace object are passed to the function instead of the object itself. :param parser: the ArgumentParser instance. :param argv: a list of strings representing the arguments. If `None`, ``sys.argv`` is used instead. Default is `None`. :param add_help_command: if `True`, converts first positional argument "help" to a keyword argument so that ``help foo`` becomes ``foo --help`` and displays usage information for "foo". Default is `True`. :param output_file: A file-like object for output. If `None`, the resulting lines are collected and returned as a string. Default is ``sys.stdout``. :param errors_file: Same as `output_file` but for ``sys.stderr``. :param raw_output: If `True`, results are written to the output file raw, without adding whitespaces or newlines between yielded strings. Default is `False`. :param completion: If `True`, shell tab completion is enabled. Default is `True`. (You will also need to install it.) See :mod:`argh.completion`. :param skip_unknown_args: If `True`, unknown arguments do not cause an error (`ArgumentParser.parse_known_args` is used). :param namespace: An `argparse.Namespace`-like object. By default an :class:`ArghNamespace` object is used. Please note that support for combined default and nested functions may be broken if a different type of object is forced. By default the exceptions are not wrapped and will propagate. The only exception that is always wrapped is :class:`~argh.exceptions.CommandError` which is interpreted as an expected event so the traceback is hidden. You can also mark arbitrary exceptions as "wrappable" by using the :func:`~argh.decorators.wrap_errors` decorator. """ if completion: autocomplete(parser) if argv is None: argv = sys.argv[1:] if add_help_command: if argv and argv[0] == 'help': argv.pop(0) argv.append('--help') if skip_unknown_args: parse_args = parser.parse_known_args else: parse_args = parser.parse_args if not namespace: namespace = ArghNamespace() # this will raise SystemExit if parsing fails namespace_obj = parse_args(argv, namespace=namespace) function = _get_function_from_namespace_obj(namespace_obj) if function: lines = _execute_command(function, namespace_obj, errors_file, pre_call=pre_call) else: # no commands declared, can't dispatch; display help message lines = [parser.format_usage()] if output_file is None: # user wants a string; we create an internal temporary file-like object # and will return its contents as a string if sys.version_info < (3,0): f = compat.BytesIO() else: f = compat.StringIO() else: # normally this is stdout; can be any file f = output_file for line in lines: # print the line as soon as it is generated to ensure that it is # displayed to the user before anything else happens, e.g. # raw_input() is called io.dump(line, f) if not raw_output: # in most cases user wants one message per line io.dump('\n', f) if output_file is None: # user wanted a string; return contents of our temporary file-like obj f.seek(0) return f.read() def _get_function_from_namespace_obj(namespace_obj): if isinstance(namespace_obj, ArghNamespace): # our special namespace object keeps the stack of assigned functions try: function = namespace_obj.get_function() except (AttributeError, IndexError): return None else: # a custom (probably vanilla) namespace object keeps the last assigned # function; this may be wrong but at least something may work if not hasattr(namespace_obj, DEST_FUNCTION): return None function = getattr(namespace_obj, DEST_FUNCTION) if not function or not hasattr(function, '__call__'): return None return function def _execute_command(function, namespace_obj, errors_file, pre_call=None): """ Assumes that `function` is a callable. Tries different approaches to call it (with `namespace_obj` or with ordinary signature). Yields the results line by line. If :class:`~argh.exceptions.CommandError` is raised, its message is appended to the results (i.e. yielded by the generator as a string). All other exceptions propagate unless marked as wrappable by :func:`wrap_errors`. """ if pre_call: # XXX undocumented because I'm unsure if it's OK # Actually used in real projects: # * https://google.com/search?q=argh+dispatch+pre_call # * https://github.com/neithere/argh/issues/63 pre_call(namespace_obj) # the function is nested to catch certain exceptions (see below) 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 wrappable_exceptions = [CommandError] wrappable_exceptions += getattr(function, ATTR_WRAPPED_EXCEPTIONS, []) try: result = _call() for line in result: yield line except tuple(wrappable_exceptions) as e: processor = getattr(function, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, lambda e: '{0.__class__.__name__}: {0}'.format(e)) errors_file.write(compat.text_type(processor(e))) errors_file.write('\n') def dispatch_command(function, *args, **kwargs): """ A wrapper for :func:`dispatch` that creates a one-command parser. Uses :attr:`PARSER_FORMATTER`. This:: dispatch_command(foo) ...is a shortcut for:: parser = ArgumentParser() set_default_command(parser, foo) dispatch(parser) This function can be also used as a decorator. """ parser = argparse.ArgumentParser(formatter_class=PARSER_FORMATTER) set_default_command(parser, function) dispatch(parser, *args, **kwargs) def dispatch_commands(functions, *args, **kwargs): """ A wrapper for :func:`dispatch` that creates a parser, adds commands to the parser and dispatches them. Uses :attr:`PARSER_FORMATTER`. This:: dispatch_commands([foo, bar]) ...is a shortcut for:: parser = ArgumentParser() add_commands(parser, [foo, bar]) dispatch(parser) """ parser = argparse.ArgumentParser(formatter_class=PARSER_FORMATTER) add_commands(parser, functions) dispatch(parser, *args, **kwargs) class EntryPoint(object): """ An object to which functions can be attached and then dispatched. When called with an argument, the argument (a function) is registered at this entry point as a command. When called without an argument, dispatching is triggered with all previously registered commands. Usage:: from argh import EntryPoint app = EntryPoint('main', dict(description='This is a cool app')) @app def ls(): for i in range(10): print i @app def greet(): print 'hello' if __name__ == '__main__': app() """ def __init__(self, name=None, parser_kwargs=None): self.name = name or 'unnamed' self.commands = [] self.parser_kwargs = parser_kwargs or {} def __call__(self, f=None): if f: self._register_command(f) return f return self._dispatch() def _register_command(self, f): self.commands.append(f) def _dispatch(self): if not self.commands: raise DispatchingError('no commands for entry point "{0}"' .format(self.name)) parser = argparse.ArgumentParser(**self.parser_kwargs) add_commands(parser, self.commands) dispatch(parser)