Skip to content

Added some methods and functions to option and result #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 89 additions & 3 deletions expression/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@

if TYPE_CHECKING:
from expression.collections.seq import Seq
from expression.core.result import Result

_TSource = TypeVar("_TSource")
_TResult = TypeVar("_TResult")
_TError = TypeVar("_TError")

_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
Expand Down Expand Up @@ -150,6 +152,26 @@ def of_optional(cls, value: _TSource | None) -> Option[_TSource]:
"""Convert optional value to an option."""
return of_optional(value)

@classmethod
def of_result(cls, result: Result[_TSource, _TError]) -> Option[_TSource]:
"""Convert result to an option."""
return of_result(result)

@abstractmethod
def to_optional(self) -> _TSource | None:
"""Convert option to an optional."""
raise NotImplementedError

@abstractmethod
def to_result(self, error: _TError) -> Result[_TSource, _TError]:
"""Convert option to a result."""
raise NotImplementedError

@abstractmethod
def to_result_with(self, error: Callable[[], _TError]) -> Result[_TSource, _TError]:
"""Convert option to a result."""
raise NotImplementedError

@abstractmethod
def dict(self) -> _TSource | builtins.dict[str, Any]:
"""Returns a json string representation of the option."""
Expand Down Expand Up @@ -271,6 +293,22 @@ def to_seq(self) -> Seq[_TSource]:

return Seq.of(self._value)

def to_optional(self) -> _TSource | None:
"""Convert option to an optional."""
return self._value

def to_result(self, error: _TError) -> Result[_TSource, _TError]:
"""Convert option to a result."""
from expression.core.result import Ok

return Ok(self._value)

def to_result_with(self, error: Callable[[], _TError]) -> Result[_TSource, _TError]:
"""Convert option to a result."""
from expression.core.result import Ok

return Ok(self._value)

def dict(self) -> _TSource:
attr = getattr(self._value, "dict", None) or getattr(self._value, "dict", None)
if attr and callable(attr):
Expand Down Expand Up @@ -385,6 +423,22 @@ def to_seq(self) -> Seq[_TSource]:

return Seq()

def to_optional(self) -> _TSource | None:
"""Convert option to an optional."""
return None

def to_result(self, error: _TError) -> Result[_TSource, _TError]:
"""Convert option to a result."""
from expression.core.result import Error

return Error(error)

def to_result_with(self, error: Callable[[], _TError]) -> Result[_TSource, _TError]:
"""Convert option to a result."""
from expression.core.result import Error

return Error(error())

def dict(self) -> builtins.dict[str, Any]:
return {} # Pydantic cannot handle None or other types than Optional

Expand Down Expand Up @@ -518,12 +572,21 @@ def to_seq(option: Option[_TSource]) -> Seq[_TSource]:
return option.to_seq()


def to_optional(value: Option[_TSource]) -> _TSource | None:
"""Convert an option value to an optional.

Args:
value: The input option value.

Return:
The result optional.
"""
return value.to_optional()


def of_optional(value: _TSource | None) -> Option[_TSource]:
"""Convert an optional value to an option.

Convert a value that could be `None` into an `Option` value. Same as
`of_obj` but with typed values.

Args:
value: The input optional value.

Expand All @@ -550,6 +613,26 @@ def of_obj(value: Any) -> Option[Any]:
return of_optional(value)


def of_result(result: Result[_TSource, _TError]) -> Option[_TSource]:
from expression.core.result import Ok

match result:
case Ok(value):
return Some(value)
case _:
return Nothing


def to_result(value: Option[_TSource], error: _TError) -> Result[_TSource, _TError]:
return value.to_result(error)


def to_result_with(
value: Option[_TSource], error: Callable[[], _TError]
) -> Result[_TSource, _TError]:
return value.to_result_with(error)


def dict(value: Option[_TSource]) -> _TSource | builtins.dict[Any, Any] | None:
return value.dict()

Expand Down Expand Up @@ -583,6 +666,9 @@ def default_arg(value: Option[_TSource], default_value: _TSource) -> _TSource:
"to_list",
"dict",
"to_seq",
"to_optional",
"of_optional",
"to_result",
"of_result",
"of_obj",
]
83 changes: 80 additions & 3 deletions expression/core/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from abc import ABC, abstractmethod
from collections.abc import Callable, Generator, Iterable, Iterator
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Generic,
Expand All @@ -24,6 +25,10 @@
get_origin,
)


if TYPE_CHECKING:
from expression.core.option import Option

from .curry import curry_flip
from .error import EffectError
from .pipe import PipeMixin
Expand Down Expand Up @@ -78,7 +83,7 @@ class BaseResult(
def default_value(self, value: _TSource) -> _TSource:
"""Get with default value.

Gets the value of the option if the option is Some, otherwise
Gets the value of the result if the result is Ok, otherwise
returns the specified default value.
"""
raise NotImplementedError
Expand All @@ -87,7 +92,7 @@ def default_value(self, value: _TSource) -> _TSource:
def default_with(self, getter: Callable[[_TError], _TSource]) -> _TSource:
"""Get with default value lazily.

