Skip to content

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

Closed

Description

@sobolevn

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

Activity

  1. sobolevn commented on Sep 15, 2020

    @sobolevn
    MemberAuthor

    This also applies to impure_safe and future_safe

  2. thepabloaguilar commented on Apr 24, 2021

    @thepabloaguilar
    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?

  3. sobolevn commented on Apr 24, 2021

    @sobolevn
    MemberAuthor

    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.

  4. thepabloaguilar commented on Apr 24, 2021

    @thepabloaguilar
    Member

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

  5. sobolevn commented on Apr 24, 2021

    @sobolevn
    MemberAuthor

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

  6. thepabloaguilar commented on Apr 24, 2021

    @thepabloaguilar
    Member

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

    I'll try to fix it!

  7. thepabloaguilar commented on Apr 24, 2021

    @thepabloaguilar
    Member

    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)
    
  8. thepabloaguilar commented on Apr 24, 2021

    @thepabloaguilar
    Member

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

  9. thepabloaguilar commented on Apr 24, 2021

    @thepabloaguilar
    Member

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

  10. thepabloaguilar commented on Apr 24, 2021

    @thepabloaguilar
    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)
    
  11. sobolevn commented on Apr 25, 2021

    @sobolevn
    MemberAuthor

    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.

  12. thepabloaguilar commented on Apr 25, 2021

    @thepabloaguilar
    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

  13. sobolevn commented on Apr 25, 2021

    @sobolevn
    MemberAuthor

    Let's wait for it, I guess!

  14. thepabloaguilar commented on Apr 25, 2021

    @thepabloaguilar
    Member

    No prob!

  15. thepabloaguilar commented on Apr 25, 2021

    @thepabloaguilar
    Member

    Waiting for #823

  16. internetimagery commented on Aug 12, 2021

    @internetimagery

    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
  17. internetimagery commented on Aug 12, 2021

    @internetimagery

    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

Metadata

Metadata

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions