diff --git a/rusty_results/prelude.py b/rusty_results/prelude.py index c30a143..c7d703d 100644 --- a/rusty_results/prelude.py +++ b/rusty_results/prelude.py @@ -1,6 +1,6 @@ from abc import abstractmethod from dataclasses import dataclass -from typing import TypeVar, Union, Callable, Generic, Iterator, Tuple, Dict, Any +from typing import cast, TypeVar, Union, Callable, Generic, Iterator, Tuple, Dict, Any from rusty_results.exceptions import UnwrapException try: from pydantic.fields import ModelField @@ -240,6 +240,16 @@ def flatten(self) -> "Option[T]": """ ... # pragma: no cover + @abstractmethod + def transpose(self) -> "Result[Option[T], E]": + """ + Transposes an Option of a Result into a Result of an Option. + Empty will be mapped to Ok(Empty). Some(Ok(_)) and Some(Err(_)) will be mapped to Ok(Some(_)) and Err(_). + :return: `Result[Option[T], E]` + :raises TypeError if inner value is not a `Result` + """ + ... # pragma: no cover + @abstractmethod def __bool__(self) -> bool: ... # pragma: no cover @@ -378,7 +388,7 @@ def zip(self, other: "Option[U]") -> "Option[Tuple[T, U]]": if other.is_some: # function typing is correct, we really return an Option[Tuple] but mypy complains that # other may not have a Value attribute because it do not understand the previous line check. - return Some((self.Some, other.Some)) # type: ignore + return Some((self.Some, other.Some)) # type: ignore[union-attr] return Empty() @@ -394,7 +404,7 @@ def unwrap_empty(self): def flatten_one(self) -> "Option[T]": inner: T = self.unwrap() if isinstance(inner, OptionProtocol): - return inner # type: ignore[return-value] + return cast(Option, inner) return self def flatten(self) -> "Option[T]": @@ -404,6 +414,12 @@ def flatten(self) -> "Option[T]": this, inner = (inner, inner.flatten_one()) return this + def transpose(self) -> "Result[Option[T], E]": + if not isinstance(self.Some, ResultProtocol): + raise TypeError("Inner value is not a Result") + value: "ResultProtocol[T, E]" = self.Some + return value.map(Some) + def __bool__(self) -> bool: return True @@ -485,6 +501,9 @@ def flatten_one(self) -> "Option[T]": def flatten(self) -> "Option[T]": return self + def transpose(self) -> "Result[Option[T], E]": + return Ok(self) + def __bool__(self) -> bool: return False @@ -681,6 +700,32 @@ def expect_err(self, msg: str) -> E: """ ... # pragma: no cover + @abstractmethod + 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 + + @abstractmethod + 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 + + @abstractmethod + def transpose(self) -> Option["Result[T, E]"]: + """ + Transposes a Result of an Option into an Option of a Result. + Ok(Empty) will be mapped to Empty. Ok(Some(_)) and Err(_) will be mapped to Some(Ok(_)) and Some(Err(_)) + :return: Option[Result[T, E]] as per the mapping above + :raises TypeError if inner value is not an `Option` + """ + ... # pragma: no cover + @abstractmethod def __bool__(self) -> bool: ... # pragma: no cover @@ -836,6 +881,23 @@ def unwrap_err(self) -> E: def expect_err(self, msg: str) -> E: raise UnwrapException(msg) + def flatten_one(self) -> "Result[T, E]": + if isinstance(self.Ok, ResultProtocol): + return cast(Result, self.unwrap()) + return self + + def flatten(self) -> "Result[T, E]": + this: Result[T, E] = self + inner: Result[T, E] = self.flatten_one() + while inner is not this: + this, inner = (inner, inner.flatten_one()) + return this + + def transpose(self) -> Option["Result[T, E]"]: + if not isinstance(self.Ok, OptionProtocol): + raise TypeError("Inner value is not of type Option") + return cast(Option, self.unwrap()).map(Ok) + def __repr__(self): return f"Ok({self.Ok})" @@ -912,6 +974,15 @@ def unwrap_err(self) -> E: def expect_err(self, msg: str) -> E: return self.Error + def flatten_one(self) -> "Result[T, E]": + return self + + def flatten(self) -> "Result[T, E]": + return self + + def transpose(self) -> Option["Result[T, E]"]: + return Some(self) + def __repr__(self): return f"Err({self.Error})" diff --git a/rusty_results/tests/option/test_option_empty.py b/rusty_results/tests/option/test_option_empty.py index 5e3b1b6..441e547 100644 --- a/rusty_results/tests/option/test_option_empty.py +++ b/rusty_results/tests/option/test_option_empty.py @@ -92,11 +92,17 @@ def test_empty_unwrap_empty(): Empty().unwrap_empty() -def test_flatten(): +def test_flatten_one(): this: Empty = Empty() assert this.flatten_one() == this -def test_flatten_all(): +def test_flatten(): this: Empty = Empty() assert this.flatten() == this + + +def test_transpose(): + this: Empty = Empty() + assert this.transpose() == Ok(Empty()) + diff --git a/rusty_results/tests/option/test_option_some.py b/rusty_results/tests/option/test_option_some.py index 02d0b75..af6df23 100644 --- a/rusty_results/tests/option/test_option_some.py +++ b/rusty_results/tests/option/test_option_some.py @@ -156,3 +156,19 @@ def test_flatten_one(option: Option, expected_flatten: Option): ) def test_flatten(option: Option, expected_flatten: Option): assert option.flatten() == expected_flatten + + +@pytest.mark.parametrize( + "option, expected_transpose", + [ + (Some(Ok(1)), Ok(Some(1))), + (Some(Err(2)), Err(2)), + ] +) +def test_transpose(option, expected_transpose): + assert option.transpose() == expected_transpose + + +def test_transpose_type_error(): + with pytest.raises(TypeError): + Some(10).transpose() diff --git a/rusty_results/tests/result/test_result_err.py b/rusty_results/tests/result/test_result_err.py index cf8811a..1c6cdd9 100644 --- a/rusty_results/tests/result/test_result_err.py +++ b/rusty_results/tests/result/test_result_err.py @@ -133,3 +133,18 @@ def test_err_unwrap_err(): def test_err_expect_err(): err: Result[int, int] = Err(0) assert err.expect_err("foo") == 0 + + +def test_flatten_one(): + this: Result = Err(None) + assert this.flatten_one() == this + + +def test_flatten(): + this: Result = Err(None) + assert this.flatten() == this + + +def test_transpose(): + this: Result = Err(None) + assert this.transpose() == Some(Err(None)) diff --git a/rusty_results/tests/result/test_result_ok.py b/rusty_results/tests/result/test_result_ok.py index 12d176d..2435688 100644 --- a/rusty_results/tests/result/test_result_ok.py +++ b/rusty_results/tests/result/test_result_ok.py @@ -135,3 +135,47 @@ def test_ok_expect_err(): with pytest.raises(UnwrapException) as e: ok.expect_err(exception_msg) assert str(e.value) == exception_msg + + +@pytest.mark.parametrize( + "result, expected_flatten", + [ + (Ok(1), Ok(1)), + (Ok(Ok(2)), Ok(2)), + (Ok(Ok(Ok(3))), Ok(Ok(3))), + (Ok(Err(None)), Err(None)), + (Ok(Ok(Ok(Ok(Ok(Ok(Err(None))))))), Ok(Ok(Ok(Ok(Ok(Err(None))))))), + ] +) +def test_flatten_one(result: Result, expected_flatten: Result): + assert result.flatten_one() == expected_flatten + + +@pytest.mark.parametrize( + "result, expected_flatten", + [ + (Ok(1), Ok(1)), + (Ok(Ok(2)), Ok(2)), + (Ok(Ok(Ok(3))), Ok(3)), + (Ok(Err(None)), Err(None)), + (Ok(Ok(Ok(Ok(Ok(Ok(Err(None))))))), Err(None)), + ] +) +def test_flatten(result: Result, expected_flatten: Result): + assert result.flatten() == expected_flatten + + +@pytest.mark.parametrize( + "result, expected_transpose", + [ + (Ok(Some(1)), Some(Ok(1))), + (Ok(Empty()), Empty()), + ] +) +def test_transpose(result, expected_transpose): + assert result.transpose() == expected_transpose + + +def test_transpose_type_error(): + with pytest.raises(TypeError): + Ok(10).transpose()