diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cabb81..55d63ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: - '3.8' - '3.9' - '3.10' + - '3.11' + - '3.12' steps: - uses: actions/checkout@v2 - name: Install Python 3 diff --git a/examples/early_return.py b/examples/early_return.py new file mode 100644 index 0000000..886257e --- /dev/null +++ b/examples/early_return.py @@ -0,0 +1,20 @@ +from rusty_results import Option, Some, Empty +from rusty_results import early_return + + +@early_return +def fail_on_operation() -> Option[int]: + value1 = Some(10) + value2 = Empty() + return Some(~value1 + ~value2) + + +def success_on_operation() -> Option[int]: + value1 = Some(10) + value2 = Some(10) + return Some(~value1 + ~value2) + + +if __name__ == "__main__": + print("Success so it return value: ", success_on_operation()) + print("Fail so it return Empty: ", fail_on_operation()) diff --git a/rusty_results/__init__.py b/rusty_results/__init__.py index 03eb29a..559e57a 100644 --- a/rusty_results/__init__.py +++ b/rusty_results/__init__.py @@ -1,2 +1,2 @@ from .prelude import Option, Some, Empty, Result, Ok, Err -from .exceptions import UnwrapException +from .exceptions import UnwrapException, early_return diff --git a/rusty_results/exceptions.py b/rusty_results/exceptions.py index d2dd3e2..aa6d73f 100644 --- a/rusty_results/exceptions.py +++ b/rusty_results/exceptions.py @@ -1,2 +1,25 @@ +from functools import wraps +from typing import TypeVar + + class UnwrapException(Exception): ... + + +T = TypeVar("T") + + +class EarlyReturnException(ValueError): + def __init__(self, value: T): + self.value = value + super().__init__(self.value) + + +def early_return(f): + @wraps(f) + def wrapper(*args, **kwargs): + try: + f(*args, **kwargs) + except EarlyReturnException as e: + return e.value + return wrapper diff --git a/rusty_results/prelude.py b/rusty_results/prelude.py index c7d703d..a555ecf 100644 --- a/rusty_results/prelude.py +++ b/rusty_results/prelude.py @@ -1,7 +1,8 @@ from abc import abstractmethod from dataclasses import dataclass from typing import cast, TypeVar, Union, Callable, Generic, Iterator, Tuple, Dict, Any -from rusty_results.exceptions import UnwrapException +from rusty_results.exceptions import UnwrapException, EarlyReturnException + try: from pydantic.fields import ModelField except ImportError: # pragma: no cover @@ -248,7 +249,16 @@ def transpose(self) -> "Result[Option[T], E]": :return: `Result[Option[T], E]` :raises TypeError if inner value is not a `Result` """ - ... # pragma: no cover + ... # pragma: no cover + + @abstractmethod + def early_return(self) -> T: + """ + Access hook for `early_return` wrapper style. + :return: Self if self is Some(T) otherwise + :raises: EarlyReturnException(Empty) + """ + ... # pragma: no cover @abstractmethod def __bool__(self) -> bool: @@ -260,6 +270,14 @@ def __contains__(self, item: T) -> bool: def __iter__(self): return self.iter() + def __invert__(self) -> T: + """ + Access hook for `early_return` wrapper style. + :return: Self if self is Some(T) otherwise + :raises: EarlyReturnException(Empty) + """ + return self.early_return() + @classmethod def __get_validators__(cls): yield cls.__validate @@ -420,6 +438,10 @@ def transpose(self) -> "Result[Option[T], E]": value: "ResultProtocol[T, E]" = self.Some return value.map(Some) + def early_return(self) -> T: + # it is safe to unwrap here as we know we are Some + return self.unwrap() + def __bool__(self) -> bool: return True @@ -504,6 +526,9 @@ def flatten(self) -> "Option[T]": def transpose(self) -> "Result[Option[T], E]": return Ok(self) + def early_return(self) -> T: + raise EarlyReturnException(self) + def __bool__(self) -> bool: return False @@ -706,7 +731,7 @@ def flatten_one(self) -> "Result[T, E]": Converts from Result[Result[T, E], E] to Result, one nested level. :return: Flattened Result[T, E] """ - ... # pragma: no cover + ... # pragma: no cover @abstractmethod def flatten(self) -> "Result[T, E]": @@ -714,7 +739,7 @@ def flatten(self) -> "Result[T, E]": Converts from Result[Result[T, E], E] to Result, any nested level :return: Flattened Result[T, E] """ - ... # pragma: no cover + ... # pragma: no cover @abstractmethod def transpose(self) -> Option["Result[T, E]"]: @@ -724,7 +749,24 @@ def transpose(self) -> Option["Result[T, E]"]: :return: Option[Result[T, E]] as per the mapping above :raises TypeError if inner value is not an `Option` """ - ... # pragma: no cover + ... # pragma: no cover + + @abstractmethod + def early_return(self) -> T: + """ + Access hook for `early_return` wrapper style. + :return: T if self is Ok(T) otherwise + :raises: EarlyReturnException(Err(e)) + """ + ... # pragma: no cover + + def __invert__(self) -> T: + """ + Access hook for `early_return` wrapper style. + :return: T if self is Ok(T) otherwise + :raises: EarlyReturnException(Err(e)) + """ + return self.early_return() @abstractmethod def __bool__(self) -> bool: @@ -898,6 +940,10 @@ def transpose(self) -> Option["Result[T, E]"]: raise TypeError("Inner value is not of type Option") return cast(Option, self.unwrap()).map(Ok) + def early_return(self) -> T: + # safe to unwrap here as we know it is Ok + return self.unwrap() + def __repr__(self): return f"Ok({self.Ok})" @@ -983,6 +1029,9 @@ def flatten(self) -> "Result[T, E]": def transpose(self) -> Option["Result[T, E]"]: return Some(self) + def early_return(self) -> T: + raise EarlyReturnException(self) + def __repr__(self): return f"Err({self.Error})" diff --git a/rusty_results/tests/exceptions/__init__.py b/rusty_results/tests/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rusty_results/tests/exceptions/test_early_return.py b/rusty_results/tests/exceptions/test_early_return.py new file mode 100644 index 0000000..9e0ba76 --- /dev/null +++ b/rusty_results/tests/exceptions/test_early_return.py @@ -0,0 +1,11 @@ +from rusty_results import early_return, Option, Some, Empty + + +def test_early_return(): + @early_return + def __test_it() -> Option[str]: + foo: Option = Empty() + _ = ~foo + return Some(10) # pragma: no cover + + assert __test_it() == Empty() diff --git a/rusty_results/tests/option/test_option_empty.py b/rusty_results/tests/option/test_option_empty.py index 441e547..410b873 100644 --- a/rusty_results/tests/option/test_option_empty.py +++ b/rusty_results/tests/option/test_option_empty.py @@ -106,3 +106,8 @@ def test_transpose(): this: Empty = Empty() assert this.transpose() == Ok(Empty()) + +def test_early_return(): + with pytest.raises(EarlyReturnException): + this: Empty = Empty() + _ = ~this diff --git a/rusty_results/tests/option/test_option_some.py b/rusty_results/tests/option/test_option_some.py index af6df23..a43f5c8 100644 --- a/rusty_results/tests/option/test_option_some.py +++ b/rusty_results/tests/option/test_option_some.py @@ -172,3 +172,8 @@ def test_transpose(option, expected_transpose): def test_transpose_type_error(): with pytest.raises(TypeError): Some(10).transpose() + + +def test_early_return(): + value = ~Some(10) + assert value == 10 diff --git a/rusty_results/tests/result/test_result_err.py b/rusty_results/tests/result/test_result_err.py index 1c6cdd9..3385805 100644 --- a/rusty_results/tests/result/test_result_err.py +++ b/rusty_results/tests/result/test_result_err.py @@ -148,3 +148,9 @@ def test_flatten(): def test_transpose(): this: Result = Err(None) assert this.transpose() == Some(Err(None)) + + +def test_early_return(): + err: Result[int, int] = Err(0) + with pytest.raises(EarlyReturnException): + _ = ~err diff --git a/rusty_results/tests/result/test_result_ok.py b/rusty_results/tests/result/test_result_ok.py index 2435688..f6cb35c 100644 --- a/rusty_results/tests/result/test_result_ok.py +++ b/rusty_results/tests/result/test_result_ok.py @@ -179,3 +179,8 @@ def test_transpose(result, expected_transpose): def test_transpose_type_error(): with pytest.raises(TypeError): Ok(10).transpose() + + +def test_early_return(): + err: Result[int, int] = Ok(0) + assert ~err == 0