diff --git a/CHANGELOG.md b/CHANGELOG.md index 7188f05e3276aa62cbff1381140fa57637bcd669..1ef88df5d2f43e51491ad77c8543fe9bcd8d2b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ # v2.15.5 + +* added `expect_exception` diff --git a/docs/coding/functions.rst b/docs/coding/functions.rst index d0fd22cf5d7cb4d3fec431643d05d3bd86a9eeed..c68e74ee3498cf484783a1f93611420a0b5069d5 100644 --- a/docs/coding/functions.rst +++ b/docs/coding/functions.rst @@ -2,6 +2,8 @@ Functions and decorators ======================== +.. autoclass:: satella.coding.expect_exception + .. autofunction:: satella.coding.enum_value .. autofunction:: satella.coding.contains diff --git a/satella/__init__.py b/satella/__init__.py index 08ca75688f1c460f5961b8c447f200b3b31df6a3..c9353a9f1740a4a5a7200c7014798fef2a934671 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.15.5a1' +__version__ = '2.15.5' diff --git a/satella/coding/__init__.py b/satella/coding/__init__.py index 089da2f4ad56f05b6bc6aef39160d23a94c4e494..fa8cfbfae2ff088db6c2d58b445700348ecf63de 100644 --- a/satella/coding/__init__.py +++ b/satella/coding/__init__.py @@ -19,9 +19,11 @@ from .misc import update_if_not_none, update_key_if_none, update_attr_if_none, q from .overloading import overload, class_or_instancemethod from .recast_exceptions import rethrow_as, silence_excs, catch_exception, log_exceptions, \ raises_exception +from .expect_exception import expect_exception __all__ = [ 'Closeable', 'contains', 'enum_value', + 'expect_exception', 'overload', 'class_or_instancemethod', 'update_if_not_none', 'DocsFromParent', 'update_key_if_none', 'queue_iterator', 'update_attr_if_none', 'update_key_if_not_none', 'source_to_function', diff --git a/satella/coding/expect_exception.py b/satella/coding/expect_exception.py new file mode 100644 index 0000000000000000000000000000000000000000..06b2cc65ca97432a1bacf0c8b1ebb45c4dc534f3 --- /dev/null +++ b/satella/coding/expect_exception.py @@ -0,0 +1,40 @@ +from satella.coding.typing import ExceptionList +import typing as tp + + +class expect_exception: + """ + A decorator to use as following: + + >>> a = {'test': 2} + >>> with expect_exception(KeyError, ValueError, 'KeyError not raised'): + >>> a['test2'] + + If other exception than the expected is raised, it is passed through + + :param exc_to_except: a list of exceptions or a single exception to expect + :param else_raise: raise a particular exception if no exception is raised. + This should be a callable that accepts provided args and kwargs and returns + an exception instance. + :param args: args to provide to constructor + :param kwargs: kwargs to provide to constructor + """ + __slots__ = ('exc_to_except', 'else_raise', 'else_raise_args', 'else_raise_kwargs') + + def __init__(self, exc_to_except: ExceptionList, else_raise: tp.Type[Exception], + *args, **kwargs): + self.exc_to_except = exc_to_except + self.else_raise = else_raise + self.else_raise_args = args + self.else_raise_kwargs = kwargs + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + raise self.else_raise(*self.else_raise_args, + **self.else_raise_kwargs) + elif not isinstance(exc_val, self.exc_to_except): + return False + return True diff --git a/tests/test_coding/test_debug.py b/tests/test_coding/test_debug.py index b08c641a3ac761d98205c38467a5823286acfbc6..23d405091cf802040dffaa54f1dc885197c4fa98 100644 --- a/tests/test_coding/test_debug.py +++ b/tests/test_coding/test_debug.py @@ -1,13 +1,30 @@ import unittest from satella.coding import precondition, short_none, has_keys, update_if_not_none, postcondition, \ - get_arguments + get_arguments, expect_exception from satella.coding.decorators import for_argument from satella.exceptions import PreconditionError class TestTypecheck(unittest.TestCase): + def test_except_exception(self): + def expect_exception_1(): + with expect_exception(KeyError, ValueError, 'Hello world!'): + pass + + self.assertRaises(ValueError, expect_exception_1) + + a = {} + with expect_exception(KeyError, ValueError, 'Hello world!'): + a['test'] + + def expect_exception_2(): + with expect_exception(KeyError, ValueError, 'Hello world!'): + raise TypeError() + + self.assertRaises(TypeError, expect_exception_2) + def test_for_argument_bug(self): class Device: @for_argument(None, str)