diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a75e49b7..2356db8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/pages/result.rst b/docs/pages/result.rst index a82b3189d..2639a7521 100644 --- a/docs/pages/result.rst +++ b/docs/pages/result.rst @@ -99,6 +99,22 @@ use :func:`future_safe ` instead. >>> str(divide(0)) '' +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 --- diff --git a/returns/result.py b/returns/result.py index ceff2f382..81901b7ac 100644 --- a/returns/result.py +++ b/returns/result.py @@ -9,9 +9,11 @@ List, NoReturn, Optional, + Tuple, Type, TypeVar, Union, + overload, ) from typing_extensions import ParamSpec, final @@ -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. @@ -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 diff --git a/tests/test_result/test_result_functions/test_safe.py b/tests/test_result/test_result_functions/test_safe.py index 0c1c435e8..d574ba2c1 100644 --- a/tests/test_result/test_result_functions/test_safe.py +++ b/tests/test_result/test_result_functions/test_safe.py @@ -1,3 +1,6 @@ +from typing import Union + +import pytest from returns.result import Success, safe @@ -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) @@ -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') diff --git a/typesafety/test_result/test_safe.yml b/typesafety/test_result/test_safe.yml index e9da0b25c..ed94c45b8 100644 --- a/typesafety/test_result/test_safe.yml +++ b/typesafety/test_result/test_safe.yml @@ -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: | @@ -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: | @@ -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: | @@ -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: | @@ -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: | @@ -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: | @@ -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: | @@ -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: | @@ -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]]"