/argh/decorators.py
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