From d28858ecbd80b690b27a37fae3d089d0c68ffb9d Mon Sep 17 00:00:00 2001 From: francium Date: Sat, 26 Aug 2023 22:52:48 -0400 Subject: [PATCH] Implement core API and update rest of the repo --- MANIFEST.in | 2 +- README.rst | 52 ++++- RELEASING.md | 2 +- setup.cfg | 2 +- src/maybe/__init__.py | 4 - src/maybe/maybe.py | 261 +++++----------------- tests/test_maybe.py | 338 ++++++++--------------------- tests/test_pattern_matching.py | 15 +- tests/type-checking/test_maybe.yml | 110 +++++----- 9 files changed, 250 insertions(+), 536 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5de61aa..2660273 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include maybe/py.typed +include src/maybe/py.typed diff --git a/README.rst b/README.rst index 3e3215f..c412be1 100644 --- a/README.rst +++ b/README.rst @@ -2,4 +2,54 @@ Maybe ====== -Experimental. Do not use yet. +====== +Result +====== + +.. image:: https://img.shields.io/github/actions/workflow/status/rustedpy/result/ci.yml?branch=master + :alt: GitHub Workflow Status (branch) + :target: https://github.com/rustedpy/result/actions/workflows/ci.yml?query=branch%3Amaster + +.. image:: https://codecov.io/gh/rustedpy/result/branch/master/graph/badge.svg + :alt: Coverage + :target: https://codecov.io/gh/rustedpy/result + +A simple Maybe (Option) type for Python 3 `inspired by Rust +`__, fully type annotated. + +Installation +============ + +Not yet available on PyPI. PyPI package coming soon. + +Latest GitHub ``master`` branch version: + +.. sourcecode:: sh + + $ pip install git+https://github.com/rustedpy/result + +Summary +======= + +**Experimental. API subject to change.** + +The idea is that a result value can be either ``Some(value)`` or ``Nothing()``, +with a way to differentiate between the two. ``Some`` and ``Nothing`` are both classes +encapsulating a possible value. + +Example usage, + +.. sourcecode:: Python + + from rustedpy-maybe import Nothing, Some + + o = Some('yay') + n = Nothing() + assert o.unwrap_or_else(str.upper) == 'yay' + assert n.unwrap_or_else(lambda: 'default') == 'default' + + +License +======= + +MIT License diff --git a/RELEASING.md b/RELEASING.md index f11f9e2..a05e512 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -21,7 +21,7 @@ vim CHANGELOG.md 4) Do a signed commit and signed tag of the release: ``` -git add maybe/__init__.py CHANGELOG.md +git add src/maybe/__init__.py CHANGELOG.md git commit -S${GPG} -m "Release v${VERSION}" git tag -u ${GPG} -m "Release v${VERSION}" v${VERSION} ``` diff --git a/setup.cfg b/setup.cfg index 63e1c2d..2c010d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -name = maybe +name = rustedpy-maybe version = attr: maybe.__version__ description = A Rust-like option type for Python long_description = file: README.rst diff --git a/src/maybe/__init__.py b/src/maybe/__init__.py index 786c62e..3b60f7a 100644 --- a/src/maybe/__init__.py +++ b/src/maybe/__init__.py @@ -4,8 +4,6 @@ SomeNothing, Maybe, UnwrapError, - as_async_maybe, - as_maybe, is_some, is_nothing, ) @@ -16,8 +14,6 @@ "SomeNothing", "Maybe", "UnwrapError", - "as_async_maybe", - "as_maybe", "is_some", "is_nothing", ] diff --git a/src/maybe/maybe.py b/src/maybe/maybe.py index f23028d..679a657 100644 --- a/src/maybe/maybe.py +++ b/src/maybe/maybe.py @@ -1,12 +1,8 @@ from __future__ import annotations -import functools -import inspect import sys -from warnings import warn from typing import ( Any, - Awaitable, Callable, Final, Generic, @@ -24,7 +20,6 @@ T = TypeVar("T", covariant=True) # Success type -E = TypeVar("E", covariant=True) # Error type U = TypeVar("U") F = TypeVar("F") P = ParamSpec("P") @@ -34,7 +29,7 @@ class Some(Generic[T]): """ - A value that indicates success and which stores arbitrary data for the return value. + An object that indicates some inner value is present """ __match_args__ = ("some_value",) @@ -44,7 +39,7 @@ def __init__(self, value: T) -> None: self._value = value def __repr__(self) -> str: - return "Some({})".format(repr(self._value)) + return f"Some({self._value!r})" def __eq__(self, other: Any) -> bool: return isinstance(other, Some) and self._value == other._value @@ -67,28 +62,6 @@ def some(self) -> T: """ return self._value - def nothing(self) -> None: - """ - Return `None`. - """ - return None - - @property - def value(self) -> T: - """ - Return the inner value. - - @deprecated Use `some_value` or `nothing_value` instead. This method will be - removed in a future version. - """ - warn( - "Accessing `.value` on Maybe type is deprecated, please use " - + "`.some_value` or '.nothing_value' instead", - DeprecationWarning, - stacklevel=2, - ) - return self._value - @property def some_value(self) -> T: """ @@ -102,24 +75,12 @@ def expect(self, _message: str) -> T: """ return self._value - def expect_nothing(self, message: str) -> NoReturn: - """ - Raise an UnwrapError since this type is `Some` - """ - raise UnwrapError(self, message) - def unwrap(self) -> T: """ Return the value. """ return self._value - def unwrap_nothing(self) -> NoReturn: - """ - Raise an UnwrapError since this type is `Some` - """ - raise UnwrapError(self, "Called `Maybe.unwrap_nothing()` on an `Some` value") - def unwrap_or(self, _default: U) -> T: """ Return the value. @@ -140,67 +101,63 @@ def unwrap_or_raise(self, e: object) -> T: def map(self, op: Callable[[T], U]) -> Some[U]: """ - The contained maybe is `Some`, so return `Some` with original value mapped to - a new value using the passed in function. + There is a contained value, so return `Some` with original value mapped + to a new value using the passed in function. """ return Some(op(self._value)) - def map_or(self, default: object, op: Callable[[T], U]) -> U: + def map_or(self, _default: object, op: Callable[[T], U]) -> U: """ - The contained maybe is `Some`, so return the original value mapped to a new - value using the passed in function. + There is a contained value, so return the original value mapped to a + new value using the passed in function. """ return op(self._value) - def map_or_else(self, default_op: object, op: Callable[[T], U]) -> U: + def map_or_else(self, _default_op: object, op: Callable[[T], U]) -> U: """ - The contained maybe is `Some`, so return original value mapped to - a new value using the passed in `op` function. + There is a contained value, so return original value mapped to a new + value using the passed in `op` function. """ return op(self._value) - def map_nothing(self, op: object) -> Some[T]: - """ - The contained maybe is `Some`, so return `Some` with the original value - """ - return self - - def and_then(self, op: Callable[[T], Maybe[U, E]]) -> Maybe[U, E]: + def and_then(self, op: Callable[[T], Maybe[U]]) -> Maybe[U]: """ - The contained maybe is `Some`, so return the maybe of `op` with the + There is a contained value, so return the maybe of `op` with the original value passed in """ return op(self._value) - def or_else(self, op: object) -> Some[T]: + def or_else(self, _op: object) -> Some[T]: """ - The contained maybe is `Some`, so return `Some` with the original value + There is a contained value, so return `Some` with the original value """ return self -class Nothing(Generic[E]): +class Nothing: """ - A value that signifies failure and which stores arbitrary data for the error. + An object that indicates no inner value is present """ __match_args__ = ("nothing_value",) - __slots__ = ("_value",) + __slots__ = () - def __init__(self, value: E) -> None: - self._value = value + def __init__(self) -> None: + pass def __repr__(self) -> str: - return "Nothing({})".format(repr(self._value)) + return "Nothing()" def __eq__(self, other: Any) -> bool: - return isinstance(other, Nothing) and self._value == other._value + return isinstance(other, Nothing) def __ne__(self, other: Any) -> bool: - return not (self == other) + return not isinstance(other, Nothing) def __hash__(self) -> int: - return hash((False, self._value)) + # A large random number is used here to avoid a hash collision with + # something else since there is no real inner value for us to hash. + return hash((False, 982006445019657274590041599673)) def is_some(self) -> Literal[False]: return False @@ -214,97 +171,51 @@ def some(self) -> None: """ return None - def nothing(self) -> E: - """ - Return the error. - """ - return self._value - - @property - def value(self) -> E: - """ - Return the inner value. - - @deprecated Use `some_value` or `nothing_value` instead. This method will be - removed in a future version. - """ - warn( - "Accessing `.value` on Maybe type is deprecated, please use " - + "`.some_value` or '.nothing_value' instead", - DeprecationWarning, - stacklevel=2, - ) - return self._value - - @property - def nothing_value(self) -> E: - """ - Return the inner value. - """ - return self._value - def expect(self, message: str) -> NoReturn: """ Raises an `UnwrapError`. """ exc = UnwrapError( self, - f"{message}: {self._value!r}", + f"{message}", ) - if isinstance(self._value, BaseException): - raise exc from self._value raise exc - def expect_nothing(self, _message: str) -> E: - """ - Return the inner value - """ - return self._value - def unwrap(self) -> NoReturn: """ Raises an `UnwrapError`. """ exc = UnwrapError( self, - f"Called `Maybe.unwrap()` on an `Nothing` value: {self._value!r}", + "Called `Maybe.unwrap()` on a `Nothing` value", ) - if isinstance(self._value, BaseException): - raise exc from self._value raise exc - def unwrap_nothing(self) -> E: - """ - Return the inner value - """ - return self._value - def unwrap_or(self, default: U) -> U: """ Return `default`. """ return default - def unwrap_or_else(self, op: Callable[[E], T]) -> T: + def unwrap_or_else(self, op: Callable[[], T]) -> T: """ - The contained maybe is ``Nothing``, so return the maybe of applying - ``op`` to the error value. + There is no contained value, so return a new value by calling `op`. """ - return op(self._value) + return op() def unwrap_or_raise(self, e: Type[TBE]) -> NoReturn: """ - The contained maybe is ``Nothing``, so raise the exception with the value. + There is no contained value, so raise the exception with the value. """ - raise e(self._value) + raise e() - def map(self, op: object) -> Nothing[E]: + def map(self, _op: object) -> Nothing: """ - Return `Nothing` with the same value + Return `Nothing` """ return self - def map_or(self, default: U, op: object) -> U: + def map_or(self, default: U, _op: object) -> U: """ Return the default value """ @@ -312,29 +223,21 @@ def map_or(self, default: U, op: object) -> U: def map_or_else(self, default_op: Callable[[], U], op: object) -> U: """ - Return the maybe of the default operation + Return the result of the `default_op` function """ return default_op() - def map_nothing(self, op: Callable[[E], F]) -> Nothing[F]: - """ - The contained maybe is `Nothing`, so return `Nothing` with original error mapped to - a new value using the passed in function. - """ - return Nothing(op(self._value)) - - def and_then(self, op: object) -> Nothing[E]: + def and_then(self, _op: object) -> Nothing: """ - The contained maybe is `Nothing`, so return `Nothing` with the original value + There is no contained value, so return `Nothing` """ return self - def or_else(self, op: Callable[[E], Maybe[T, F]]) -> Maybe[T, F]: + def or_else(self, op: Callable[[], Maybe[T]]) -> Maybe[T]: """ - The contained maybe is `Nothing`, so return the maybe of `op` with the - original value passed in + There is no contained value, so return the result of `op` """ - return op(self._value) + return op() # define Maybe as a generic type alias for use @@ -344,7 +247,7 @@ def or_else(self, op: Callable[[E], Maybe[T, F]]) -> Maybe[T, F]: Not all methods (https://doc.rust-lang.org/std/option/enum.Option.html) have been implemented, only the ones that make sense in the Python context. """ -Maybe: TypeAlias = Union[Some[T], Nothing[E]] +Maybe: TypeAlias = Union[Some[T], Nothing] """ A type to use in `isinstance` checks. @@ -359,91 +262,25 @@ class UnwrapError(Exception): The original ``Maybe`` can be accessed via the ``.maybe`` attribute, but this is not intended for regular use, as type information is lost: - ``UnwrapError`` doesn't know about both ``T`` and ``E``, since it's raised - from ``Some()`` or ``Nothing()`` which only knows about either ``T`` or ``E``, - not both. + ``UnwrapError`` doesn't know about ``T``, since it's raised from ``Some()`` + or ``Nothing()`` which only knows about either ``T`` or no-value, not both. """ - _maybe: Maybe[object, object] + _maybe: Maybe[object] - def __init__(self, maybe: Maybe[object, object], message: str) -> None: + def __init__(self, maybe: Maybe[object], message: str) -> None: self._maybe = maybe super().__init__(message) @property - def maybe(self) -> Maybe[Any, Any]: + def maybe(self) -> Maybe[Any]: """ Returns the original maybe. """ return self._maybe -def as_maybe( - *exceptions: Type[TBE], -) -> Callable[[Callable[P, R]], Callable[P, Maybe[R, TBE]]]: - """ - Make a decorator to turn a function into one that returns a ``Maybe``. - - Regular return values are turned into ``Some(return_value)``. Raised - exceptions of the specified exception type(s) are turned into ``Nothing(exc)``. - """ - if not exceptions or not all( - inspect.isclass(exception) and issubclass(exception, BaseException) - for exception in exceptions - ): - raise TypeError("as_maybe() requires one or more exception types") - - def decorator(f: Callable[P, R]) -> Callable[P, Maybe[R, TBE]]: - """ - Decorator to turn a function into one that returns a ``Maybe``. - """ - - @functools.wraps(f) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> Maybe[R, TBE]: - try: - return Some(f(*args, **kwargs)) - except exceptions as exc: - return Nothing(exc) - - return wrapper - - return decorator - - -def as_async_maybe( - *exceptions: Type[TBE], -) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[Maybe[R, TBE]]]]: - """ - Make a decorator to turn an async function into one that returns a ``Maybe``. - Regular return values are turned into ``Some(return_value)``. Raised - exceptions of the specified exception type(s) are turned into ``Nothing(exc)``. - """ - if not exceptions or not all( - inspect.isclass(exception) and issubclass(exception, BaseException) - for exception in exceptions - ): - raise TypeError("as_maybe() requires one or more exception types") - - def decorator( - f: Callable[P, Awaitable[R]] - ) -> Callable[P, Awaitable[Maybe[R, TBE]]]: - """ - Decorator to turn a function into one that returns a ``Maybe``. - """ - - @functools.wraps(f) - async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Maybe[R, TBE]: - try: - return Some(await f(*args, **kwargs)) - except exceptions as exc: - return Nothing(exc) - - return async_wrapper - - return decorator - - -def is_some(maybe: Maybe[T, E]) -> TypeGuard[Some[T]]: +def is_some(maybe: Maybe[T]) -> TypeGuard[Some[T]]: """A typeguard to check if a maybe is an Some Usage: @@ -456,7 +293,7 @@ def is_some(maybe: Maybe[T, E]) -> TypeGuard[Some[T]]: return maybe.is_some() -def is_nothing(maybe: Maybe[T, E]) -> TypeGuard[Nothing[E]]: +def is_nothing(maybe: Maybe[T]) -> TypeGuard[Nothing]: """A typeguard to check if a maybe is an Nothing Usage: diff --git a/tests/test_maybe.py b/tests/test_maybe.py index 411557f..2155dac 100644 --- a/tests/test_maybe.py +++ b/tests/test_maybe.py @@ -4,354 +4,194 @@ import pytest -from result import Err, Ok, OkErr, Result, UnwrapError, as_async_result, as_result +from maybe import Some, SomeNothing, Maybe, Nothing, UnwrapError -def test_ok_factories() -> None: - instance = Ok(1) +def test_some_factories() -> None: + instance = Some(1) assert instance._value == 1 - assert instance.is_ok() is True + assert instance.is_some() is True -def test_err_factories() -> None: - instance = Err(2) - assert instance._value == 2 - assert instance.is_err() is True +def test_nothing_factory() -> None: + instance = Nothing() + assert instance.is_nothing() is True def test_eq() -> None: - assert Ok(1) == Ok(1) - assert Err(1) == Err(1) - assert Ok(1) != Err(1) - assert Ok(1) != Ok(2) - assert Err(1) != Err(2) - assert not (Ok(1) != Ok(1)) - assert Ok(1) != "abc" - assert Ok("0") != Ok(0) + assert Some(1) == Some(1) + assert Nothing() == Nothing() + assert Some(1) != Nothing() + assert Some(1) != Some(2) + assert not (Some(1) != Some(1)) + assert Some(1) != "abc" + assert Some("0") != Some(0) def test_hash() -> None: - assert len({Ok(1), Err("2"), Ok(1), Err("2")}) == 2 - assert len({Ok(1), Ok(2)}) == 2 - assert len({Ok("a"), Err("a")}) == 2 + assert len({Some(1), Nothing(), Some(1), Nothing()}) == 2 + assert len({Some(1), Some(2)}) == 2 + assert len({Some("a"), Nothing()}) == 2 def test_repr() -> None: """ ``repr()`` returns valid code if the wrapped value's ``repr()`` does as well. """ - o = Ok(123) - n = Err(-1) + o = Some(123) + n = Nothing() - assert repr(o) == "Ok(123)" + assert repr(o) == "Some(123)" assert o == eval(repr(o)) - assert repr(n) == "Err(-1)" + assert repr(n) == "Nothing()" assert n == eval(repr(n)) -def test_ok_value() -> None: - res = Ok('haha') - assert res.ok_value == 'haha' +def test_some_value() -> None: + res = Some('haha') + assert res.some_value == 'haha' -def test_err_value() -> None: - res = Err('haha') - assert res.err_value == 'haha' +def test_some() -> None: + res = Some('haha') + assert res.is_some() is True + assert res.is_nothing() is False + assert res.some_value == 'haha' -def test_ok() -> None: - res = Ok('haha') - assert res.is_ok() is True - assert res.is_err() is False - assert res.ok_value == 'haha' +def test_nothing() -> None: + res = Nothing() + assert res.is_some() is False + assert res.is_nothing() is True -def test_err() -> None: - res = Err(':(') - assert res.is_ok() is False - assert res.is_err() is True - assert res.err_value == ':(' - - -def test_err_value_is_exception() -> None: - res = Err(ValueError("Some Error")) - assert res.is_ok() is False - assert res.is_err() is True - - with pytest.raises(UnwrapError): - res.unwrap() - - try: - res.unwrap() - except UnwrapError as e: - cause = e.__cause__ - assert isinstance(cause, ValueError) - - -def test_ok_method() -> None: - o = Ok('yay') - n = Err('nay') - assert o.ok() == 'yay' - assert n.ok() is None # type: ignore[func-returns-value] - - -def test_err_method() -> None: - o = Ok('yay') - n = Err('nay') - assert o.err() is None # type: ignore[func-returns-value] - assert n.err() == 'nay' +def test_some_method() -> None: + o = Some('yay') + n = Nothing() + assert o.some() == 'yay' + # TODO(francium): Can this type ignore directive be removed? mypy may fail? + assert n.some() is None # type: ignore[func-returns-value] def test_expect() -> None: - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() assert o.expect('failure') == 'yay' with pytest.raises(UnwrapError): n.expect('failure') -def test_expect_err() -> None: - o = Ok('yay') - n = Err('nay') - assert n.expect_err('hello') == 'nay' - with pytest.raises(UnwrapError): - o.expect_err('hello') - - def test_unwrap() -> None: - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() assert o.unwrap() == 'yay' with pytest.raises(UnwrapError): n.unwrap() -def test_unwrap_err() -> None: - o = Ok('yay') - n = Err('nay') - assert n.unwrap_err() == 'nay' - with pytest.raises(UnwrapError): - o.unwrap_err() - - def test_unwrap_or() -> None: - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() assert o.unwrap_or('some_default') == 'yay' assert n.unwrap_or('another_default') == 'another_default' def test_unwrap_or_else() -> None: - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() assert o.unwrap_or_else(str.upper) == 'yay' - assert n.unwrap_or_else(str.upper) == 'NAY' + assert n.unwrap_or_else(lambda: 'default') == 'default' def test_unwrap_or_raise() -> None: - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() assert o.unwrap_or_raise(ValueError) == 'yay' with pytest.raises(ValueError) as exc_info: n.unwrap_or_raise(ValueError) - assert exc_info.value.args == ('nay',) + assert exc_info.value.args == () def test_map() -> None: - o = Ok('yay') - n = Err('nay') - assert o.map(str.upper).ok() == 'YAY' - assert n.map(str.upper).err() == 'nay' - - num = Ok(3) - errnum = Err(2) - assert num.map(str).ok() == '3' - assert errnum.map(str).err() == 2 + o = Some('yay') + n = Nothing() + assert o.map(str.upper).some() == 'YAY' + assert n.map(str.upper).is_nothing() def test_map_or() -> None: - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() assert o.map_or('hay', str.upper) == 'YAY' assert n.map_or('hay', str.upper) == 'hay' - num = Ok(3) - errnum = Err(2) - assert num.map_or('-1', str) == '3' - assert errnum.map_or('-1', str) == '-1' - def test_map_or_else() -> None: - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() assert o.map_or_else(lambda: 'hay', str.upper) == 'YAY' assert n.map_or_else(lambda: 'hay', str.upper) == 'hay' - num = Ok(3) - errnum = Err(2) - assert num.map_or_else(lambda: '-1', str) == '3' - assert errnum.map_or_else(lambda: '-1', str) == '-1' - - -def test_map_err() -> None: - o = Ok('yay') - n = Err('nay') - assert o.map_err(str.upper).ok() == 'yay' - assert n.map_err(str.upper).err() == 'NAY' - def test_and_then() -> None: - assert Ok(2).and_then(sq).and_then(sq).ok() == 16 - assert Ok(2).and_then(sq).and_then(to_err).err() == 4 - assert Ok(2).and_then(to_err).and_then(sq).err() == 2 - assert Err(3).and_then(sq).and_then(sq).err() == 3 + assert Some(2).and_then(sq).and_then(sq).some() == 16 + assert Some(2).and_then(sq).and_then(to_nothing).is_nothing() + assert Some(2).and_then(to_nothing).and_then(sq).is_nothing() + assert Nothing().and_then(sq).and_then(sq).is_nothing() - assert Ok(2).and_then(sq_lambda).and_then(sq_lambda).ok() == 16 - assert Ok(2).and_then(sq_lambda).and_then(to_err_lambda).err() == 4 - assert Ok(2).and_then(to_err_lambda).and_then(sq_lambda).err() == 2 - assert Err(3).and_then(sq_lambda).and_then(sq_lambda).err() == 3 + assert Some(2).and_then(sq_lambda).and_then(sq_lambda).some() == 16 + assert Some(2).and_then(sq_lambda).and_then(to_nothing_lambda).is_nothing() + assert Some(2).and_then(to_nothing_lambda).and_then(sq_lambda).is_nothing() + assert Nothing().and_then(sq_lambda).and_then(sq_lambda).is_nothing() def test_or_else() -> None: - assert Ok(2).or_else(sq).or_else(sq).ok() == 2 - assert Ok(2).or_else(to_err).or_else(sq).ok() == 2 - assert Err(3).or_else(sq).or_else(to_err).ok() == 9 - assert Err(3).or_else(to_err).or_else(to_err).err() == 3 + assert Some(2).or_else(sq).or_else(sq).some() == 2 + assert Some(2).or_else(to_nothing).or_else(sq).some() == 2 + assert Nothing().or_else(lambda: sq(3)).or_else(lambda: to_nothing(2)).some() == 9 + assert Nothing().or_else(lambda: to_nothing(2)).or_else(lambda: to_nothing(2)).is_nothing() - assert Ok(2).or_else(sq_lambda).or_else(sq).ok() == 2 - assert Ok(2).or_else(to_err_lambda).or_else(sq_lambda).ok() == 2 - assert Err(3).or_else(sq_lambda).or_else(to_err_lambda).ok() == 9 - assert Err(3).or_else(to_err_lambda).or_else(to_err_lambda).err() == 3 + assert Some(2).or_else(sq_lambda).or_else(sq).some() == 2 + assert Some(2).or_else(to_nothing_lambda).or_else(sq_lambda).some() == 2 def test_isinstance_result_type() -> None: - o = Ok('yay') - n = Err('nay') - assert isinstance(o, OkErr) - assert isinstance(n, OkErr) - assert not isinstance(1, OkErr) + o = Some('yay') + n = Nothing() + assert isinstance(o, SomeNothing) + assert isinstance(n, SomeNothing) + assert not isinstance(1, SomeNothing) def test_error_context() -> None: - n = Err('nay') + n = Nothing() with pytest.raises(UnwrapError) as exc_info: n.unwrap() exc = exc_info.value - assert exc.result is n + assert exc.maybe is n def test_slots() -> None: """ - Ok and Err have slots, so assigning arbitrary attributes fails. + Some and Nothing have slots, so assigning arbitrary attributes fails. """ - o = Ok('yay') - n = Err('nay') + o = Some('yay') + n = Nothing() with pytest.raises(AttributeError): 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 functions into ones that return a ``Result``. - """ - - @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 - - -def test_as_result_type_checking() -> None: - """ - The ``as_result()`` is a signature-preserving decorator. - """ - - @as_result(ValueError) - def f(a: int) -> int: - return a - - res: Result[int, ValueError] - res = f(123) # No mypy error here. - assert res.ok() == 123 - - -@pytest.mark.asyncio -async def test_as_async_result() -> None: - """ - ``as_async_result()`` turns functions into ones that return a ``Result``. - """ - - @as_async_result(ValueError) - async def good(value: int) -> int: - return value - - @as_async_result(IndexError, ValueError) - async def bad(value: int) -> int: - raise ValueError - - good_result = await good(123) - bad_result = await 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 sq(i: int) -> Result[int, int]: - return Ok(i * i) +def sq(i: int) -> Maybe[int]: + return Some(i * i) -def to_err(i: int) -> Result[int, int]: - return Err(i) +def to_nothing(_: int) -> Maybe[int]: + return Nothing() # Lambda versions of the same functions, just for test/type coverage -sq_lambda: Callable[[int], Result[int, int]] = lambda i: Ok(i * i) -to_err_lambda: Callable[[int], Result[int, int]] = lambda i: Err(i) +sq_lambda: Callable[[int], Maybe[int]] = lambda i: Some(i * i) +to_nothing_lambda: Callable[[int], Maybe[int]] = lambda _: Nothing() diff --git a/tests/test_pattern_matching.py b/tests/test_pattern_matching.py index fb73a80..3bccf54 100644 --- a/tests/test_pattern_matching.py +++ b/tests/test_pattern_matching.py @@ -1,15 +1,15 @@ from __future__ import annotations -from result import Err, Ok, Result +from maybe import Nothing, Some, Maybe -def test_pattern_matching_on_ok_type() -> None: +def test_pattern_matching_on_some_type() -> None: """ - Pattern matching on ``Ok()`` matches the contained value. + Pattern matching on ``Some()`` matches the contained value. """ - o: Result[str, int] = Ok("yay") + o: Maybe[str] = Some("yay") match o: - case Ok(value): + case Some(value): reached = True assert value == "yay" @@ -20,10 +20,9 @@ def test_pattern_matching_on_err_type() -> None: """ Pattern matching on ``Err()`` matches the contained value. """ - n: Result[int, str] = Err("nay") + n: Maybe[int] = Nothing() match n: - case Err(value): + case Nothing(): reached = True - assert value == "nay" assert reached diff --git a/tests/type-checking/test_maybe.yml b/tests/type-checking/test_maybe.yml index 2322b15..505440f 100644 --- a/tests/type-checking/test_maybe.yml +++ b/tests/type-checking/test_maybe.yml @@ -1,95 +1,87 @@ --- -# reveal_type(res3) # N: Revealed type is "result.result.Err[builtins.int]" +# reveal_type(res3) # N: Revealed type is "maybe.maybe.Nothing" - case: failure_lash disable_cache: false main: | from typing import Callable, List, Optional - from result import Result, Ok, Err + from maybe import Maybe, Some, Nothing - res1: Result[str, int] = Ok('hello') - reveal_type(res1) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.int]]" - if isinstance(res1, Ok): - ok: Ok[str] = res1 - reveal_type(ok) # N: Revealed type is "result.result.Ok[builtins.str]" - okValue: str = res1.ok() - reveal_type(okValue) # N: Revealed type is "builtins.str" + res1: Maybe[str] = Some('hello') + reveal_type(res1) # N: Revealed type is "Union[maybe.maybe.Some[builtins.str], maybe.maybe.Nothing]" + if isinstance(res1, Some): + some: Some[str] = res1 + reveal_type(some) # N: Revealed type is "maybe.maybe.Some[builtins.str]" + someValue: str = res1.some() + reveal_type(someValue) # N: Revealed type is "builtins.str" mapped_to_float: float = res1.map_or(1.0, lambda s: len(s) * 1.5) reveal_type(mapped_to_float) # N: Revealed type is "builtins.float" else: - err: Err[int] = res1 - reveal_type(err) # N: Revealed type is "result.result.Err[builtins.int]" - errValue: int = err.err() - reveal_type(errValue) # N: Revealed type is "builtins.int" - mapped_to_list: Optional[List[int]] = res1.map_err(lambda e: [e]).err() - reveal_type(mapped_to_list) # N: Revealed type is "Union[builtins.list[builtins.int], None]" + nothing: Nothing = res1 + reveal_type(nothing) # N: Revealed type is "maybe.maybe.Nothing" # Test constructor functions - res2 = Ok(42) - reveal_type(res2) # N: Revealed type is "result.result.Ok[builtins.int]" - res3 = Err(1) - reveal_type(res3) # N: Revealed type is "result.result.Err[builtins.int]" - - res4 = Ok(4) - add1: Callable[[int], Result[int, str]] = lambda i: Ok(i + 1) - toint: Callable[[str], Result[int, str]] = lambda i: Ok(int(i)) + res2 = Some(42) + reveal_type(res2) # N: Revealed type is "maybe.maybe.Some[builtins.int]" + res3 = Nothing() + reveal_type(res3) # N: Revealed type is "maybe.maybe.Nothing" + + res4 = Some(4) + add1: Callable[[int], Maybe[int]] = lambda i: Some(i + 1) + toint: Callable[[str], Maybe[int]] = lambda i: Some(int(i)) res5 = res4.and_then(add1) - reveal_type(res5) # N: Revealed type is "Union[result.result.Ok[builtins.int], result.result.Err[builtins.str]]" + reveal_type(res5) # N: Revealed type is "Union[maybe.maybe.Some[builtins.int], maybe.maybe.Nothing]" res6 = res4.or_else(toint) - reveal_type(res6) # N: Revealed type is "result.result.Ok[builtins.int]" + reveal_type(res6) # N: Revealed type is "maybe.maybe.Some[builtins.int]" - case: covariance disable_cache: false main: | - from result import Result, Ok, Err + from maybe import Maybe, Some, Nothing - ok_int: Ok[int] = Ok(42) - ok_float: Ok[float] = ok_int - ok_int = ok_float # E: Incompatible types in assignment (expression has type "Ok[float]", variable has type "Ok[int]") [assignment] + some_int: Some[int] = Some(42) + some_float: Some[float] = some_int + some_int = some_float # E: Incompatible types in assignment (expression has type "Some[float]", variable has type "Some[int]") [assignment] - err_type: Err[TypeError] = Err(TypeError("foo")) - err_exc: Err[Exception] = err_type - err_type = err_exc # E: Incompatible types in assignment (expression has type "Err[Exception]", variable has type "Err[TypeError]") [assignment] + nothing: Nothing = Nothing() - result_int_type: Result[int, TypeError] = ok_int or err_type - result_float_exc: Result[float, Exception] = result_int_type - result_int_type = result_float_exc # E: Incompatible types in assignment (expression has type "Ok[float] | Err[Exception]", variable has type "Ok[int] | Err[TypeError]") [assignment] + maybe_int: Maybe[int] = some_int or nothing + maybe_float: Maybe[float] = maybe_int + maybe_int = maybe_float # E: Incompatible types in assignment (expression has type "Some[float] | Nothing", variable has type "Some[int] | Nothing") [assignment] -- case: map_ok_err +- case: map_ok disable_cache: false main: | - from result import Err, Ok + from maybe import Maybe, Some, Nothing - o = Ok("42") - reveal_type(o.map(int)) # N: Revealed type is "result.result.Ok[builtins.int]" - reveal_type(o.map_err(int)) # N: Revealed type is "result.result.Ok[builtins.str]" + s = Some("42") + reveal_type(s.map(int)) # N: Revealed type is "maybe.maybe.Some[builtins.int]" - e = Err("42") - reveal_type(e.map(int)) # N: Revealed type is "result.result.Err[builtins.str]" - reveal_type(e.map_err(int)) # N: Revealed type is "result.result.Err[builtins.int]" + n = Nothing() + reveal_type(n.map(int)) # N: Revealed type is "maybe.maybe.Nothing" -- case: map_result +- case: map_maybe disable_cache: false main: | - from result import Result, Ok + from maybe import Maybe, Some - greeting_res: Result[str, ValueError] = Ok("Hello") + greeting: Maybe[str] = Some("Hello") - personalized_greeting_res = greeting_res.map(lambda g: f"{g}, John") - reveal_type(personalized_greeting_res) # N: Revealed type is "Union[result.result.Ok[builtins.str], result.result.Err[builtins.ValueError]]" + personalized_greeting = greeting.map(lambda g: f"{g}, John") + reveal_type(personalized_greeting) # N: Revealed type is "Union[maybe.maybe.Some[builtins.str], maybe.maybe.Nothing]" - personalized_greeting = personalized_greeting_res.ok() - reveal_type(personalized_greeting) # N: Revealed type is "Union[builtins.str, None]" + some = personalized_greeting.some() + reveal_type(some) # N: Revealed type is "Union[builtins.str, None]" -- case: map_result +- case: map_maybe disable_cache: false main: | - from result import Ok, Err, is_ok, is_err - - result = Ok(1) - err = Err("error") - if is_ok(result): - reveal_type(result) # N: Revealed type is "result.result.Ok[builtins.int]" - elif is_err(err): - reveal_type(err) # N: Revealed type is "result.result.Err[builtins.str]" + from maybe import Maybe, Some, Nothing, is_some, is_nothing + + maybe = Some(1) + nothing = Nothing() + if is_some(maybe): + reveal_type(maybe) # N: Revealed type is "maybe.maybe.Some[builtins.int]" + elif is_nothing(nothing): + reveal_type(nothing) # N: Revealed type is "maybe.maybe.Nothing"