diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a347badb44a981274aeccc291e9350c6e5c8fea..21fd24ffbdbfff715e4f253abebb571266b4311b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ # v2.16.5 + +* added `reraise_as` diff --git a/docs/coding/functions.rst b/docs/coding/functions.rst index c68e74ee3498cf484783a1f93611420a0b5069d5..828ab5fddd37d18bcbb07520b01fa0be210abcff 100644 --- a/docs/coding/functions.rst +++ b/docs/coding/functions.rst @@ -39,6 +39,9 @@ Functions and decorators .. autoclass:: satella.coding.rethrow_as :members: +.. autoclass:: satella.coding.reraise_as + :members: + .. autofunction:: satella.coding.catch_exception .. autofunction:: satella.coding.raises_exception diff --git a/satella/__init__.py b/satella/__init__.py index f56af103d373630757a35d1e9fab48b7c2aed626..d0d3958f391c12f3be5d6aa4534b3570edd05800 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.16.5a1' +__version__ = '2.16.5a2' diff --git a/satella/coding/__init__.py b/satella/coding/__init__.py index fa8cfbfae2ff088db6c2d58b445700348ecf63de..d30cbf4308887def9778fd7c804695170ee2e77c 100644 --- a/satella/coding/__init__.py +++ b/satella/coding/__init__.py @@ -18,11 +18,11 @@ from .misc import update_if_not_none, update_key_if_none, update_attr_if_none, q enum_value from .overloading import overload, class_or_instancemethod from .recast_exceptions import rethrow_as, silence_excs, catch_exception, log_exceptions, \ - raises_exception + raises_exception, reraise_as from .expect_exception import expect_exception __all__ = [ - 'Closeable', 'contains', 'enum_value', + 'Closeable', 'contains', 'enum_value', 'reraise_as', 'expect_exception', 'overload', 'class_or_instancemethod', 'update_if_not_none', 'DocsFromParent', 'update_key_if_none', 'queue_iterator', diff --git a/satella/coding/decorators/arguments.py b/satella/coding/decorators/arguments.py index dfda9fdf128a29b99cbde470a7bb2dd76825f755..4591b4fa2eaa6e56dbf68e70742716af629bbef2 100644 --- a/satella/coding/decorators/arguments.py +++ b/satella/coding/decorators/arguments.py @@ -80,7 +80,8 @@ def execute_before(callable_: tp.Callable) -> tp.Callable: >>> def do_things(): >>> print('Things are done') - Then the following will print 'Things are done' + Then the following will print :code:`Things are done`: + >>> @do_things >>> def nothing(): >>> ... diff --git a/satella/coding/expect_exception.py b/satella/coding/expect_exception.py index 06b2cc65ca97432a1bacf0c8b1ebb45c4dc534f3..f22187c7b6b6f2cfca86c3da65185d1a07136b2c 100644 --- a/satella/coding/expect_exception.py +++ b/satella/coding/expect_exception.py @@ -4,7 +4,7 @@ import typing as tp class expect_exception: """ - A decorator to use as following: + A context manager to use as following: >>> a = {'test': 2} >>> with expect_exception(KeyError, ValueError, 'KeyError not raised'): diff --git a/satella/coding/predicates.py b/satella/coding/predicates.py index 8f828e006e0b4791d7fd271e78db77e5c4fb35bb..80acbd48c75ac7980c7b136a2a8fd0fea10aac13 100644 --- a/satella/coding/predicates.py +++ b/satella/coding/predicates.py @@ -4,7 +4,7 @@ import typing as tp from satella.coding.typing import Predicate from satella.configuration.schema import Descriptor -__all__ = ['x', 'build_structure'] +__all__ = ['x', 'build_structure', 'PredicateClass'] import warnings diff --git a/satella/coding/recast_exceptions.py b/satella/coding/recast_exceptions.py index 437954af87819cb5c15dc519a8b9c502e2a71c27..e57d7a383e833a8bcd78a590974bada6df843c33 100644 --- a/satella/coding/recast_exceptions.py +++ b/satella/coding/recast_exceptions.py @@ -4,7 +4,8 @@ import threading import typing as tp from .decorators.decorators import wraps -from .typing import ExceptionClassType, T, NoArgCallable +from .typing import ExceptionClassType, T, NoArgCallable, ExceptionList + def silence_excs(*exc_types: ExceptionClassType, returns=None, @@ -129,6 +130,73 @@ class log_exceptions: return inner +# noinspection PyPep8Naming +class reraise_as: + """ + Transform some exceptions into others. + + Either a decorator or a context manager + + New exception will be created by calling exception to transform to with + repr of current one. + + You can also provide just two exceptions, eg. + + >>> reraise_as(NameError, ValueError, 'a value error!') + + You can also provide a catch-all: + + >>> reraise_as((NameError, ValueError), OSError, 'an OS error!') + + New exception will be raised from the one caught! + + .. note:: This checks if exception matches directly via :code:`isinstance`, so defining + your own subclassing hierarchy by :code:`__isinstance__` or :code:`__issubclass__` + will work here. + + This is meant as an improvement of :class:`~satella.coding.rethrow_as` + + :param source_exc: source exception or a tuple of exceptions to catch + :param target_exc: target exception to throw. If given a None, the exception will + be silently swallowed. + :param args: arguments to constructor of target exception + :param kwargs: keyword arguments to constructor of target exception + """ + __slots__ = 'source', 'target_exc', 'args', 'kwargs' + + def __init__(self, source_exc: ExceptionList, + target_exc: tp.Optional[ExceptionClassType], *args, **kwargs): + self.source = source_exc + self.target_exc = target_exc + self.args = args + self.kwargs = kwargs + + def __call__(self, fun: tp.Callable) -> tp.Callable: + @wraps(fun) + def inner(*args, **kwargs): + try: + return fun(*args, **kwargs) + except Exception as e: + if isinstance(e, self.source): + if self.target_exc is not None: + raise self.target_exc(*self.args, **self.kwargs) from e + else: + raise + + return inner + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_val is not None: + if isinstance(exc_val, self.source): + if self.target_exc is None: + return True + raise self.target_exc(*self.args, **self.kwargs) from exc_val + return False + + # noinspection PyPep8Naming class rethrow_as: """ @@ -139,6 +207,10 @@ class rethrow_as: New exception will be created by calling exception to transform to with repr of current one. + .. note:: This checks if exception matches directly via :code:`isinstance`, so defining + your own subclassing hierarchy by :code:`__isinstance__` or :code:`__issubclass__` + will work here. + You can also provide just two exceptions, eg. >>> rethrow_as(NameError, ValueError) @@ -152,6 +224,10 @@ class rethrow_as: Pass tuples of (exception to catch - exception to transform to). + .. warning:: Try to use :class:`~satella.coding.reraise_as` instead. + However, during to richer set of switches and capability to return a value + this is not deprecated. + :param exception_preprocessor: other callable/1 to use instead of repr. Should return a str, a text description of the exception :param returns: what value should the function return if this is used as a decorator @@ -159,10 +235,10 @@ class rethrow_as: is used as as decorator :raises ValueError: you specify both returns and returns_factory """ - __slots__ = ('mapping', 'exception_preprocessor', 'returns', '__exception_remapped', - 'returns_factory') + __slots__ = 'mapping', 'exception_preprocessor', 'returns', '__exception_remapped', \ + 'returns_factory' - def __init__(self, *pairs: tp.Union[ExceptionClassType, tp.Tuple[ExceptionClassType, ...]], + def __init__(self, *pairs: ExceptionList, exception_preprocessor: tp.Optional[tp.Callable[[Exception], str]] = repr, returns=None, returns_factory: tp.Optional[NoArgCallable[tp.Any]] = None): diff --git a/tests/test_coding/test_rethrow.py b/tests/test_coding/test_rethrow.py index d3ccc75ae2d4f7eae2d7e314b943d74246caadd0..85bb07c74880e9554e47e61082628b38f18adf0b 100644 --- a/tests/test_coding/test_rethrow.py +++ b/tests/test_coding/test_rethrow.py @@ -1,7 +1,8 @@ import logging import unittest -from satella.coding import rethrow_as, silence_excs, catch_exception, log_exceptions, raises_exception +from satella.coding import rethrow_as, silence_excs, catch_exception, log_exceptions, \ + raises_exception, reraise_as logger = logging.getLogger(__name__) @@ -135,6 +136,23 @@ class TestStuff(unittest.TestCase): self.assertRaises(NameError, lol) + def test_reraise(self): + try: + with reraise_as(ValueError, NameError): + raise ValueError() + except NameError: + return + + self.fail() + + def test_reraise_silencer(self): + + @reraise_as(ValueError, None) + def lol(): + raise ValueError() + + lol() + def test_issue_10(self): class WTFException1(Exception): pass