Commits

Andy Mikhailenko  committed 94b2ee4

Fix #36: Specify preprocessor for wrapped errors

Added hack for backwards compatibility + deprecation warning.

  • Participants
  • Parent commits 0ce31d4
  • Tags 0.22.0

Comments (0)

Files changed (7)

File argh/__init__.py

 from .helpers import *
 
 
-__version__ = '0.21.2'
+__version__ = '0.22.0'

File argh/constants.py

 # list of exception classes that should be wrapped and printed as results
 ATTR_WRAPPED_EXCEPTIONS = 'argh_wrap_errors'
 
+# a function to preprocess the exception object when it is wrapped
+ATTR_WRAPPED_EXCEPTIONS_PROCESSOR = 'argh_wrap_errors_processor'
+
 # forcing argparse.Namespace object instead of signature introspection
 ATTR_EXPECTS_NAMESPACE_OBJECT = 'argh_expects_namespace_object'
 

File argh/decorators.py

 """
 from argh.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME,
                             ATTR_WRAPPED_EXCEPTIONS,
+                            ATTR_WRAPPED_EXCEPTIONS_PROCESSOR,
                             ATTR_INFER_ARGS_FROM_SIGNATURE,
                             ATTR_EXPECTS_NAMESPACE_OBJECT)
 
     return func
 
 
-def wrap_errors(*exceptions):
+def _fix_compat_issue36(func, errors, processor, args):
+    #
+    # TODO: remove before 1.0 release (will break backwards compatibility)
+    #
+
+    if errors and not hasattr(errors, '__iter__'):
+        # what was expected to be a list is actually its first item
+        errors = [errors]
+
+        # what was expected to be a function is actually the second item
+        if processor:
+            errors.append(processor)
+            processor = None
+
+        # *args, if any, are the remaining items
+        if args:
+            errors.extend(args)
+
+        import warnings
+        warnings.warn('{func.__name__}: wrappable exceptions must be declared '
+                      'as list, i.e. @wrap_errors([{errors}]) instead of '
+                      '@wrap_errors({errors})'.format(
+                        func=func, errors=', '.join(x.__name__ for x in errors)),
+                      DeprecationWarning)
+
+    return errors, processor
+
+
+def wrap_errors(errors=None, processor=None, *args):
     """
     Decorator. Wraps given exceptions into
     :class:`~argh.exceptions.CommandError`. Usage::
 
         @arg('-x')
         @arg('-y')
-        @wrap_errors(AssertionError)
+        @wrap_errors([AssertionError])
         def foo(args):
             assert args.x or args.y, 'x or y must be specified'
 
     If the assertion fails, its message will be correctly printed and the
     stack hidden. This helps to avoid boilerplate code.
+
+    :param errors:
+        A list of exception classes to catch.
+    :param processor:
+        A callable that expects the exception object and returns a string.
+        For example, this renders all wrapped errors in red colour::
+
+            from termcolor import colored
+
+            def failure(err):
+                return colored(str(err), 'red')
+
+            @wrap_errors(processor=failure)
+            def my_command(...):
+                ...
+
+    .. warning::
+
+       The `exceptions` argument **must** be a list.
+
+       For backward compatibility reasons the old way is still allowed::
+
+           @wrap_errors(KeyError, ValueError)
+
+       However, the hack that allows that will be **removed** in Argh 1.0.
+
+       Please make sure to update your code.
+
     """
+
     def wrapper(func):
-        setattr(func, ATTR_WRAPPED_EXCEPTIONS, exceptions)
+        errors_, processor_ = _fix_compat_issue36(func, errors, processor, args)
+
+        if errors_:
+            setattr(func, ATTR_WRAPPED_EXCEPTIONS, errors_)
+
+        if processor_:
+            setattr(func, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, processor_)
+
         return func
     return wrapper
 

File argh/dispatching.py

 
 from argh import compat, io
 from argh.constants import (ATTR_WRAPPED_EXCEPTIONS,
+                            ATTR_WRAPPED_EXCEPTIONS_PROCESSOR,
                             ATTR_EXPECTS_NAMESPACE_OBJECT)
 from argh.completion import autocomplete
 from argh.assembling import add_commands, set_default_command
         for line in result:
             yield line
     except tuple(wrappable_exceptions) as e:
-        yield compat.text_type(e)
+        processor = getattr(args.function, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR,
+                            lambda x:x)
+
+        yield compat.text_type(processor(e))
 
 
 def dispatch_command(function, *args, **kwargs):

File docs/tutorial.rst

 `Argh` will wrap this exception and choose the right way to display its
 message (depending on how :func:`~argh.dispatching.dispatch` was called).
 
-The decorator :func:`~argh.decorators.wrap_errors` reduces the code even further::
+Decorator :func:`~argh.decorators.wrap_errors` reduces the code even further::
 
-    @wrap_errors(KeyError)        # catch KeyError, show the message, hide traceback
+    @wrap_errors([KeyError])  # catch KeyError, show the message, hide traceback
     def show_item(key):
-        return items[key]    # raise KeyError
+        return items[key]     # raise KeyError
 
 Of course it should be used with care in more complex commands.
+
+The decorator accepts a list as its first argument, so multiple commands can be
+specified.  It also allows plugging in a preprocessor for the catched errors::
+
+    @wrap_errors(processor=lambda excinfo: 'ERR: {0}'.format(excinfo))
+    def func():
+        raise CommandError('some error')
+
+The command above will print `ERR: some error`.

File test/test_decorators.py

 
 
 def test_wrap_errors():
-    @argh.wrap_errors(KeyError, ValueError)
+    @argh.wrap_errors([KeyError, ValueError])
     def func():
         pass
 
     attr = getattr(func, argh.constants.ATTR_WRAPPED_EXCEPTIONS)
-    assert attr == (KeyError, ValueError)
+    assert attr == [KeyError, ValueError]
+
+
+def test_wrap_errors_processor():
+    @argh.wrap_errors(processor='STUB')
+    def func():
+        pass
+
+    attr = getattr(func, argh.constants.ATTR_WRAPPED_EXCEPTIONS_PROCESSOR)
+    assert attr == 'STUB'
+
+
+def test_wrap_errors_compat():
+    "Legacy decorator signature. TODO: remove in 1.0"
+
+    @argh.wrap_errors(KeyError, ValueError, TypeError)
+    def func():
+        pass
+
+    attr = getattr(func, argh.constants.ATTR_WRAPPED_EXCEPTIONS)
+    assert attr == [KeyError, ValueError, TypeError]
 
 
 def test_expects_obj():

File test/test_integration.py

 class TestErrorWrapping:
 
     def _get_parrot(self):
-        @argh.arg('--dead', default=False)
-        def parrot(args):
-            if args.dead:
+        def parrot(dead=False):
+            if dead:
                 raise ValueError('this parrot is no more')
             else:
                 return 'beautiful plumage'
         assert run(p, '') == 'beautiful plumage\n'
         assert run(p, '--dead') == 'this parrot is no more\n'
 
+    def test_processor(self):
+        parrot = self._get_parrot()
+        wrapped_parrot = argh.wrap_errors(ValueError)(parrot)
+
+        def failure(err):
+            return 'ERR: ' + str(err) + '!'
+        processed_parrot = argh.wrap_errors(processor=failure)(wrapped_parrot)
+
+        p = argh.ArghParser()
+        p.set_default_command(processed_parrot)
+
+        assert run(p, '--dead') == 'ERR: this parrot is no more!\n'
+
 
 def test_argv():