/argh/decorators.py

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