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