Skip to content

Commit

Permalink
Make @safe((TypeError, ValueError)) variant (#1199)
Browse files Browse the repository at this point in the history
* Changes `safe` decorator adding a overload that accepts a tuple of exceptions to handle

* Updates `CHANGELOG.md`

* Fixes naming

* Fixes CI

* Fixes CI

* Fixes CI

Co-authored-by: sobolevn <[email protected]>
  • Loading branch information
thepabloaguilar and sobolevn authored Dec 31, 2021
1 parent b5f7c18 commit 82a40dc
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ See [0Ver](https://0ver.org/).
- Enables Pattern Matching support for `IOResult` container
- Improves `hypothesis` plugin, now we detect
when type cannot be constructed and give a clear error message
- Adds the option to pass what exceptions `@safe` will handle


## 0.16.0
Expand Down
16 changes: 16 additions & 0 deletions docs/pages/result.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ use :func:`future_safe <returns.future.future_safe>` instead.
>>> str(divide(0))
'<Failure: division by zero>'
If you want to `safe` handle only a set of exceptions:

.. code:: python
>>> @safe(exceptions=(ZeroDivisionError,)) # Other exceptions will be raised
... def divide(number: int) -> float:
... if number > 10:
... raise ValueError('Too big')
... return number / number
>>> assert divide(5) == Success(1.0)
>>> assert divide(0).failure()
>>> divide(15)
Traceback (most recent call last):
...
ValueError: Too big
FAQ
---
Expand Down
70 changes: 60 additions & 10 deletions returns/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
List,
NoReturn,
Optional,
Tuple,
Type,
TypeVar,
Union,
overload,
)

from typing_extensions import ParamSpec, final
Expand Down Expand Up @@ -442,9 +444,33 @@ def failure(self) -> NoReturn:

# Decorators:

@overload
def safe(
function: Callable[_FuncParams, _ValueType],
) -> Callable[_FuncParams, ResultE[_ValueType]]:
"""Decorator to convert exception-throwing for any kind of Exception."""


@overload
def safe(
exceptions: Tuple[Type[Exception], ...],
) -> Callable[
[Callable[_FuncParams, _ValueType]],
Callable[_FuncParams, ResultE[_ValueType]],
]:
"""Decorator to convert exception-throwing just for a set of Exceptions."""


def safe( # type: ignore # noqa: WPS234, C901
function: Optional[Callable[_FuncParams, _ValueType]] = None,
exceptions: Optional[Tuple[Type[Exception], ...]] = None,
) -> Union[
Callable[_FuncParams, ResultE[_ValueType]],
Callable[
[Callable[_FuncParams, _ValueType]],
Callable[_FuncParams, ResultE[_ValueType]],
],
]:
"""
Decorator to convert exception-throwing function to ``Result`` container.
Expand All @@ -466,16 +492,40 @@ def safe(
>>> assert might_raise(1) == Success(1.0)
>>> assert isinstance(might_raise(0), Result.failure_type)
You can also use it with explicit exception types as the first argument:
.. code:: python
>>> from returns.result import Result, Success, safe
>>> @safe(exceptions=(ZeroDivisionError,))
... def might_raise(arg: int) -> float:
... return 1 / arg
>>> assert might_raise(1) == Success(1.0)
>>> assert isinstance(might_raise(0), Result.failure_type)
In this case, only exceptions that are explicitly
listed are going to be caught.
Similar to :func:`returns.io.impure_safe`
and :func:`returns.future.future_safe` decorators.
"""
@wraps(function)
def decorator(
*args: _FuncParams.args,
**kwargs: _FuncParams.kwargs,
) -> ResultE[_ValueType]:
try:
return Success(function(*args, **kwargs))
except Exception as exc:
return Failure(exc)
return decorator
def factory(
inner_function: Callable[_FuncParams, _ValueType],
inner_exceptions: Tuple[Type[Exception], ...],
) -> Callable[_FuncParams, ResultE[_ValueType]]:
@wraps(inner_function)
def decorator(*args: _FuncParams.args, **kwargs: _FuncParams.kwargs):
try:
return Success(inner_function(*args, **kwargs))
except inner_exceptions as exc:
return Failure(exc)
return decorator

if callable(function):
return factory(function, (Exception,))
if isinstance(function, tuple):
exceptions = function # type: ignore
function = None
return lambda function: factory(function, exceptions) # type: ignore
30 changes: 30 additions & 0 deletions tests/test_result/test_result_functions/test_safe.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Union

import pytest

from returns.result import Success, safe

Expand All @@ -7,6 +10,18 @@ def _function(number: int) -> float:
return number / number


@safe(exceptions=(ZeroDivisionError,))
def _function_two(number: Union[int, str]) -> float:
assert isinstance(number, int)
return number / number


@safe((ZeroDivisionError,)) # no name
def _function_three(number: Union[int, str]) -> float:
assert isinstance(number, int)
return number / number


def test_safe_success():
"""Ensures that safe decorator works correctly for Success case."""
assert _function(1) == Success(1.0)
Expand All @@ -16,3 +31,18 @@ def test_safe_failure():
"""Ensures that safe decorator works correctly for Failure case."""
failed = _function(0)
assert isinstance(failed.failure(), ZeroDivisionError)


def test_safe_failure_with_expected_error():
"""Ensures that safe decorator works correctly for Failure case."""
failed = _function_two(0)
assert isinstance(failed.failure(), ZeroDivisionError)

failed2 = _function_three(0)
assert isinstance(failed2.failure(), ZeroDivisionError)


def test_safe_failure_with_non_expected_error():
"""Ensures that safe decorator works correctly for Failure case."""
with pytest.raises(AssertionError):
_function_two('0')
118 changes: 118 additions & 0 deletions typesafety/test_result/test_safe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@
reveal_type(test) # N: Revealed type is "def () -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_decorator_passing_exceptions_no_params
disable_cache: false
main: |
from returns.result import safe
@safe((ValueError,))
def test() -> int:
return 1
reveal_type(test) # N: Revealed type is "def () -> returns.result.Result[builtins.int, builtins.Exception]"
@safe(exceptions=(ValueError,))
def test2() -> int:
return 1
reveal_type(test2) # N: Revealed type is "def () -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_composition_no_params
disable_cache: false
main: |
Expand All @@ -21,6 +39,17 @@
reveal_type(safe(test)) # N: Revealed type is "def () -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_composition_passing_exceptions_no_params
disable_cache: false
main: |
from returns.result import safe
def test() -> int:
return 1
reveal_type(safe((EOFError,))(test)) # N: Revealed type is "def () -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_decorator_with_args
disable_cache: false
main: |
Expand All @@ -34,6 +63,19 @@
reveal_type(test) # N: Revealed type is "def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_decorator_passing_exceptions_with_args
disable_cache: false
main: |
from typing import Optional
from returns.result import safe
@safe((ValueError, EOFError))
def test(first: int, second: Optional[str] = None, *, kw: bool = True) -> int:
return 1
reveal_type(test) # N: Revealed type is "def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_composition_with_args
disable_cache: false
main: |
Expand All @@ -46,6 +88,18 @@
reveal_type(safe(test)) # N: Revealed type is "def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_composition_passing_exceptions_with_args
disable_cache: false
main: |
from typing import Optional
from returns.result import safe
def test(first: int, second: Optional[str] = None, *, kw: bool = True) -> int:
return 1
reveal_type(safe((ValueError,))(test)) # N: Revealed type is "def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_regression333
disable_cache: false
main: |
Expand All @@ -59,6 +113,19 @@
reveal_type(send) # N: Revealed type is "def (text: builtins.str) -> returns.result.Result[Any, builtins.Exception]"
- case: safe_passing_exceptions_regression333
disable_cache: false
main: |
from returns.result import safe
from typing import Any
@safe((Exception,))
def send(text: str) -> Any:
return "test"
reveal_type(send) # N: Revealed type is "def (text: builtins.str) -> returns.result.Result[Any, builtins.Exception]"
- case: safe_regression641
disable_cache: false
main: |
Expand All @@ -72,6 +139,19 @@
reveal_type(safe(tap(Response.raise_for_status))) # N: Revealed type is "def (main.Response*) -> returns.result.Result[main.Response, builtins.Exception]"
- case: safe_passing_exceptions_regression641
disable_cache: false
main: |
from returns.result import safe
from returns.functions import tap
class Response(object):
def raise_for_status(self) -> None:
...
reveal_type(safe((EOFError,))(tap(Response.raise_for_status))) # N: Revealed type is "def (main.Response*) -> returns.result.Result[main.Response, builtins.Exception]"
- case: safe_decorator_with_args_kwargs
disable_cache: false
main: |
Expand All @@ -84,6 +164,18 @@
reveal_type(test) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_decorator_passing_exceptions_with_args_kwargs
disable_cache: false
main: |
from returns.result import safe
@safe((EOFError,))
def test(*args, **kwargs) -> int:
return 1
reveal_type(test) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_decorator_with_args_kwargs
disable_cache: false
main: |
Expand All @@ -96,6 +188,18 @@
reveal_type(test) # N: Revealed type is "def (*args: builtins.int, **kwargs: builtins.str) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_decorator_passing_exceptions_with_args_kwargs
disable_cache: false
main: |
from returns.result import safe
@safe((Exception,))
def test(*args: int, **kwargs: str) -> int:
return 1
reveal_type(test) # N: Revealed type is "def (*args: builtins.int, **kwargs: builtins.str) -> returns.result.Result[builtins.int, builtins.Exception]"
- case: safe_decorator_composition
disable_cache: false
main: |
Expand All @@ -108,3 +212,17 @@
return 1
reveal_type(test) # N: Revealed type is "def (*args: builtins.int, **kwargs: builtins.str) -> returns.io.IO[returns.result.Result*[builtins.int*, builtins.Exception]]"
- case: safe_decorator_passing_exceptions_composition
disable_cache: false
main: |
from returns.io import impure
from returns.result import safe
@impure
@safe((ValueError,))
def test(*args: int, **kwargs: str) -> int:
return 1
reveal_type(test) # N: Revealed type is "def (*args: builtins.int, **kwargs: builtins.str) -> returns.io.IO[returns.result.Result*[builtins.int*, builtins.Exception]]"

0 comments on commit 82a40dc

Please sign in to comment.