Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make @safe(exceptions=(TypeError, ValueError)) variant #605

Closed
sobolevn opened this issue Sep 14, 2020 · 17 comments · Fixed by #1199
Closed

Make @safe(exceptions=(TypeError, ValueError)) variant #605

sobolevn opened this issue Sep 14, 2020 · 17 comments · Fixed by #1199
Assignees
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@sobolevn
Copy link
Member

This decorator will help to catch only expected errors, not all.

@sobolevn sobolevn added enhancement New feature or request help wanted Extra attention is needed labels Sep 14, 2020
@sobolevn
Copy link
Member Author

This also applies to impure_safe and future_safe

@thepabloaguilar thepabloaguilar self-assigned this Apr 23, 2021
@thepabloaguilar
Copy link
Member

Hey @sobolevn, I'm working on this issue but I got a problem with typesafety tests :(

Here's the implementation:

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


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


def safe(  # noqa: WPS234
    arg: Union[
        Tuple[Type[Exception], ...],
        Callable[..., _ValueType],
    ],
) -> Union[
    Callable[
        [Callable[..., _ValueType]],
        Callable[..., ResultE[_ValueType]],
    ],
    Callable[..., ResultE[_ValueType]],
]:
    ...

pytest output:

__________________________________________________________________________________________________________ safe_decorator_no_params __________________________________________________________________________________________________________
returns/typesafety/test_result/test_safe.yml:10: 
E   pytest_mypy_plugins.utils.TypecheckAssertionError: Invalid output: 
E   Expected:
E     main:7: note: Revealed type is 'def () -> returns.result.Result[builtins.int, builtins.Exception]' (diff)
E   Actual:
E     main:7: note: Revealed type is 'def (*Any, **Any) -> returns.result.Result[builtins.int, builtins.Exception]' (diff)

It should work by the fact we have the overload signature correctly!

Do you have any idea of why it's not working?

@sobolevn
Copy link
Member Author

I think that the reason is https://github.com/dry-python/returns/blob/master/returns/contrib/mypy/_features/decorators.py#L26-L27

Try to catch FunctionalLike and Overloaded types there.

@thepabloaguilar
Copy link
Member

Just a question, should it work if we don't use our plugin??

@sobolevn
Copy link
Member Author

It would work partially (except argument types). Because we don't have ParamSpec support yet.

@thepabloaguilar
Copy link
Member

@sobolevn You were right, the problem is in that statement!

I'll try to fix it!

@thepabloaguilar
Copy link
Member

thepabloaguilar commented Apr 24, 2021

It's not treating tuples correctly 😢

@safe(args=(ValueError, EOFError))
def test() -> int:
    return 1
E   Expected:
E     main:7: note: Revealed type is 'def () -> returns.result.Result[builtins.int, builtins.Exception]' (diff)
E   Actual:
E     main:3: error: No overload variant of "safe" matches argument type "Tuple[Type[ValueError], Type[EOFError]]" (diff)
E     main:3: note:     def [_ValueType] safe(arg: Callable[..., _ValueType]) -> Callable[..., Result[_ValueType, Exception]] (diff)
E     main:3: note:     def safe(arg: Tuple[Type[Exception], ...]) -> Callable[[Callable[..., _ValueType]], Callable[..., Result[_ValueType, Exception]]] (diff)
E     main:3: note: Possible overload variants:     (diff)
E     main:7: note: Revealed type is 'Any'          (diff)

@thepabloaguilar
Copy link
Member

And that error isn't our fault (I guess), 'cause it is not executing our decorator plugin function!

@thepabloaguilar
Copy link
Member

Oh, my fault! I've used wrong parameter name args instead of arg

@thepabloaguilar
Copy link
Member

Ok, now I think I have the same feelings you have about mypy!! 😠

The problem is when decorator has parameters, like we trying to do here, see the example below:

def decorator_argument(arg: int) -> Callable[[Callable[..., int]], Callable[..., int]]:
    def decorator(function: Callable[..., int]) -> Callable[..., int]:
        @wraps(function)
        def decorated(*args, **kwargs):
            pass
        return decorated
    return decorator

Test case:

@decorator_argument(42)
def test() -> int:
    return 1

reveal_type(test)  # N: Revealed type is 'def () -> builtins.int'

Output:

E   pytest_mypy_plugins.utils.TypecheckAssertionError: Invalid output: 
E   Expected:
E     main:8: note: Revealed type is 'def () -> builtins.int' (diff)
E   Actual:
E     main:8: note: Revealed type is 'def (*Any, **Any) -> builtins.int' (diff)

@sobolevn
Copy link
Member Author

sobolevn commented Apr 25, 2021

The problem is when decorator has parameters

Yeah, that's a known problem. We had solved it already in the previous versions.

Take a look at our definitions: https://github.com/dry-python/returns/blob/0.14.0/returns/_generated/pointfree/map.pyi#L22
And mypy plugin for it: https://github.com/dry-python/returns/blob/0.14.0/returns/contrib/mypy/returns_plugin.py#L46 and https://github.com/dry-python/returns/blob/0.14.0/returns/contrib/mypy/_features/pointfree.py

I hope, it helps.

@thepabloaguilar
Copy link
Member

I'm thinking, it is worth it now? I'm saying this because I can implement this and maintain the backward compatibility. The guys who use safe without passing the exceptions won't be affected.

In about 4 months we'll have ParamSpec

