diff --git a/CHANGELOG.md b/CHANGELOG.md index da9d9e518..073256bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ We follow Semantic Versions since the `0.1.0` release. ### Features +- Reintroduces the `Maybe` monad, typed! - Adds `mypy` plugin to type decorators - Complete rewrite of `Result` types - Partial API change, now `Success` and `Failure` are not types, but functions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29a6dea7d..044d2e329 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,22 @@ flake8 returns tests docs These steps are mandatory during the CI. +### Fixing pytest coverage issue + +Coverage does not work well with `pytest-mypy-plugin`, +that's why we have two phases of `pytest` run. + +If you accidentally mess things up +and see `INTERNALERROR> coverage.misc.CoverageException` in your log, +do: + +```bash +rm .coverage* +rm -rf .pytest_cache htmlcov +``` + +And it should solve it. +Then use correct test commands. ## Type checks diff --git a/README.md b/README.md index 2bb1a2894..4e8f74790 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Make sure you know how to get started, [check out our docs](https://returns.read - [Result container](#result-container) that let's you to get rid of exceptions - [IO marker](#io-marker) that marks all impure operations and structures them +- [Maybe container](#maybe-container) that allows you to write `None`-free code ## Result container @@ -215,6 +216,38 @@ Whenever we access `FetchUserProfile` we now know that it does `IO` and might fail. So, we act accordingly! + +## Maybe container + +Have you ever since code with a lot of `if some is not None` conditions? +It really bloats your source code and makes it unreadable. + +But, having `None` in your source code is even worth. +Actually, `None` is called the [worth mistake in the history of Computer Science](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/). + +So, what to do? Use `Maybe` container! +It consists of `Some(...)` and `Nothing` types, +representing existing state and `None` state respectively. + +```python +from typing import Optional +from returns.maybe import Maybe + +def bad_function() -> Optional[int]: + ... + +maybe_result: Maybe[float] = Maybe.new( + bad_function(), +).map( + lambda number: number / 2 +) +# => Maybe will return Some(float) only if there's a non-None value +# Otherwise, will return Nothing +``` + +Forget about `None`-related errors forever! + + ## More! Want more? [Go to the docs!](https://returns.readthedocs.io) diff --git a/docs/index.rst b/docs/index.rst index da7c7901f..bb597a37a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,7 @@ Contents pages/container.rst pages/result.rst + pages/maybe.rst pages/io.rst pages/unsafe.rst pages/functions.rst diff --git a/docs/pages/maybe.rst b/docs/pages/maybe.rst new file mode 100644 index 000000000..315f5da44 --- /dev/null +++ b/docs/pages/maybe.rst @@ -0,0 +1,110 @@ +Maybe +===== + +The ``Maybe`` container is used when a series of computations +could return ``None`` at any point. + + +Maybe container +--------------- + +``Maybe`` consist of two types: ``Some`` and ``Nothing``. +We have a convenient method to create different ``Maybe`` types +based on just a single value: + +.. code:: python + + from returns.maybe import Maybe + + Maybe.new(1) + # => Some(1) + + Maybe.new(None) + # => Nothing + + +Usage +----- + +It might be very useful for complex operations like the following one: + +.. code:: python + + from dataclasses import dataclass + from typing import Optional + + @dataclass + class Address(object): + street: Optional[str] + + @dataclass + class User(object): + address: Optional[Address] + + @dataclass + class Order(object): + user: Optional[User] + + order: Order # some existing Order instance + street: Maybe[str] = Maybe.new(order.user).map( + lambda user: user.address, + ).map( + lambda address: address.street, + ) + # => `Some('address street info')` if all fields are not None + # => `Nothing` if at least one field is `None` + +Optional type +~~~~~~~~~~~~~ + +One may ask: "How is that different to the ``Optional[]`` type?" +That's a really good question! + +Consider the same code to get the street name +without ``Maybe`` and using raw ``Optional`` values: + +.. code:: python + + order: Order # some existing Order instance + street: Optional[str] = None + if order.user is not None: + if order.user.address is not None: + street = order.user.address.street + +It looks way uglier and can grow even more uglier and complex +when new logic will be introduced. + + +@maybe decorator +---------------- + +Sometimes we have to deal with functions +that dears to return ``Optional`` values! + +We have to work with the carefully and write ``if x is not None:`` everywhere. +Luckily, we have your back! ``maybe`` function decorates +any other function that returns ``Optional`` +and converts it to return ``Maybe`` instead: + +.. code:: python + + from typing import Optional + from returns.maybe import Maybe, maybe + + @maybe + def number(num: int) -> Optional[int]: + if number > 0: + return num + return None + + result: Maybe[int] = number(1) + # => 1 + + +API Reference +------------- + +.. autoclasstree:: returns.maybe + +.. automodule:: returns.maybe + :members: diff --git a/returns/__init__.py b/returns/__init__.py index 7d9b5e54a..8a9bf9942 100644 --- a/returns/__init__.py +++ b/returns/__init__.py @@ -15,17 +15,12 @@ from returns.functions import compose, raise_exception from returns.io import IO, impure +from returns.maybe import Maybe, Nothing, Some, maybe +from returns.pipeline import is_successful, pipeline from returns.primitives.exceptions import UnwrapFailedError -from returns.result import ( - Failure, - Result, - Success, - is_successful, - pipeline, - safe, -) +from returns.result import Failure, Result, Success, safe -__all__ = ( # noqa: Z410 +__all__ = ( # Functions: 'compose', 'raise_exception', @@ -34,12 +29,20 @@ 'IO', 'impure', + # Maybe: + 'Some', + 'Nothing', + 'Maybe', + 'maybe', + # Result: - 'is_successful', 'safe', - 'pipeline', 'Failure', 'Result', 'Success', 'UnwrapFailedError', + + # pipeline: + 'is_successful', + 'pipeline', ) diff --git a/returns/contrib/mypy/decorator_plugin.py b/returns/contrib/mypy/decorator_plugin.py index cc271b2fd..54b001166 100644 --- a/returns/contrib/mypy/decorator_plugin.py +++ b/returns/contrib/mypy/decorator_plugin.py @@ -26,6 +26,7 @@ _TYPED_DECORATORS = { 'returns.result.safe', 'returns.io.impure', + 'returns.maybe.maybe', } diff --git a/returns/maybe.py b/returns/maybe.py new file mode 100644 index 000000000..047531498 --- /dev/null +++ b/returns/maybe.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +from abc import ABCMeta +from functools import wraps +from inspect import iscoroutinefunction +from typing import TypeVar + +from returns.primitives.container import ( + FixableContainer, + GenericContainerOneSlot, + ValueUnwrapContainer, +) +from returns.primitives.exceptions import UnwrapFailedError + +_ValueType = TypeVar('_ValueType') + + +class Maybe( + GenericContainerOneSlot[_ValueType], + FixableContainer, + ValueUnwrapContainer, + metaclass=ABCMeta, +): + """ + Represents a result of a series of commutation that can return ``None``. + + An alternative to using exceptions or constant ``is None`` checks. + ``Maybe`` is an abstract type and should not be instantiated directly. + Instead use ``Some`` and ``Nothing``. + """ + + @classmethod + def new(cls, inner_value): + """Creates new instance of Maybe container based on a value.""" + if inner_value is None: + return _Nothing(inner_value) + return _Some(inner_value) + + +class _Nothing(Maybe[None]): # noqa: Z214 + """Represents an empty state.""" + + def __init__(self, inner_value=None): + """ + Wraps the given value in the Container. + + 'value' can only be ``None``. + """ + object.__setattr__(self, '_inner_value', inner_value) # noqa: Z462 + + def __str__(self): + """Custom str definition without state inside.""" + return '' + + def map(self, function): # noqa: A003 + """Returns the 'Nothing' instance that was used to call the method.""" + return self + + def bind(self, function): + """Returns the 'Nothing' instance that was used to call the method.""" + return self + + def fix(self, function): + """ + Applies function to the inner value. + + Applies 'function' to the contents of the 'Some' instance + and returns a new 'Some' object containing the result. + 'function' should not accept any arguments + and return a non-container result. + """ + return Maybe.new(function()) + + def rescue(self, function): + """ + Applies 'function' to the result of a previous calculation. + + 'function' should not accept any arguments + and return Maybe a 'Nothing' or 'Some' type object. + """ + return function() + + def value_or(self, default_value): + """Returns the value if we deal with 'Some' or default if 'Nothing'.""" + return default_value + + def unwrap(self): + """Raises an exception, since it does not have a value inside.""" + raise UnwrapFailedError(self) + + def failure(self): + """Unwraps inner error value from failed container.""" + return self._inner_value + + +class _Some(Maybe[_ValueType]): + """ + Represents a calculation which has succeeded and contains the value. + + Quite similar to ``Success`` type. + """ + + def map(self, function): # noqa: A003 + """ + Applies function to the inner value. + + Applies 'function' to the contents of the 'Some' instance + and returns a new 'Maybe' object containing the result. + 'function' should accept a single "normal" (non-container) argument + and return a non-container result. + """ + return Maybe.new(function(self._inner_value)) + + def bind(self, function): + """ + Applies 'function' to the result of a previous calculation. + + 'function' should accept a single "normal" (non-container) argument + and return 'Nothing' or 'Some' type object. + """ + return function(self._inner_value) + + def fix(self, function): + """Returns the 'Some' instance that was used to call the method.""" + return self + + def rescue(self, function): + """Returns the 'Some' instance that was used to call the method.""" + return self + + def value_or(self, default_value): + """Returns the value if we deal with 'Some' or default if 'Nothing'.""" + return self._inner_value + + def unwrap(self): + """Returns the unwrapped value from the inside of this container.""" + return self._inner_value + + def failure(self): + """Raises an exception, since it does not have an error inside.""" + raise UnwrapFailedError(self) + + +def Some(inner_value): # noqa: N802 + """Public unit function of protected `_Some` type.""" + return _Some(inner_value) + + +#: Public unit value of protected `_Nothing` type. +Nothing = _Nothing() + + +def maybe(function): + """ + Decorator to covert ``None`` returning function to ``Maybe`` container. + + Supports both async and regular functions. + """ + if iscoroutinefunction(function): + async def decorator(*args, **kwargs): + regular_result = await function(*args, **kwargs) + if regular_result is None: + return Nothing + return Some(regular_result) + else: + def decorator(*args, **kwargs): + regular_result = function(*args, **kwargs) + if regular_result is None: + return Nothing + return Some(regular_result) + return wraps(function)(decorator) diff --git a/returns/maybe.pyi b/returns/maybe.pyi new file mode 100644 index 000000000..3160636c2 --- /dev/null +++ b/returns/maybe.pyi @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +from abc import ABCMeta +from typing import Any, Callable, Coroutine, Optional, TypeVar, Union, overload + +from typing_extensions import final + +from returns.primitives.container import ( + FixableContainer, + GenericContainerOneSlot, + ValueUnwrapContainer, +) + +_ValueType = TypeVar('_ValueType') +_NewValueType = TypeVar('_NewValueType') +_ErrorType = TypeVar('_ErrorType') + + +class Maybe( + GenericContainerOneSlot[_ValueType], + FixableContainer, + ValueUnwrapContainer, + metaclass=ABCMeta, +): + @classmethod + def new(cls, inner_value: Optional[_ValueType]) -> 'Maybe[_ValueType]': + ... + + def map( # noqa: A003 + self, + function: Callable[[_ValueType], Optional[_NewValueType]], + ) -> 'Maybe[_NewValueType]': + ... + + def bind( + self, + function: Callable[[_ValueType], 'Maybe[_NewValueType]'], + ) -> 'Maybe[_NewValueType]': + ... + + def fix( + self, + function: Callable[[], Optional[_NewValueType]], + ) -> 'Maybe[_NewValueType]': + ... + + def rescue( + self, + function: Callable[[], 'Maybe[_NewValueType]'], + ) -> 'Maybe[_NewValueType]': + ... + + def value_or( + self, + default_value: _NewValueType, + ) -> Union[_ValueType, _NewValueType]: + ... + + def unwrap(self) -> _ValueType: + ... + + def failure(self) -> None: + ... + + +@final +class _Nothing(Maybe[Any]): + _inner_value: None + + def __init__(self, inner_value: None = ...) -> None: # noqa: Z459 + ... + + +@final +class _Some(Maybe[_ValueType]): + _inner_value: _ValueType + + def __init__(self, inner_value: _ValueType) -> None: + ... + + +def Some(inner_value: Optional[_ValueType]) -> Maybe[_ValueType]: # noqa: N802 + ... + + +Nothing: Maybe[Any] + + +@overload # noqa: Z320 +def maybe( # type: ignore + function: Callable[ + ..., + Coroutine[_ValueType, _ErrorType, Optional[_NewValueType]], + ], +) -> Callable[ + ..., + Coroutine[_ValueType, _ErrorType, Maybe[_NewValueType]], +]: + ... + + +@overload +def maybe( + function: Callable[..., Optional[_NewValueType]], +) -> Callable[..., Maybe[_NewValueType]]: + ... diff --git a/returns/pipeline.py b/returns/pipeline.py new file mode 100644 index 000000000..14c39cd08 --- /dev/null +++ b/returns/pipeline.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +from functools import wraps +from inspect import iscoroutinefunction + +from returns.primitives.exceptions import UnwrapFailedError + + +def is_successful(container): + """ + Determins if a container was successful or not. + + We treat container that raise ``UnwrapFailedError`` on ``.unwrap()`` + not successful. + """ + try: + container.unwrap() + except UnwrapFailedError: + return False + else: + return True + + +def pipeline(function): # noqa: C901 + """ + Decorator to enable 'do-notation' context. + + Should be used for series of computations that rely on ``.unwrap`` method. + + Supports both async and regular functions. + """ + if iscoroutinefunction(function): + async def decorator(*args, **kwargs): + try: + return await function(*args, **kwargs) + except UnwrapFailedError as exc: + return exc.halted_container + else: + def decorator(*args, **kwargs): + try: + return function(*args, **kwargs) + except UnwrapFailedError as exc: + return exc.halted_container + return wraps(function)(decorator) diff --git a/returns/pipeline.pyi b/returns/pipeline.pyi new file mode 100644 index 000000000..3b3321d33 --- /dev/null +++ b/returns/pipeline.pyi @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from typing import Callable, Coroutine, TypeVar, Union, overload + +from returns.maybe import Maybe +from returns.result import Result + +# Logical aliases: +_Unwrapable = Union[Result, Maybe] + +# Just aliases: +_FirstType = TypeVar('_FirstType') +_SecondType = TypeVar('_SecondType') + +# Hacks for functions: +_ReturnsResultType = TypeVar( + '_ReturnsResultType', + bound=Callable[..., _Unwrapable], +) +_AsyncReturnsResultType = TypeVar( + '_AsyncReturnsResultType', + bound=Callable[..., Coroutine[_FirstType, _SecondType, _Unwrapable]], +) + + +def is_successful(container: _Unwrapable) -> bool: + ... + + +@overload +def pipeline( + function: _AsyncReturnsResultType, +) -> _AsyncReturnsResultType: + ... + + +@overload +def pipeline(function: _ReturnsResultType) -> _ReturnsResultType: + ... diff --git a/returns/result.py b/returns/result.py index 53d3cde90..ee6878b4d 100644 --- a/returns/result.py +++ b/returns/result.py @@ -136,26 +136,11 @@ def Failure(inner_value): # noqa: N802 return _Failure(inner_value) -def is_successful(container): - """ - Determins if a container was successful or not. - - We treat container that raise ``UnwrapFailedError`` on ``.unwrap()`` - not successful. - """ - try: - container.unwrap() - except UnwrapFailedError: - return False - else: - return True - - def safe(function): # noqa: C901 """ Decorator to covert exception throwing function to 'Result' container. - Show be used with care, since it only catches 'Exception' subclasses. + Should be used with care, since it only catches 'Exception' subclasses. It does not catch 'BaseException' subclasses. Supports both async and regular functions. @@ -163,36 +148,13 @@ def safe(function): # noqa: C901 if iscoroutinefunction(function): async def decorator(*args, **kwargs): try: - return _Success(await function(*args, **kwargs)) + return Success(await function(*args, **kwargs)) except Exception as exc: - return _Failure(exc) + return Failure(exc) else: def decorator(*args, **kwargs): try: - return _Success(function(*args, **kwargs)) + return Success(function(*args, **kwargs)) except Exception as exc: - return _Failure(exc) - return wraps(function)(decorator) - - -def pipeline(function): # noqa: C901 - """ - Decorator to enable 'do-notation' context. - - Should be used for series of computations that rely on ``.unwrap`` method. - - Supports both async and regular functions. - """ - if iscoroutinefunction(function): - async def decorator(*args, **kwargs): - try: - return await function(*args, **kwargs) - except UnwrapFailedError as exc: - return exc.halted_container - else: - def decorator(*args, **kwargs): - try: - return function(*args, **kwargs) - except UnwrapFailedError as exc: - return exc.halted_container + return Failure(exc) return wraps(function)(decorator) diff --git a/returns/result.pyi b/returns/result.pyi index 12beee8ab..4ab4e6dfe 100644 --- a/returns/result.pyi +++ b/returns/result.pyi @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from abc import ABCMeta -from typing import Any, Callable, Coroutine, NoReturn, TypeVar, Union, overload +from typing import Any, Callable, Coroutine, TypeVar, Union, overload from typing_extensions import final @@ -19,21 +19,6 @@ _NewValueType = TypeVar('_NewValueType') _ErrorType = TypeVar('_ErrorType') _NewErrorType = TypeVar('_NewErrorType') -# Just aliases: -_FirstType = TypeVar('_FirstType') -_SecondType = TypeVar('_SecondType') -_ThirdType = TypeVar('_ThirdType') - -# Hacks for functions: -_ReturnsResultType = TypeVar( - '_ReturnsResultType', - bound=Callable[..., 'Result'], -) -_AsyncReturnsResultType = TypeVar( - '_AsyncReturnsResultType', - bound=Callable[..., Coroutine[_FirstType, _SecondType, 'Result']], -) - class Result( GenericContainerTwoSlots[_ValueType, _ErrorType], @@ -77,10 +62,10 @@ class Result( ) -> Union[_ValueType, _NewValueType]: ... - def unwrap(self) -> Union[_ValueType, NoReturn]: + def unwrap(self) -> _ValueType: ... - def failure(self) -> Union[_ErrorType, NoReturn]: + def failure(self) -> _ErrorType: ... @@ -108,25 +93,6 @@ def Failure(inner_value: _ErrorType) -> Result[Any, _ErrorType]: # noqa: N802 ... -def is_successful(container: Result) -> bool: - ... - - -# Typing decorators is not an easy task, see: -# https://github.com/python/mypy/issues/3157 - -@overload -def pipeline( - function: _AsyncReturnsResultType, -) -> _AsyncReturnsResultType: - ... - - -@overload -def pipeline(function: _ReturnsResultType) -> _ReturnsResultType: - ... - - @overload # noqa: Z320 def safe( # type: ignore function: Callable[..., Coroutine[_ValueType, _ErrorType, _NewValueType]], diff --git a/setup.cfg b/setup.cfg index a4a0e700d..543d9b732 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,7 @@ per-file-ignores = # Disable some pydocstyle checks for package: returns/*.py: D104 # Disable imports in `__init__.py`: - returns/__init__.py: F401, Z412 + returns/__init__.py: F401, Z410, Z412 # There are multiple assert's in tests: tests/*.py: S101, Z202, Z440 # Disable some pydocstyle checks globally: diff --git a/tests/test_maybe/test_maybe_bind.py b/tests/test_maybe/test_maybe_bind.py new file mode 100755 index 000000000..95f02e5dc --- /dev/null +++ b/tests/test_maybe/test_maybe_bind.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +from returns.maybe import Maybe, Nothing, Some + + +def test_bind_some(): + """Ensures that left identity works for Some container.""" + def factory(inner_value: int) -> Maybe[int]: + return Some(inner_value * 2) + + input_value = 5 + bound = Some(input_value).bind(factory) + + assert bound == factory(input_value) + assert str(bound) == '' + + +def test_bind_nothing(): + """Ensures that left identity works for Nothing container.""" + def factory(inner_value) -> Maybe[int]: + return Some(1) + + bound = Nothing.bind(factory) + + assert bound == Nothing + assert str(bound) == '' + + +def test_rescue_some(): + """Ensures that rescue works for Some container.""" + def factory() -> Maybe[int]: + return Some(10) + + bound = Some(5).rescue(factory) + + assert bound == Some(5) + + +def test_rescue_nothing(): + """Ensures that rescue works for Nothing container.""" + def factory() -> Maybe[int]: + return Some(1) + + bound = Nothing.rescue(factory) + + assert bound == Some(1) diff --git a/tests/test_maybe/test_maybe_equality.py b/tests/test_maybe/test_maybe_equality.py new file mode 100755 index 000000000..0b4ad835e --- /dev/null +++ b/tests/test_maybe/test_maybe_equality.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +import pytest + +from returns.maybe import Nothing, Some, _Nothing +from returns.primitives.exceptions import ImmutableStateError + + +def test_equality(): + """Ensures that containers can be compared.""" + assert Nothing is Nothing # noqa: Z312 + assert Nothing == _Nothing() == _Nothing(None) + assert Some(5) == Some(5) + assert Some(None) == Some(None) + + +def test_nonequality(): + """Ensures that containers are not compared to regular values.""" + assert Nothing is not None + assert Nothing != None # noqa: E711 + assert _Nothing(None) is not None + assert _Nothing(None) != None # noqa: E711 + assert Some(5) != 5 + assert Some(3) is not Some(3) + assert Some(None) != Nothing + + +def test_is_compare(): + """Ensures that `is` operator works correctly.""" + some_container = Some(1) + + assert Nothing.bind(lambda state: state) is Nothing + assert some_container.rescue(lambda: Some('fix')) is some_container + assert some_container is not Some(1) + + +def test_immutability_failure(): + """Ensures that Failure container is immutable.""" + with pytest.raises(ImmutableStateError): + Nothing._inner_state = 1 # noqa: Z441 + + with pytest.raises(ImmutableStateError): + Nothing.missing = 2 + + with pytest.raises(ImmutableStateError): + del Nothing._inner_state # type: ignore # noqa: Z420, Z441 + + with pytest.raises(AttributeError): + Nothing.missing # type: ignore # noqa: Z444 + + +def test_immutability_success(): + """Ensures that Success container is immutable.""" + with pytest.raises(ImmutableStateError): + Some(0)._inner_state = 1 # noqa: Z441 + + with pytest.raises(ImmutableStateError): + Some(1).missing = 2 + + with pytest.raises(ImmutableStateError): + del Some(0)._inner_state # type: ignore # noqa: Z420, Z441 + + with pytest.raises(AttributeError): + Some(1).missing # type: ignore # noqa: Z444 diff --git a/tests/test_maybe/test_maybe_failure.py b/tests/test_maybe/test_maybe_failure.py new file mode 100755 index 000000000..64a71683e --- /dev/null +++ b/tests/test_maybe/test_maybe_failure.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import pytest + +from returns.maybe import Nothing, Some +from returns.primitives.exceptions import UnwrapFailedError + + +def test_unwrap_success(): + """Ensures that unwrap works for Some container.""" + with pytest.raises(UnwrapFailedError): + assert Some(1).failure() # type: ignore + + +def test_unwrap_failure(): + """Ensures that unwrap works for Nothing container.""" + assert Nothing.failure() is None # type: ignore diff --git a/tests/test_maybe/test_maybe_functions/test_maybe_decorator.py b/tests/test_maybe/test_maybe_functions/test_maybe_decorator.py new file mode 100644 index 000000000..00a114a5f --- /dev/null +++ b/tests/test_maybe/test_maybe_functions/test_maybe_decorator.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from typing import Dict, Optional + +import pytest + +from returns.maybe import Nothing, Some, maybe + + +@maybe +def _function(hashmap: Dict[str, str], key: str) -> Optional[str]: + return hashmap.get(key, None) + + +@maybe +async def _coroutine(hashmap: Dict[str, str], key: str) -> Optional[str]: + return hashmap.get(key, None) + + +def test_maybe_some(): + """Ensures that maybe decorator works correctly for some case.""" + assert _function({'a': 'b'}, 'a') == Some('b') + + +def test_maybe_nothing(): + """Ensures that maybe decorator works correctly for nothing case.""" + assert _function({'a': 'b'}, 'c') == Nothing + + +@pytest.mark.asyncio +async def test_async_maybe_some(): + """Ensures that maybe decorator works correctly for some case.""" + container = await _coroutine({'a': 'b'}, 'a') + assert container == Some('b') + + +@pytest.mark.asyncio +async def test_async_maybe_nothing(): + """Ensures that maybe decorator works correctly for nothing case.""" + container = await _coroutine({'a': 'b'}, 'c') + assert container == Nothing diff --git a/tests/test_maybe/test_maybe_map.py b/tests/test_maybe/test_maybe_map.py new file mode 100755 index 000000000..e718a3cbb --- /dev/null +++ b/tests/test_maybe/test_maybe_map.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from returns.maybe import Nothing, Some + + +def test_map_some(): + """Ensures that map works for Some container.""" + assert Some(5).map(str) == Some('5') + assert Some(5).map(lambda num: None) == Nothing + + +def test_map_nothing(): + """Ensures that map works for Nothing container.""" + assert Nothing.map(str) == Nothing + + +def test_fix_some(): + """Ensures that fix works for Some container.""" + assert Some(5).fix(str) == Some(5) + + +def test_fix_nothing(): + """Ensures that fix works for Nothing container.""" + assert Nothing.fix(lambda: 2) == Some(2) + assert Nothing.fix(lambda: None) == Nothing diff --git a/tests/test_maybe/test_maybe_new.py b/tests/test_maybe/test_maybe_new.py new file mode 100755 index 000000000..87a381476 --- /dev/null +++ b/tests/test_maybe/test_maybe_new.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from returns.maybe import Maybe, Nothing, Some + + +def test_maybe_new_some(): + """Ensures that `new` works for Some container.""" + assert Maybe.new(5) == Some(5) + + +def test_maybe_new_nothing(): + """Ensures that `new` works for Nothing container.""" + assert Maybe.new(None) == Nothing diff --git a/tests/test_maybe/test_maybe_unwrap.py b/tests/test_maybe/test_maybe_unwrap.py new file mode 100755 index 000000000..82f6a0cbb --- /dev/null +++ b/tests/test_maybe/test_maybe_unwrap.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import pytest + +from returns.maybe import Nothing, Some +from returns.primitives.exceptions import UnwrapFailedError + + +def test_unwrap_success(): + """Ensures that unwrap works for Some container.""" + assert Some(5).unwrap() == 5 + + +def test_unwrap_failure(): + """Ensures that unwrap works for Nothing container.""" + with pytest.raises(UnwrapFailedError): + assert Nothing.unwrap() diff --git a/tests/test_maybe/test_maybe_value_or.py b/tests/test_maybe/test_maybe_value_or.py new file mode 100755 index 000000000..34693ff6c --- /dev/null +++ b/tests/test_maybe/test_maybe_value_or.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from returns.maybe import Nothing, Some + + +def test_some_value(): + """Ensures that value is fetch correctly from the Some.""" + assert Some(5).value_or(None) == 5 + + +def test_nothing_value(): + """Ensures that value is fetch correctly from the Nothing.""" + assert Nothing.value_or(default_value=1) == 1 diff --git a/tests/test_pipeline/test_is_successful.py b/tests/test_pipeline/test_is_successful.py new file mode 100644 index 000000000..9f5380b11 --- /dev/null +++ b/tests/test_pipeline/test_is_successful.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +import pytest + +from returns.maybe import Nothing, Some +from returns.pipeline import is_successful +from returns.result import Failure, Success + + +@pytest.mark.parametrize('container, correct_result', [ + (Success('a'), True), + (Failure('a'), False), + (Some('a'), True), + (Nothing, False), +]) +def test_is_successful(container, correct_result): + """Ensures that successful state works correctly.""" + assert is_successful(container) is correct_result diff --git a/tests/test_result/test_helper_functions/test_pipeline.py b/tests/test_pipeline/test_pipeline.py similarity index 75% rename from tests/test_result/test_helper_functions/test_pipeline.py rename to tests/test_pipeline/test_pipeline.py index a71e60152..342a0aa90 100644 --- a/tests/test_result/test_helper_functions/test_pipeline.py +++ b/tests/test_pipeline/test_pipeline.py @@ -4,14 +4,21 @@ import pytest -from returns.result import ( - Failure, - Result, - Success, - _Failure, - _Success, - pipeline, -) +from returns.maybe import Maybe, Nothing, Some +from returns.pipeline import pipeline +from returns.result import Failure, Result, Success, _Failure, _Success + + +@pipeline +def _maybe_pipeline(number: int) -> Maybe[int]: + first: int = Some(number).unwrap() if number else Nothing.unwrap() + return Some(first + number) + + +@pipeline +def _async_maybe_pipeline(number: int) -> Maybe[int]: + first: int = Some(number).unwrap() if number else Nothing.unwrap() + return Some(first + number) @pipeline @@ -38,6 +45,10 @@ def _transformation(number: int) -> Result[int, Any]: return Success(-number) +def test_maybe_pipeline_some(): + """Ensures that pipeline works well for Some.""" + + def test_pipeline_success(): """Ensures that pipeline works well for Success.""" assert _example1(5) == Success(6) diff --git a/tests/test_result/test_helper_functions/test_is_successful.py b/tests/test_result/test_helper_functions/test_is_successful.py deleted file mode 100644 index 0c945b94b..000000000 --- a/tests/test_result/test_helper_functions/test_is_successful.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest - -from returns.result import Failure, Success, is_successful - - -@pytest.mark.parametrize('container, correct_result', [ - (Success, True), - (Failure, False), -]) -def test_is_successful(container, correct_result): - """Ensures that successful state works correctly.""" - assert is_successful(container('some value')) is correct_result diff --git a/tests/test_result/test_result_bind.py b/tests/test_result/test_result_bind.py index 848787cb0..37e73d5ff 100644 --- a/tests/test_result/test_result_bind.py +++ b/tests/test_result/test_result_bind.py @@ -26,7 +26,7 @@ def factory(inner_value: int) -> Result[int, str]: def test_left_identity_success(): - """Ensures that Failure identity works for Success container.""" + """Ensures that left identity works for Success container.""" def factory(inner_value: int) -> Result[int, Any]: return Success(inner_value * 2) @@ -38,7 +38,7 @@ def factory(inner_value: int) -> Result[int, Any]: def test_left_identity_failure(): - """Ensures that Failure identity works for Success container.""" + """Ensures that left identity works for Failure container.""" def factory(inner_value: int) -> Result[Any, TypeError]: return Failure(TypeError()) @@ -61,7 +61,7 @@ def factory(inner_value: int) -> Result[int, Any]: def test_rescue_failure(): - """Ensures that rescue works for Success container.""" + """Ensures that rescue works for Failure container.""" def factory(inner_value: int) -> Result[Any, float]: return Failure(float(inner_value + 1)) diff --git a/tests/test_result/test_helper_functions/test_safe.py b/tests/test_result/test_result_functions/test_safe.py similarity index 100% rename from tests/test_result/test_helper_functions/test_safe.py rename to tests/test_result/test_result_functions/test_safe.py diff --git a/typesafety/test_maybe_types/maybe_decorator.test b/typesafety/test_maybe_types/maybe_decorator.test new file mode 100644 index 000000000..455ea6de6 --- /dev/null +++ b/typesafety/test_maybe_types/maybe_decorator.test @@ -0,0 +1,94 @@ +[CASE maybe_decorator_no_params] +[disable_cache] +from returns.maybe import maybe + +@maybe +def test() -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def () -> returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_decorator_no_params_optional] +[disable_cache] +from typing import Optional +from returns.maybe import maybe + +@maybe +def test() -> Optional[int]: + return 1 + +reveal_type(test) # E: Revealed type is 'def () -> returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_composition_no_params] +[disable_cache] +from returns.maybe import maybe + +def test() -> int: + return 1 + +reveal_type(maybe(test)) # E: Revealed type is 'def () -> returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_decorator_with_args] +[disable_cache] +from returns.maybe import maybe + +@maybe +def test(first: int, second: str = None, *, kw: bool = True) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_composition_with_args] +[disable_cache] +from returns.maybe import maybe + +def test(first: int, second: str = None, *, kw: bool = True) -> int: + return 1 + +reveal_type(maybe(test)) # E: Revealed type is 'def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_decorator_with_args_kwargs] +[disable_cache] +from returns.maybe import maybe + +@maybe +def test(*args, **kwargs) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (*args: Any, **kwargs: Any) -> returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_decorator_with_typed_args_kwargs] +[disable_cache] +from returns.maybe import maybe + +@maybe +def test(*args: int, **kwargs: str) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (*args: builtins.int, **kwargs: builtins.str) -> returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_decorator_with_optional] +[disable_cache] +from typing import Optional +from returns.maybe import maybe + +@maybe +def test() -> Optional[int]: + return 1 + +reveal_type(test) # E: Revealed type is 'def () -> returns.maybe.Maybe[builtins.int*]' +[/CASE] diff --git a/typesafety/test_maybe_types/maybe_type.test b/typesafety/test_maybe_types/maybe_type.test new file mode 100644 index 000000000..91fe2cb54 --- /dev/null +++ b/typesafety/test_maybe_types/maybe_type.test @@ -0,0 +1,93 @@ +[CASE maybe_type_regular] +[disable_cache] +from returns.maybe import Maybe + +value: int +reveal_type(Maybe.new(value)) # E: Revealed type is 'returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_type_optional] +[disable_cache] +from typing import Optional +from returns.maybe import Maybe + +value: Optional[int] +reveal_type(Maybe.new(value)) # E: Revealed type is 'returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_map_regular] +[disable_cache] +from returns.maybe import Maybe + +result = Maybe.new(1).map(lambda i: i / i) +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.float*]' +[/CASE] + + +[CASE maybe_map_optional] +[disable_cache] +from returns.maybe import Maybe + +result = Maybe.new({'a': 'b'}).map(lambda d: d.get('a', None)) +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.str*]' +[/CASE] + + +[CASE maybe_bind_regular] +[disable_cache] +from returns.maybe import Maybe + +result = Maybe.new(1).bind(lambda d: Maybe.new(str(d))) +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.str*]' +[/CASE] + + +[CASE maybe_bind_optional] +[disable_cache] +from returns.maybe import Maybe + +result = Maybe.new({'a': 'b'}).bind(lambda d: Maybe.new(d.get('a', None))) +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.str*]' +[/CASE] + + +[CASE maybe_fix_regular] +[disable_cache] +from returns.maybe import Maybe + +result = Maybe.new({'a': 'b'}).fix(lambda: 'b') +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.str*]' +[/CASE] + + +[CASE maybe_fix_optional] +[disable_cache] +from typing import Optional +from returns.maybe import Maybe + +value: Optional[int] +result = Maybe.new({'a': 'b'}).fix(lambda: value) +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.int*]' +[/CASE] + + +[CASE maybe_rescue_regular] +[disable_cache] +from returns.maybe import Maybe + +result = Maybe.new({'a': 'b'}).rescue(lambda: Maybe.new('b')) +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.str*]' +[/CASE] + + +[CASE maybe_rescue_optional] +[disable_cache] +from typing import Optional +from returns.maybe import Maybe + +value: Optional[int] +result = Maybe.new({'a': 'b'}).rescue(lambda: Maybe.new(value)) +reveal_type(result) # E: Revealed type is 'returns.maybe.Maybe[builtins.int*]' +[/CASE]