Commits

Andy Mikhailenko  committed 6bc2d83

Added decorator wrap_errors(). Improved error wrapping in dispatcher. Added license headers. Bumped version.

  • Participants
  • Parent commits c8e56c8

Comments (0)

Files changed (7)

-version = '0.13.0'
+version = '0.14.0'

File argh/__init__.py

 # -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010 Andrey Mikhailenko 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 for copying conditions.
+#
 """
 API reference
 =============

File argh/completion.py

 # -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010 Andrey Mikhailenko 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 for copying conditions.
+#
 """
 Shell completion
 ================

File argh/constants.py

 # -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010 Andrey Mikhailenko 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 for copying conditions.
+#
 
 #
 # Names of function attributes where Argh stores command behaviour
 
 # forcing plain signature (instead of an argparse.Namespace object)
 ATTR_NO_NAMESPACE = 'argh_no_namespace'
+
+# list of exception classes that should be wrapped and printed as results
+ATTR_WRAPPED_EXCEPTIONS = 'argh_wrap_errors'

File argh/helpers.py

+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010 Andrey Mikhailenko 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 for copying conditions.
+#
 """
 Helpers
 =======
 """
 import argparse
+from functools import wraps
 import locale
 from StringIO import StringIO
 import sys
 from argh.exceptions import CommandError
 from argh.utils import get_subparsers
 from argh.completion import autocomplete
-from argh.constants import ATTR_ALIAS, ATTR_ARGS, ATTR_NO_NAMESPACE
+from argh.constants import (
+    ATTR_ALIAS, ATTR_ARGS, ATTR_NO_NAMESPACE, ATTR_WRAPPED_EXCEPTIONS
+)
 
 
-__all__ = ['ArghParser', 'add_commands', 'autocomplete', 'dispatch', 'confirm']
+__all__ = [
+    'ArghParser', 'add_commands', 'autocomplete', 'dispatch', 'confirm',
+    'wrap_errors'
+]
 def add_commands(parser, functions, namespace=None, title=None,
                  description=None, help=None):
     """Adds given functions as commands to given parser.
 
     Exceptions are not wrapped and will propagate. The only exception among the
     exceptions is :class:`CommandError` which is interpreted as an expected
-    event so the traceback is hidden.
+    event so the traceback is hidden. See also :func:`wrap_errors`.
     """
     # TODO: can be safely removed at version ~= 0.14
     if intercept: # PRAGMA: NOCOVER
         # displayed to the user before anything else happens, e.g.
         # raw_input() is called
         output = _encode(line, f, encoding)