@sobolevn
Copy link
Member Author

Let's wait for it, I guess!

@thepabloaguilar
Copy link
Member

No prob!

@thepabloaguilar
Copy link
Member

Waiting for #823

@internetimagery
Copy link

Could you use a helper function, to composition, and then employ that as the base for the decorator with a bit or currying? ie

from functools import wraps
from typing import Any, Callable, Tuple, Type, TypeVar, Union, overload

from returns.result import Result, Success, Failure

_ValueType = TypeVar("_ValueType")
_ErrorType = TypeVar("_ErrorType")

_Callee = Callable[..., _ValueType]
_Caller = Callable[..., Result[_ValueType, _ErrorType]]

# Placeholder Errors
E1 = TypeVar("E1", bound=BaseException)
E2 = TypeVar("E2", bound=BaseException)
E3 = TypeVar("E3", bound=BaseException)
E4 = TypeVar("E4", bound=BaseException)
E5 = TypeVar("E5", bound=BaseException)


@overload
def catch(
    error: Type[E1],
    function: _Callee[_ValueType],
) -> _Caller[_ValueType, E1]:
    ...


@overload
def catch(
    error: Tuple[Type[E1]],
    function: _Callee[_ValueType],
) -> _Caller[_ValueType, E1]:
    ...


@overload
def catch(
    error: Tuple[Type[E1], Type[E2]],
    function: _Callee[_ValueType],
) -> _Caller[_ValueType, Union[E1, E2]]:
    ...


@overload
def catch(
    error: Tuple[Type[E1], Type[E2], Type[E3]],
    function: _Callee[_ValueType],
) -> _Caller[_ValueType, Union[E1, E2, E3]]:
    ...


@overload
def catch(
    error: Tuple[Type[E1], Type[E2], Type[E3], Type[E4]],
    function: _Callee[_ValueType],
) -> _Caller[_ValueType, Union[E1, E2, E3, E4]]:
    ...


@overload
def catch(
    error: Tuple[Type[E1], Type[E2], Type[E3], Type[E4], Type[E5]],
    function: _Callee[_ValueType],
) -> _Caller[_ValueType, Union[E1, E2, E3, E4, E5]]:
    ...


@overload
def catch(
    error: Tuple[Type[E1], ...],
    function: _Callee[_ValueType],
) -> _Caller[_ValueType, E1]:
    ...


def catch(error: Any, function: Callable) -> Callable:
    @wraps(function)
    def decorator(*args, **kwargs) -> Result:
        try:
            return Success(function(*args, **kwargs))
        except error as exc:
            return Failure(exc)
    return decorator

@internetimagery
Copy link

Or use an intermediate object to carry the typed parameters. Unsure if this helps, or I'm just missing the point. Sorry if the latter. :)

from typing import (
    Callable,
    Generic,
    NoReturn as Unused,
    Tuple,
    Type,
    TypeVar,
    Union,
    overload,
)

Y = TypeVar("Y")
N = TypeVar("N")


class Result(Generic[Y, N]):
    def __init__(self, value):
        ...


R = TypeVar("R")

E1 = TypeVar("E1", bound=BaseException)
E2 = TypeVar("E2", bound=BaseException)
E3 = TypeVar("E3", bound=BaseException)
E4 = TypeVar("E4", bound=BaseException)


class Decorator(Generic[E1, E2, E3, E4]):
    def __init__(self, error: Tuple[Type[E1], Type[E2], Type[E3], Type[E4]]):
        self._error = error

    def __call__(
        self, func: Callable[..., R]
    ) -> Callable[..., Result[R, Union[E1, E2, E3, E4]]]:
        def wrapper(*args, **kwargs):
            try:
                return Result(self._func(*args, **kwargs))
            except self._error as err:
                return Result(err)

        return wrapper


@overload
def safe(
    error: Type[E1],
) -> Decorator[E1, Unused, Unused, Unused]:
    ...


@overload
def safe(
    error: Tuple[Type[E1]],
) -> Decorator[E1, Unused, Unused, Unused]:
    ...


@overload
def safe(error: Tuple[Type[E1], Type[E2]]) -> Decorator[E1, E2, Unused, Unused]:
    ...


@overload
def safe(error: Tuple[Type[E1], Type[E2], Type[E3]]) -> Decorator[E1, E2, E3, Unused]:
    ...


@overload
def safe(
    error: Tuple[Type[E1], Type[E2], Type[E3], Type[E4]]
) -> Decorator[E1, E2, E3, E4]:
    ...


@overload
def safe(func: Callable[..., R]) -> Callable[..., Result[R, Exception]]:
    ...


def safe(error):
    if isinstance(error, BaseException):
        return Decorator((error,))
    elif isinstance(error, tuple):
        return Decorator(error)
    return Decorator((Exception,))(error)


@safe
def test1(num: int) -> str:
    return str(num)


@safe(ValueError)
def test2(num: int) -> str:
    return str(num)


@safe((ValueError, TypeError))
def test3(num: int) -> str:
    return str(num)


reveal_type(test1(123)) # note: Revealed type is 'Result[builtins.str*, builtins.Exception]'
reveal_type(test2(123)) # note: Revealed type is 'Result[builtins.str*, builtins.ValueError]'
reveal_type(test3(123)) # note: Revealed type is 'Result[builtins.str*, Union[builtins.ValueError, builtins.TypeError]]'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Development

Successfully merging a pull request may close this issue.

3 participants