Skip to content

Commit

Permalink
Add decorator to turn regular functions in Result returning ones
Browse files Browse the repository at this point in the history
Add as_result() helper to make a decorator to turn a function into one
that returns a a ``Result``.

For type checking, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/)

This is currently not supported by Mypy; see
python/mypy#8645

Fixes rustedpy#33.
  • Loading branch information
wbolster committed Nov 1, 2021
1 parent ac58fec commit b02d319
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 3 deletions.
3 changes: 2 additions & 1 deletion result/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from .result import Err, Ok, OkErr, Result, UnwrapError
from .result import Err, Ok, OkErr, Result, UnwrapError, as_result

__all__ = [
"Err",
"Ok",
"OkErr",
"Result",
"UnwrapError",
"as_result",
]
__version__ = "0.7.0.dev"
50 changes: 49 additions & 1 deletion result/result.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
from typing import Callable, Generic, TypeVar, Union, Any, cast, overload, NoReturn
import functools
import inspect
from typing import (
Any,
Callable,
Generic,
NoReturn,
ParamSpec,
Type,
TypeVar,
Union,
cast,
overload,
)

T = TypeVar("T", covariant=True) # Success type
E = TypeVar("E", covariant=True) # Error type
U = TypeVar("U")
F = TypeVar("F")
P = ParamSpec("P")
R = TypeVar("R")
TBE = TypeVar("TBE", bound=BaseException)


class Ok(Generic[T]):
Expand Down Expand Up @@ -272,3 +288,35 @@ def result(self) -> Result[Any, Any]:
Returns the original result.
"""
return self._result


def as_result(
*exceptions: Type[TBE],
) -> Callable[[Callable[P, R]], Callable[P, Result[R, TBE]]]:
"""
Make a decorator to turn a function into one that returns a ``Result``.
Regular return values are turned into ``Ok(return_value)``. Raised
exceptions of the specified exception type(s) are turned into ``Err(exc)``.
"""
if not exceptions or not all(
inspect.isclass(exception) and issubclass(exception, BaseException)
for exception in exceptions
):
raise TypeError("as_result() requires one or more exception types")

def decorator(f: Callable[P, R]) -> Callable[P, Result[R, TBE]]:
"""
Decorator to turn a function into one that returns a ``Result``.
"""

@functools.wraps(f)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[R, TBE]:
try:
return Ok(f(*args, **kwargs))
except exceptions as exc:
return Err(exc)

return wrapper

return decorator
53 changes: 52 additions & 1 deletion tests/test_result.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from result import Err, Ok, OkErr, Result, UnwrapError
from result import Err, Ok, OkErr, Result, UnwrapError, as_result


def test_ok_factories() -> None:
Expand Down Expand Up @@ -179,3 +179,54 @@ def test_slots() -> None:
o.some_arbitrary_attribute = 1 # type: ignore[attr-defined]
with pytest.raises(AttributeError):
n.some_arbitrary_attribute = 1 # type: ignore[attr-defined]


def test_as_result() -> None:
"""
``as_result()`` turns function into Result-returning ones.
"""

@as_result(ValueError)
def good(value: int) -> int:
return value

@as_result(IndexError, ValueError)
def bad(value: int) -> int:
raise ValueError

good_result = good(123)
bad_result = bad(123)

assert isinstance(good_result, Ok)
assert good_result.unwrap() == 123
assert isinstance(bad_result, Err)
assert isinstance(bad_result.unwrap_err(), ValueError)


def test_as_result_other_exception() -> None:
"""
``as_result()`` only catches the specified exceptions.
"""
@as_result(ValueError)
def f() -> int:
raise IndexError

with pytest.raises(IndexError):
f()


def test_as_result_invalid_usage() -> None:
"""
Invalid use of ``as_result()`` raises reasonable errors.
"""
message = "requires one or more exception types"

with pytest.raises(TypeError, match=message):
@as_result() # No exception types specified
def f() -> int:
return 1

with pytest.raises(TypeError, match=message):
@as_result("not an exception type") # type: ignore[arg-type]
def g() -> int:
return 1

0 comments on commit b02d319

Please sign in to comment.