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