+        output = '' if output is None else output
         f.write(output)
         if not raw_output:
             # in most cases user wants on message per line
     """
     assert hasattr(args, 'function') and hasattr(args.function, '__call__')
 
-    # Call the function
-    if getattr(args.function, ATTR_NO_NAMESPACE, False):
-        # filter the namespace variables so that only those expected by the
-        # actual function will pass
-        f = args.function
-        expected_args = f.func_code.co_varnames[:f.func_code.co_argcount]
-        ok_args = [x for x in args._get_args() if x in expected_args]
-        ok_kwargs = dict((k,v) for k,v in args._get_kwargs()
-                         if k in expected_args)
-        result = args.function(*ok_args, **ok_kwargs)
-    else:
-        result = args.function(args)
+    # the function is nested to catch certain exceptions (see below)
+    def _call():
+        # Actually call the function
+        if getattr(args.function, ATTR_NO_NAMESPACE, False):
+            # filter the namespace variables so that only those expected by the
+            # actual function will pass
+            f = args.function
+            expected_args = f.func_code.co_varnames[:f.func_code.co_argcount]
+            ok_args = [x for x in args._get_args() if x in expected_args]
+            ok_kwargs = dict((k,v) for k,v in args._get_kwargs()
+                             if k in expected_args)
+            result = args.function(*ok_args, **ok_kwargs)
+        else:
+            result = args.function(args)
 
-    # Yield the results
-    if isinstance(result, (GeneratorType, list, tuple)):
-        # yield each line ASAP, convert CommandError message to a line
-        try:
+        # 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
-        except CommandError, e:
-            yield str(e)
-    else:
-        # yield non-empty non-iterable result as a single line
-        if result is not None:
-            yield result
+        else:
+            # yield non-empty non-iterable result as a single line
+            if result is not None:
+                yield result
+
+    wrappable_exceptions = [CommandError]
+    wrappable_exceptions += getattr(args.function, ATTR_WRAPPED_EXCEPTIONS, [])
+
+    try:
+        result = _call()
+        for line in result:
+            yield line
+    except tuple(wrappable_exceptions), e:
+        yield str(e)
 
 
 class ArghParser(argparse.ArgumentParser):
     if default is not None:
         return default
     return None
+
+def wrap_errors(*exceptions):
+    """Decorator. Wraps given exceptions into :class:`CommandError`. Usage::
+
+        @arg('-x')
+        @arg('-y')
+        @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.
+    """
+    def wrapper(func):
+        func.argh_wrap_errors = exceptions
+        return func
+    return wrapper

File argh/utils.py

 # -*- coding: utf-8 -*-
-
+#
+#  Copyright (c) 2010 Andrey Mikhailenko 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 for copying conditions.
+#
 import argparse
 
 
 import argh.helpers
 from argh import (
     alias, ArghParser, arg, add_commands, CommandError, dispatch,
-    plain_signature
+    plain_signature, wrap_errors
 )
 from argh import completion
 
 def custom_namespace(args):
     return args.custom_value
 
-def whiner(args):
+def whiner_plain(args):
+    raise CommandError('I feel depressed.')
+
+def whiner_iterable(args):
     yield 'Hello...'
     raise CommandError('I feel depressed.')
 
+@arg('text')
+def strict_hello(args):
+    assert args.text == 'world', 'Do it yourself'  # bad manners :-(
+    yield 'Hello %s' % args.text
+
+@arg('text')
+@wrap_errors(AssertionError)
+def strict_hello_smart(args):
+    assert args.text == 'world', 'Do it yourself'  # bad manners :-(
+    yield 'Hello %s' % args.text
+
 
 class BaseArghTestCase(unittest.TestCase):
     commands = {}
 
 class ArghTestCase(BaseArghTestCase):
     commands = {
-        None: [echo, plain_echo, foo_bar, do_aliased, whiner,
-               custom_namespace],
+        None: [echo, plain_echo, foo_bar, do_aliased,
+               whiner_plain, whiner_iterable, custom_namespace],
         'greet': [hello, howdy]
     }
 
         self.assert_cmd_returns('greet hello', 'Hello world!\n', output_file=None)
 
     def test_command_error(self):
-        self.assert_cmd_returns('whiner', 'Hello...\nI feel depressed.\n')
+        self.assert_cmd_returns('whiner-plain', 'I feel depressed.\n')
+        self.assert_cmd_returns('whiner-iterable', 'Hello...\nI feel depressed.\n')
 
     def test_custom_namespace(self):
         namespace = argparse.Namespace()
                                 namespace=namespace)
 
 
+class ErrorWrappingTestCase(BaseArghTestCase):
+    commands = {None: [strict_hello, strict_hello_smart]}
+    def test_error_raised(self):
+        f = lambda: self.parser.dispatch(['strict-hello', 'John'])
+        self.assertRaisesRegexp(AssertionError, 'Do it yourself', f)
+
+    def test_error_wrapped(self):
+        self.parser.dispatch(['strict-hello-smart', 'John'])
+        self.assert_cmd_returns('strict-hello-smart John', 'Do it yourself\n')
+        self.assert_cmd_returns('strict-hello-smart world', 'Hello world\n')
+
+
 class NoCommandsTestCase(BaseArghTestCase):
     "Edge case: no commands defined"
     commands = {}