From 6676fada4040e9f3996dbec21f9f111f69c818d1 Mon Sep 17 00:00:00 2001 From: roman matveev Date: Sat, 13 Jul 2024 09:32:52 +0400 Subject: [PATCH 1/8] wip --- returns/io.py | 70 ++++++++++++++++++++++++++++++++++++++++------- returns/result.py | 2 +- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/returns/io.py b/returns/io.py index 57891a7c1..563a370a1 100644 --- a/returns/io.py +++ b/returns/io.py @@ -9,9 +9,12 @@ Iterator, List, Optional, + Tuple, + Type, TypeVar, Union, final, + overload, ) from typing_extensions import ParamSpec @@ -885,9 +888,32 @@ def lash(self, function): # impure_safe decorator: +@overload def impure_safe( function: Callable[_FuncParams, _NewValueType], ) -> Callable[_FuncParams, IOResultE[_NewValueType]]: + """Decorator to convert exception-throwing for any kind of Exception except ``BaseException`` subclasses.""" + + +@overload +def impure_safe( + exceptions: Tuple[Type[Exception], ...], +) -> Callable[ + [Callable[_FuncParams, _NewValueType]], + Callable[_FuncParams, IOResultE[_NewValueType]], +]: + """Decorator to convert exception-throwing just for a set of Exceptions.""" + +def impure_safe( # type: ignore # noqa: WPS234, C901 + function: Optional[Callable[_FuncParams, _NewValueType]] = None, + exceptions: Optional[Tuple[Type[Exception], ...]] = None, +) -> Union[ + Callable[_FuncParams, IOResultE[_NewValueType]], + Callable[ + [Callable[_FuncParams, _NewValueType]], + Callable[_FuncParams, IOResultE[_NewValueType]], + ], +]: """ Decorator to mark function that it returns :class:`~IOResult` container. @@ -910,16 +936,40 @@ def impure_safe( >>> assert function(1) == IOSuccess(1.0) >>> assert function(0).failure() + You can also use it with explicit exception types as the first argument: + + .. code:: python + + >>> from returns.result import Failure, 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), Failure) + + In this case, only exceptions that are explicitly + listed are going to be caught. + Similar to :func:`returns.future.future_safe` and :func:`returns.result.safe` decorators. """ - @wraps(function) - def decorator( - *args: _FuncParams.args, - **kwargs: _FuncParams.kwargs, - ) -> IOResultE[_NewValueType]: - try: - return IOSuccess(function(*args, **kwargs)) - except Exception as exc: - return IOFailure(exc) - return decorator + def factory( + inner_function: Callable[_FuncParams, _NewValueType], + inner_exceptions: Tuple[Type[Exception], ...], + ) -> Callable[_FuncParams, IOResultE[_NewValueType]]: + @wraps(inner_function) + def decorator(*args: _FuncParams.args, **kwargs: _FuncParams.kwargs): + try: + return IOSuccess(inner_function(*args, **kwargs)) + except inner_exceptions as exc: + return IOFailure(exc) + return decorator + + if callable(function): + return factory(function, exceptions or (Exception,)) + if isinstance(function, tuple): + exceptions = function + function = None + return lambda function: factory(function, exceptions) # type: ignore diff --git a/returns/result.py b/returns/result.py index b8e3afbea..a911228dc 100644 --- a/returns/result.py +++ b/returns/result.py @@ -479,7 +479,7 @@ def failure(self) -> NoReturn: def safe( function: Callable[_FuncParams, _ValueType], ) -> Callable[_FuncParams, ResultE[_ValueType]]: - """Decorator to convert exception-throwing for any kind of Exception.""" + """Decorator to convert exception-throwing for any kind of Exception except ``BaseException`` subclasses.""" @overload From 866ed98f979c66ac8d958124cda2eac3fd33d458 Mon Sep 17 00:00:00 2001 From: roman matveev Date: Sat, 13 Jul 2024 10:10:32 +0400 Subject: [PATCH 2/8] WIP: fixes, fighting with linter --- returns/io.py | 13 ++++--- returns/result.py | 2 +- .../test_impure_safe.py | 37 +++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/returns/io.py b/returns/io.py index 563a370a1..103ead2a0 100644 --- a/returns/io.py +++ b/returns/io.py @@ -892,7 +892,7 @@ def lash(self, function): def impure_safe( function: Callable[_FuncParams, _NewValueType], ) -> Callable[_FuncParams, IOResultE[_NewValueType]]: - """Decorator to convert exception-throwing for any kind of Exception except ``BaseException`` subclasses.""" + """Decorator to convert exception-throwing for any kind of Exception.""" @overload @@ -904,6 +904,7 @@ def impure_safe( ]: """Decorator to convert exception-throwing just for a set of Exceptions.""" + def impure_safe( # type: ignore # noqa: WPS234, C901 function: Optional[Callable[_FuncParams, _NewValueType]] = None, exceptions: Optional[Tuple[Type[Exception], ...]] = None, @@ -940,14 +941,14 @@ def impure_safe( # type: ignore # noqa: WPS234, C901 .. code:: python - >>> from returns.result import Failure, Success, safe + >>> from returns.io import IOSuccess, IOFailure, impure_safe - >>> @safe(exceptions=(ZeroDivisionError,)) + >>> @impure_safe(exceptions=(ZeroDivisionError,)) ... def might_raise(arg: int) -> float: ... return 1 / arg - >>> assert might_raise(1) == Success(1.0) - >>> assert isinstance(might_raise(0), Failure) + >>> assert might_raise(1) == IOSuccess(1.0) + >>> assert isinstance(might_raise(0), IOFailure) In this case, only exceptions that are explicitly listed are going to be caught. @@ -966,7 +967,7 @@ def decorator(*args: _FuncParams.args, **kwargs: _FuncParams.kwargs): except inner_exceptions as exc: return IOFailure(exc) return decorator - + if callable(function): return factory(function, exceptions or (Exception,)) if isinstance(function, tuple): diff --git a/returns/result.py b/returns/result.py index a911228dc..b8e3afbea 100644 --- a/returns/result.py +++ b/returns/result.py @@ -479,7 +479,7 @@ def failure(self) -> NoReturn: def safe( function: Callable[_FuncParams, _ValueType], ) -> Callable[_FuncParams, ResultE[_ValueType]]: - """Decorator to convert exception-throwing for any kind of Exception except ``BaseException`` subclasses.""" + """Decorator to convert exception-throwing for any kind of Exception.""" @overload diff --git a/tests/test_io/test_ioresult_container/test_ioresult_functions/test_impure_safe.py b/tests/test_io/test_ioresult_container/test_ioresult_functions/test_impure_safe.py index 3b9162013..736f8bb05 100644 --- a/tests/test_io/test_ioresult_container/test_ioresult_functions/test_impure_safe.py +++ b/tests/test_io/test_ioresult_container/test_ioresult_functions/test_impure_safe.py @@ -1,3 +1,7 @@ +from typing import Union + +import pytest + from returns.io import IOSuccess, impure_safe @@ -6,6 +10,18 @@ def _function(number: int) -> float: return number / number +@impure_safe(exceptions=(ZeroDivisionError,)) +def _function_two(number: Union[int, str]) -> float: + assert isinstance(number, int) + return number / number + + +@impure_safe((ZeroDivisionError,)) # no name +def _function_three(number: Union[int, str]) -> float: + assert isinstance(number, int) + return number / number + + def test_safe_iosuccess(): """Ensures that safe decorator works correctly for IOSuccess case.""" assert _function(1) == IOSuccess(1.0) @@ -17,3 +33,24 @@ def test_safe_iofailure(): assert isinstance( failed.failure()._inner_value, ZeroDivisionError, # noqa: WPS437 ) + + +def test_safe_failure_with_expected_error(): + """Ensures that safe decorator works correctly for Failure case.""" + failed = _function_two(0) + assert isinstance( + failed.failure()._inner_value, # noqa: WPS437 + ZeroDivisionError, + ) + + failed2 = _function_three(0) + assert isinstance( + failed2.failure()._inner_value, # noqa: WPS437 + 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') From c32384eba3a529328a5f12953f5aee9ae4a3cfb1 Mon Sep 17 00:00:00 2001 From: roman matveev Date: Sat, 13 Jul 2024 13:08:13 +0400 Subject: [PATCH 3/8] add CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eaea6187..dd6a5ac7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ incremental in minor, bugfixes only are patches. See [0Ver](https://0ver.org/). +## 0.24.0 + +### Features + +- Add picky exceptions to `impure_safe` decorator like `safe` has. Issue #1543 + ## 0.23.0 ### Features From 15dca6b54f09e929d59446dfb31a2a44ce2c528b Mon Sep 17 00:00:00 2001 From: roman matveev Date: Sat, 13 Jul 2024 13:09:59 +0400 Subject: [PATCH 4/8] format for any case --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6a5ac7d..504aefb29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ See [0Ver](https://0ver.org/). - Add picky exceptions to `impure_safe` decorator like `safe` has. Issue #1543 + ## 0.23.0 ### Features From e9effd9195acdfb58c77111f34d8214d15449cc6 Mon Sep 17 00:00:00 2001 From: RomanMIzulin Date: Sat, 13 Jul 2024 17:05:12 +0400 Subject: [PATCH 5/8] Update CHANGELOG.md Co-authored-by: sobolevn --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 504aefb29..228329e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ incremental in minor, bugfixes only are patches. See [0Ver](https://0ver.org/). -## 0.24.0 +## 0.24.0 WIP ### Features From 31c9e57429fab2062024cc9c60d7b01bce05caf8 Mon Sep 17 00:00:00 2001 From: roman matveev Date: Thu, 18 Jul 2024 10:30:14 +0400 Subject: [PATCH 6/8] add tests; typing --- returns/io.py | 2 +- .../test_ioresult_container/test_impure_safe.yml | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/returns/io.py b/returns/io.py index 103ead2a0..d0cafe989 100644 --- a/returns/io.py +++ b/returns/io.py @@ -971,6 +971,6 @@ def decorator(*args: _FuncParams.args, **kwargs: _FuncParams.kwargs): if callable(function): return factory(function, exceptions or (Exception,)) if isinstance(function, tuple): - exceptions = function + exceptions = function # type: ignore function = None return lambda function: factory(function, exceptions) # type: ignore diff --git a/typesafety/test_io/test_ioresult_container/test_impure_safe.yml b/typesafety/test_io/test_ioresult_container/test_impure_safe.yml index 6541a587a..8b680f5ee 100644 --- a/typesafety/test_io/test_ioresult_container/test_impure_safe.yml +++ b/typesafety/test_io/test_ioresult_container/test_impure_safe.yml @@ -8,3 +8,18 @@ return 1 reveal_type(test) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]" +- case: impure_decorator_passing_exceptions_no_params + disable_cache: false + main: | + from returns.io import impure_safe + + @impure_safe((ValueError,)) + def test1(arg: str) -> int: + return 1 + + reveal_type(test) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]" + @impure_safe(exceptions=(ValueError,)) + def test2(arg: str) -> int: + return 1 + + reveal_type(test) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]" From e51dca1b2da1ebc637fffb1081221c0dbe256dbc Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 18 Jul 2024 10:31:38 +0300 Subject: [PATCH 7/8] Update typesafety/test_io/test_ioresult_container/test_impure_safe.yml --- typesafety/test_io/test_ioresult_container/test_impure_safe.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typesafety/test_io/test_ioresult_container/test_impure_safe.yml b/typesafety/test_io/test_ioresult_container/test_impure_safe.yml index 8b680f5ee..7c4cf9a4f 100644 --- a/typesafety/test_io/test_ioresult_container/test_impure_safe.yml +++ b/typesafety/test_io/test_ioresult_container/test_impure_safe.yml @@ -8,6 +8,8 @@ return 1 reveal_type(test) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]" + + - case: impure_decorator_passing_exceptions_no_params disable_cache: false main: | From f9d6212bf75553132162434d8b1cb943f1e51772 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 18 Jul 2024 10:37:03 +0300 Subject: [PATCH 8/8] Update test_impure_safe.yml --- .../test_io/test_ioresult_container/test_impure_safe.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typesafety/test_io/test_ioresult_container/test_impure_safe.yml b/typesafety/test_io/test_ioresult_container/test_impure_safe.yml index 7c4cf9a4f..4d9ed1bc6 100644 --- a/typesafety/test_io/test_ioresult_container/test_impure_safe.yml +++ b/typesafety/test_io/test_ioresult_container/test_impure_safe.yml @@ -19,9 +19,10 @@ def test1(arg: str) -> int: return 1 - reveal_type(test) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]" + reveal_type(test1) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]" + @impure_safe(exceptions=(ValueError,)) def test2(arg: str) -> int: return 1 - reveal_type(test) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]" + reveal_type(test2) # N: Revealed type is "def (arg: builtins.str) -> returns.io.IOResult[builtins.int, builtins.Exception]"