From 863392c96fb2fe8f5e4593e9481778f88b260cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl> Date: Tue, 25 May 2021 19:10:23 +0200 Subject: [PATCH] added reraise_as --- CHANGELOG.md | 2 + docs/coding/functions.rst | 3 + satella/__init__.py | 2 +- satella/coding/__init__.py | 4 +- satella/coding/decorators/arguments.py | 3 +- satella/coding/expect_exception.py | 2 +- satella/coding/predicates.py | 2 +- satella/coding/recast_exceptions.py | 84 ++++++++++++++++++++++++-- tests/test_coding/test_rethrow.py | 20 +++++- 9 files changed, 111 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a347bad..21fd24ff 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 c68e74ee..828ab5fd 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 f56af103..d0d3958f 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 fa8cfbfa..d30cbf43 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 dfda9fdf..4591b4fa 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 06b2cc65..f22187c7 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 8f828e00..80acbd48 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 437954af..e57d7a38 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 d3ccc75a..85bb07c7 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 -- GitLab