Gets the value of the option if the option is Some, otherwise
Gets the value of the result if the result is Ok, otherwise
returns the value produced by the getter
"""
raise NotImplementedError
Expand Down Expand Up @@ -136,6 +141,30 @@ def dict(self) -> builtins.dict[str, _TSource | _TError]:
"""Return a json serializable representation of the result."""
raise NotImplementedError

@abstractmethod
def swap(self) -> Result[_TError, _TResult]:
"""Swaps the value in the result so an Ok becomes an Error and an Error becomes an Ok."""
raise NotImplementedError

@abstractmethod
def to_option(self) -> Option[_TSource]:
"""Convert result to an option."""
raise NotImplementedError

@classmethod
def of_option(
cls, value: Option[_TSource], error: _TError
) -> Result[_TSource, _TError]:
"""Convert option to a result."""
return of_option(value, error)

@classmethod
def of_option_with(
cls, value: Option[_TSource], error: Callable[[], _TError]
) -> Result[_TSource, _TError]:
"""Convert option to a result."""
return of_option_with(value, error)

def __eq__(self, o: Any) -> bool:
raise NotImplementedError

Expand Down Expand Up @@ -228,6 +257,16 @@ def dict(self) -> builtins.dict[str, _TSource | _TError]:

return {"ok": value}

def swap(self) -> Result[_TError, _TSource]:
"""Swaps the value in the result so an Ok becomes an Error and an Error becomes an Ok."""
return Error(self._value)

def to_option(self) -> Option[_TSource]:
"""Convert result to an option."""
from expression.core.option import Some

return Some(self._value)

def __match__(self, pattern: Any) -> Iterable[_TSource]:
if self is pattern or self == pattern:
return [self.value]
Expand Down Expand Up @@ -342,6 +381,16 @@ def dict(self) -> builtins.dict[str, Any]:

return {"error": error}

def swap(self) -> Result[_TError, _TSource]:
"""Swaps the value in the result so an Ok becomes an Error and an Error becomes an Ok."""
return Ok(self._error)

def to_option(self) -> Option[_TSource]:
"""Convert result to an option."""
from expression.core.option import Nothing

return Nothing

def __eq__(self, o: Any) -> bool:
if isinstance(o, Error):
return self.error == o.error # type: ignore
Expand All @@ -366,7 +415,7 @@ def __hash__(self) -> int:
def default_value(value: _TSource) -> Callable[[Result[_TSource, Any]], _TSource]:
"""Get the value or default value.

Gets the value of the option if the option is Some, otherwise
Gets the value of the result if the result is Ok, otherwise
returns the specified default value.
"""

Expand Down Expand Up @@ -429,6 +478,31 @@ def is_error(result: Result[_TSource, _TError]) -> TypeGuard[Error[_TSource, _TE
return result.is_error()


def swap(result: Result[_TSource, _TError]) -> Result[_TError, _TSource]:
"""Swaps the value in the result so an Ok becomes an Error and an Error becomes an Ok."""
return result.swap()


def to_option(result: Result[_TSource, _TError]) -> Option[_TSource]:
from expression.core.option import Nothing, Some

match result:
case Ok(value):
return Some(value)
case _:
return Nothing


def of_option(value: Option[_TSource], error: _TError) -> Result[_TSource, _TError]:
return value.to_result(error)


def of_option_with(
value: Option[_TSource], error: Callable[[], _TError]
) -> Result[_TSource, _TError]:
return value.to_result_with(error)


Result: TypeAlias = Ok[_TSource, _TError] | Error[_TSource, _TError]

__all__ = [
Expand All @@ -442,4 +516,7 @@ def is_error(result: Result[_TSource, _TError]) -> TypeGuard[Error[_TSource, _TE
"dict",
"is_ok",
"is_error",
"to_option",
"of_option",
"of_option_with",
]
57 changes: 56 additions & 1 deletion tests/test_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
from pydantic import BaseModel
from tests.utils import CustomException

from expression import Nothing, Option, Some, effect, option, pipe, pipe2
from expression import (
Error,
Nothing,
Ok,
Option,
Result,
Some,
effect,
option,
pipe,
pipe2,
)
from expression.core.option import BaseOption, Nothing_
from expression.extra.option import pipeline

Expand Down Expand Up @@ -321,6 +332,50 @@ def test_option_of_object_value():
assert xs.is_some()


def test_option_of_result_ok():
result: Result[int, Any] = Ok(42)
xs = option.of_result(result)
assert xs.is_some()


def test_option_of_result_error():
xs: Option[int] = option.of_result(Error("oops"))
assert xs.is_none()


def test_option_to_result_ok():
xs = option.to_result(Some(42), "oops")
assert xs == Ok(42)


def test_option_to_result_error():
xs = option.to_result(Nothing, "oops")
assert xs == Error("oops")


def test_option_to_result_with_ok():
def raise_error() -> Any:
raise Exception("Should not be called")

xs = option.to_result_with(Some(42), error=raise_error)
assert xs == Ok(42)


def test_option_to_result_with_error():
xs = option.to_result_with(Nothing, error=lambda: "oops")
assert xs == Error("oops")


def test_option_to_optional_some():
xs = option.to_optional(Some(1))
assert xs == 1


def test_option_to_optional_nothing():
xs = option.to_optional(Nothing)
assert xs is None


def test_option_builder_zero():
@effect.option[int]()
def fn():
Expand Down
Loading