dbrattli / Expression

Pragmatic functional programming for Python inspired by F#
https://expression.readthedocs.io
MIT License
424 stars 31 forks source link

Implement the catch utility decorator #16

Closed mlw214 closed 3 years ago

mlw214 commented 3 years ago

Implements the utility decorator catch per the discussion in #5.

mlw214 commented 3 years ago

@dbrattli please let me know if there's anything you want me to add, remove, or correct!

codecov[bot] commented 3 years ago

Codecov Report

Merging #16 (1b4ccf7) into main (35d14a6) will increase coverage by 0.09%. The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main      #16      +/-   ##
==========================================
+ Coverage   80.23%   80.32%   +0.09%     
==========================================
  Files          37       38       +1     
  Lines        2130     2140      +10     
==========================================
+ Hits         1709     1719      +10     
  Misses        421      421              
Impacted Files Coverage Δ
expression/extra/result/__init__.py 100.00% <100.00%> (ø)
expression/extra/result/catch.py 100.00% <100.00%> (ø)
erlendvollset commented 3 years ago

Could also support multiple exception types without chaining decorators by packing the exception parameter like this: (note use of / separator in overload signatures for enforcing positional-only parameters)

TSource = TypeVar("TSource")
TError1 = TypeVar("TError1", bound=Exception)
TError2 = TypeVar("TError2", bound=Exception)
TError3 = TypeVar("TError3", bound=Exception)
TError4 = TypeVar("TError4", bound=Exception)

@overload
def catch(__exc1: Type[TError1], /) -> Callable[[Callable[..., TSource]], Callable[..., Result[TSource, TError1]]]:
    ...

@overload
def catch(
    __exc1: Type[TError1], __exc2: Type[TError2], /
) -> Callable[[Callable[..., TSource]], Callable[..., Result[TSource, Union[TError1, TError2]]]]:
    ...

@overload
def catch(
    __exc1: Type[TError1], __exc2: Type[TError2], __exc3: Type[TError3], /
) -> Callable[[Callable[..., TSource]], Callable[..., Result[TSource, Union[TError1, TError2, TError3]]]]:
    ...

@overload
def catch(
    __exc1: Type[TError1], __exc2: Type[TError2], __exc3: Type[TError3], __exc4: Type[TError4], /
) -> Callable[[Callable[..., TSource]], Callable[..., Result[TSource, Union[TError1, TError2, TError3, TError4]]]]:
    ...

def catch(*exception: Type[Exception]) -> Callable[[Callable[..., TSource]], Callable[..., Result[TSource, Exception]]]:
    def decorator(fn: Callable[..., TSource]) -> Callable[..., Result[TSource, Exception]]:
        @wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Result[TSource, Exception]:
            try:
                out = fn(*args, **kwargs)
            except exception as exn:
                return Error(cast(Exception, exn))
            else:
                if isinstance(out, Result):
                    return cast(Result[TSource, Exception], out)

                return Ok(out)

        return wrapper

    return decorator

Usage example:

@catch(ValueError, TypeError)
def foo() -> int:
    raise ValueError()
dbrattli commented 3 years ago

This is great! I also like the suggestion by @erlendvollset but we could do that as a separate PR to avoid blocking this PR.