PageRenderTime 30ms CodeModel.GetById 1ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

/argh/decorators.py

https://bitbucket.org/neithere/argh/
Python | 260 lines | 204 code | 15 blank | 41 comment | 10 complexity | 601d379ff50f05310430ddbf5ea34df4 MD5 | raw file
  1# -*- coding: utf-8 -*-
  2#
  3#  Copyright (c) 2010—2013 Andrey Mikhaylenko and contributors
  4#
  5#  This file is part of Argh.
  6#
  7#  Argh is free software under terms of the GNU Lesser
  8#  General Public License version 3 (LGPLv3) as published by the Free
  9#  Software Foundation. See the file README for copying conditions.
 10#
 11"""
 12Command decorators
 13~~~~~~~~~~~~~~~~~~
 14"""
 15from argh.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME,
 16                            ATTR_WRAPPED_EXCEPTIONS,
 17                            ATTR_WRAPPED_EXCEPTIONS_PROCESSOR,
 18                            ATTR_INFER_ARGS_FROM_SIGNATURE,
 19                            ATTR_EXPECTS_NAMESPACE_OBJECT)
 20
 21
 22__all__ = ['alias', 'aliases', 'named', 'arg', 'plain_signature', 'command',
 23           'wrap_errors', 'expects_obj']
 24
 25
 26def named(new_name):
 27    """
 28    Sets given string as command name instead of the function name.
 29    The string is used verbatim without further processing.
 30
 31    Usage::
 32
 33        @named('load')
 34        def do_load_some_stuff_and_keep_the_original_function_name(args):
 35            ...
 36
 37    The resulting command will be available only as ``load``.  To add aliases
 38    without renaming the command, check :func:`aliases`.
 39
 40    .. versionadded:: 0.19
 41    """
 42    def wrapper(func):
 43        setattr(func, ATTR_NAME, new_name)
 44        return func
 45    return wrapper
 46
 47
 48def alias(new_name):  # pragma: nocover
 49    """
 50    .. deprecated:: 0.19
 51
 52       Use :func:`named` or :func:`aliases` instead.
 53    """
 54    import warnings
 55    warnings.warn('Decorator @alias() is deprecated. '
 56                  'Use @aliases() or @named() instead.', DeprecationWarning)
 57    return named(new_name)
 58
 59
 60def aliases(*names):
 61    """
 62    Defines alternative command name(s) for given function (along with its
 63    original name). Usage::
 64
 65        @aliases('co', 'check')
 66        def checkout(args):
 67            ...
 68
 69    The resulting command will be available as ``checkout``, ``check`` and ``co``.
 70
 71    .. note::
 72
 73       This decorator only works with a recent version of argparse (see `Python
 74       issue 9324`_ and `Python rev 4c0426`_).  Such version ships with
 75       **Python 3.2+** and may be available in other environments as a separate
 76       package.  Argh does not issue warnings and simply ignores aliases if
 77       they are not supported.  See :attr:`~argh.assembling.SUPPORTS_ALIASES`.
 78
 79       .. _Python issue 9324: http://bugs.python.org/issue9324
 80       .. _Python rev 4c0426: http://hg.python.org/cpython/rev/4c0426261148/
 81
 82    .. versionadded:: 0.19
 83    """
 84    def wrapper(func):
 85        setattr(func, ATTR_ALIASES, names)
 86        return func
 87    return wrapper
 88
 89
 90def plain_signature(func):  # pragma: nocover
 91    """
 92    .. deprecated:: 0.20
 93
 94       Function signature is now introspected by default.
 95       Use :func:`expects_obj` for inverted behaviour.
 96    """
 97    import warnings
 98    warnings.warn('Decorator @plain_signature is deprecated. '
 99                  'Function signature is now introspected by default.',
100                  DeprecationWarning)
101    return func
102
103
104def arg(*args, **kwargs):
105    """
106    Declares an argument for given function. Does not register the function
107    anywhere, nor does it modify the function in any way. The signature is
108    exactly the same as that of :meth:`argparse.ArgumentParser.add_argument`,
109    only some keywords are not required if they can be easily guessed.
110
111    Usage::
112
113        @arg('path')
114        @arg('--format', choices=['yaml','json'], default='json')
115        @arg('--dry-run', default=False)
116        @arg('-v', '--verbosity', choices=range(0,3), default=1)
117        def load(args):
118            loaders = {'json': json.load, 'yaml': yaml.load}
119            loader = loaders[args.format]
120            data = loader(args.path)
121            if not args.dry_run:
122                if 1 < verbosity:
123                    print('saving to the database')
124                put_to_database(data)
125
126    Note that:
127
128    * you didn't have to specify ``action="store_true"`` for ``--dry-run``;
129    * you didn't have to specify ``type=int`` for ``--verbosity``.
130
131    """
132    def wrapper(func):
133        declared_args = getattr(func, ATTR_ARGS, [])
134        # The innermost decorator is called first but appears last in the code.
135        # We need to preserve the expected order of positional arguments, so
136        # the outermost decorator inserts its value before the innermost's:
137        declared_args.insert(0, dict(option_strings=args, **kwargs))
138        setattr(func, ATTR_ARGS, declared_args)
139        return func
140    return wrapper
141
142
143def command(func):
144    """
145    .. deprecated:: 0.21
146
147       Function signature is now introspected by default.
148       Use :func:`expects_obj` for inverted behaviour.
149    """
150    import warnings
151    warnings.warn('Decorator @command is deprecated. '
152                  'Function signature is now introspected by default.',
153                  DeprecationWarning)
154    setattr(func, ATTR_INFER_ARGS_FROM_SIGNATURE, True)
155    return func
156
157
158def _fix_compat_issue36(func, errors, processor, args):
159    #
160    # TODO: remove before 1.0 release (will break backwards compatibility)
161    #
162
163    if errors and not hasattr(errors, '__iter__'):
164        # what was expected to be a list is actually its first item
165        errors = [errors]
166
167        # what was expected to be a function is actually the second item
168        if processor:
169            errors.append(processor)
170            processor = None
171
172        # *args, if any, are the remaining items
173        if args:
174            errors.extend(args)
175
176        import warnings
177        warnings.warn('{func.__name__}: wrappable exceptions must be declared '
178                      'as list, i.e. @wrap_errors([{errors}]) instead of '
179                      '@wrap_errors({errors})'.format(
180                        func=func, errors=', '.join(x.__name__ for x in errors)),
181                      DeprecationWarning)
182
183    return errors, processor
184
185
186def wrap_errors(errors=None, processor=None, *args):
187    """
188    Decorator. Wraps given exceptions into
189    :class:`~argh.exceptions.CommandError`. Usage::
190
191        @wrap_errors([AssertionError])
192        def foo(x=None, y=None):
193            assert x or y, 'x or y must be specified'
194
195    If the assertion fails, its message will be correctly printed and the
196    stack hidden. This helps to avoid boilerplate code.
197
198    :param errors:
199        A list of exception classes to catch.
200    :param processor:
201        A callable that expects the exception object and returns a string.
202        For example, this renders all wrapped errors in red colour::
203
204            from termcolor import colored
205
206            def failure(err):
207                return colored(str(err), 'red')
208
209            @wrap_errors(processor=failure)
210            def my_command(...):
211                ...
212
213    .. warning::
214
215       The `exceptions` argument **must** be a list.
216
217       For backward compatibility reasons the old way is still allowed::
218
219           @wrap_errors(KeyError, ValueError)
220
221       However, the hack that allows that will be **removed** in Argh 1.0.
222
223       Please make sure to update your code.
224
225    """
226
227    def wrapper(func):
228        errors_, processor_ = _fix_compat_issue36(func, errors, processor, args)
229
230        if errors_:
231            setattr(func, ATTR_WRAPPED_EXCEPTIONS, errors_)
232
233        if processor_:
234            setattr(func, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, processor_)
235
236        return func
237    return wrapper
238
239
240def expects_obj(func):
241    """
242    Marks given function as expecting a namespace object.
243
244    Usage::
245
246        @arg('bar')
247        @arg('--quux', default=123)
248        @expects_obj
249        def foo(args):
250            yield args.bar, args.quux
251
252    This is equivalent to::
253
254        def foo(bar, quux=123):
255            yield bar, quux
256
257    In most cases you don't need this decorator.
258    """
259    setattr(func, ATTR_EXPECTS_NAMESPACE_OBJECT, True)
260    return func