diff --git a/CHANGELOG.md b/CHANGELOG.md index 134700cd4..374bc8fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ We follow Semantic Versions since the `0.1.0` release. +## Version 0.4.0 + +### Features + +- Moves all types to `.pyi` files +- Renames all classes according to new naming pattern + + ## Version 0.3.1 ### Bugfixes @@ -27,7 +35,7 @@ The project is renamed to `returns` and moved to `dry-python` org. - Adds `Maybe` monad - Adds immutability and `__slots__` to all monads - Adds methods to work with failures -- Adds `safe` decorator to convert exceptions to `Either` monad +- Adds `safe` decorator to convert exceptions to `Result` monad - Adds `is_successful()` function to detect if your result is a success - Adds `failure()` method to unwrap values from failed monads diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0bfed7c8..516735d4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ you will need to get familiar with these concepts: Here are some practical examples of what we are doing here: -- https://medium.com/@rnesytov/using-either-monad-in-python-b6eac698dff5 +- https://medium.com/@rnesytov/using-Result-monad-in-python-b6eac698dff5 - https://www.morozov.is/2018/09/08/monad-laws-in-ruby.html diff --git a/README.md b/README.md index a57102158..73cbdffe4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![wemake.services](https://img.shields.io/badge/%20-wemake.services-green.svg?label=%20&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D)](https://wemake.services) [![Build Status](https://travis-ci.org/dry-python/returns.svg?branch=master)](https://travis-ci.org/dry-python/returns) [![Coverage Status](https://coveralls.io/repos/github/dry-python/returns/badge.svg?branch=master)](https://coveralls.io/github/dry-python/returns?branch=master) [![Documentation Status](https://readthedocs.org/projects/returns/badge/?version=latest)](https://returns.readthedocs.io/en/latest/?badge=latest) [![Python Version](https://img.shields.io/pypi/pyversions/returns.svg)](https://pypi.org/project/returns/) [![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide) -Make your functions return something meaningful and safe! +Make your functions return something meaningful, typed, and safe! ## Features @@ -21,27 +21,17 @@ Make your functions return something meaningful and safe! pip install returns ``` +## Why? -## What's inside? +TODO: example with `requests` and `json` -We have several the most iconic monads inside: -- [Result, Failure, and Success](https://returns.readthedocs.io/en/latest/pages/either.html) (also known as `Either`, `Left`, and `Right`) -- [Maybe, Some, and Nothing](https://returns.readthedocs.io/en/latest/pages/maybe.html) - -We also care about code readability and developer experience, -so we have included some useful features to make your life easier: - -- [Do notation](https://returns.readthedocs.io/en/latest/pages/do-notation.html) -- [Helper functions](https://returns.readthedocs.io/en/latest/pages/functions.html) - - -## Example +## Pipeline example ```python from returns.do_notation import do_notation -from returns.either import Result, Success, Failure +from returns.result import Result, Success, Failure class CreateAccountAndUser(object): """Creates new Account-User pair.""" @@ -59,12 +49,3 @@ class CreateAccountAndUser(object): ``` We are [covering what's going on in this example](https://returns.readthedocs.io/en/latest/pages/do-notation.html) in the docs. - - -## Inspirations - -This module is heavily based on: - -- [dry-rb/dry-monads](https://github.com/dry-rb/dry-monads) -- [Ø](https://github.com/dbrattli/OSlash) -- [pymonad](https://bitbucket.org/jason_delaat/pymonad) diff --git a/docs/conf.py b/docs/conf.py index 15e3637a7..889da312e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ def _get_project_meta(): pkg_meta = _get_project_meta() project = pkg_meta['name'] -copyright = '2019, wemake.services' # noqa: A001 +copySuccess = '2019, wemake.services' # noqa: A001 author = 'wemake.services' # The short X.Y version diff --git a/docs/index.rst b/docs/index.rst index b7fe728b5..e724bf235 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,14 @@ .. mdinclude:: ../README.md +Inspirations +------------ + +This module is heavily based on: + +- `dry-rb/dry-monads `_ +- `Ø `_ +- `pymonad `_ + Contents -------- @@ -9,7 +18,7 @@ Contents pages/monad.rst pages/maybe.rst - pages/either.rst + pages/Result.rst pages/do-notation.rst pages/functions.rst diff --git a/docs/pages/do-notation.rst b/docs/pages/do-notation.rst index b918520a1..71f37f684 100644 --- a/docs/pages/do-notation.rst +++ b/docs/pages/do-notation.rst @@ -26,7 +26,7 @@ Here's the code to illustrate the task. .. code:: python from returns.do_notation import do_notation - from returns.either import Result, Success, Failure + from returns.result import Result, Success, Failure class CreateAccountAndUser(object): @@ -138,21 +138,21 @@ Limitations ----------- There's one limitation in typing -that we are facing right now +that we are facing Success now due to `mypy issue `_: .. code:: python from returns.do_notation import do_notation - from returns.either import Success + from returns.result import Success @do_notation def function(param: int) -> Success[int]: return Success(param) reveal_type(function) - # Actual => def (*Any, **Any) -> returns.either.Right*[builtins.int] - # Expected => def (int) -> returns.either.Right*[builtins.int] + # Actual => def (*Any, **Any) -> returns.result.Success*[builtins.int] + # Expected => def (int) -> returns.result.Success*[builtins.int] This effect can be reduced with the help of `Design by Contract `_ with these implementations: diff --git a/docs/pages/either.rst b/docs/pages/either.rst index 3dc7a379a..d6c9dc712 100644 --- a/docs/pages/either.rst +++ b/docs/pages/either.rst @@ -1,4 +1,4 @@ -Either +Result ====== Also known as ``Result``. @@ -13,9 +13,9 @@ and ``Failure`` indicates that something has failed. .. code:: python - from returns.either import Result, Success, Failure + from returns.result import Result, Success, Failure - def find_user(user_id: int) -> Either['User', str]: + def find_user(user_id: int) -> Result['User', str]: user = User.objects.filter(id=user_id) if user.exists(): return Success(user[0]) @@ -36,7 +36,7 @@ and other ``None`` exception-friends. API Reference ------------- -.. autoclasstree:: returns.either +.. autoclasstree:: returns.result -.. automodule:: returns.either +.. automodule:: returns.result :members: diff --git a/docs/pages/functions.rst b/docs/pages/functions.rst index e9b9cc0e7..5ed43d697 100644 --- a/docs/pages/functions.rst +++ b/docs/pages/functions.rst @@ -9,12 +9,12 @@ is_successful :func:`is_succesful ` is used to tell whether or not your monad is a success. We treat only treat monads that does not throw as a successful ones, -basically: :class:`Right ` +basically: :class:`Success ` and :class:`Some `. .. code:: python - from returns.either import Success, Failure + from returns.result import Success, Failure from returns.functions import is_successful from returns.maybe import Some, Nothing @@ -29,7 +29,7 @@ safe :func:`safe ` is used to convert regular functions that can throw exceptions to functions -that return :class:`Either ` monad. +that return :class:`Result ` monad. .. code:: python diff --git a/docs/pages/monad.rst b/docs/pages/monad.rst index b95056fd0..470f9518c 100644 --- a/docs/pages/monad.rst +++ b/docs/pages/monad.rst @@ -47,13 +47,13 @@ is used to literally bind two different monads together. .. code:: python - from returns.either import Either, Success + from returns.result import Result, Success - def make_http_call(user_id: int) -> Either[int, str]: + def make_http_call(user_id: int) -> Result[int, str]: ... result = Success(1).bind(make_http_call) - # => Will be equal to either Success[int] or Failure[str] + # => Will be equal to Result Success[int] or Failure[str] So, the rule is: whenever you have some impure functions, it should return a monad instead. @@ -63,7 +63,7 @@ to use monads with pure functions. .. code:: python - from returns.either import Success + from returns.result import Success def double(state: int) -> int: return state * 2 @@ -87,7 +87,7 @@ during the pipeline execution: .. code:: python - from returns.either import Failure + from returns.result import Failure def double(state: int) -> float: return state * 2.0 @@ -96,13 +96,13 @@ during the pipeline execution: # => Will be equal to Success(2.0) ``ebind`` can return any monad you want. -It can also fix your flow and get on the right track again: +It can also fix your flow and get on the Success track again: .. code:: python - from returns.either import Either, Failure, Success + from returns.result import Result, Failure, Success - def fix(state: Exception) -> Either[int, Exception]: + def fix(state: Exception) -> Result[int, Exception]: if isinstance(state, ZeroDivisionError): return Success(0) return Failure(state) @@ -123,7 +123,7 @@ inner state of monads into a regular types: .. code:: python - from returns.either import Failure, Success + from returns.result import Failure, Success Success(1).value_or(None) # => 1 diff --git a/pyproject.toml b/pyproject.toml index b259d4e41..728d9f32a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "returns" version = "0.3.1" -description = "Make your functions return something meaningful and safe!" +description = "Make your functions return something meaningful, typed, and safe!" license = "MIT" authors = [ diff --git a/returns/do_notation.pyi b/returns/do_notation.pyi index 905354ced..37d60ac72 100644 --- a/returns/do_notation.pyi +++ b/returns/do_notation.pyi @@ -4,10 +4,10 @@ from typing import Callable from returns.primitives.types import MonadType - # Typing decorators is not an easy task, see: # https://github.com/python/mypy/issues/3157 + def do_notation( function: Callable[..., MonadType], ) -> Callable[..., MonadType]: diff --git a/returns/either.pyi b/returns/either.pyi deleted file mode 100644 index 21a90cc67..000000000 --- a/returns/either.pyi +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- - -from abc import ABCMeta, abstractmethod -from typing import Any, Callable, Generic, NoReturn, TypeVar, Union - -from typing_extensions import final - -from returns.primitives.monad import Monad, NewValueType, ValueType - -# There's a wierd bug with mypy when we remove this line and use import: -_MonadType = TypeVar('_MonadType', bound=Union['Monad', 'Either']) - -# Regular type var, works correctly: -_ErrorType = TypeVar('_ErrorType') - - -# That's the ugliest part. -# We need to express `Either` with two type parameters and -# Left and Right with just one parameter. -# And that's how we do it. Any other and more cleaner ways are appreciated. -class Either(Generic[ValueType, _ErrorType], metaclass=ABCMeta): - _inner_value: Union[ValueType, _ErrorType] - - @abstractmethod - def unwrap(self) -> ValueType: # pragma: no cover - ... - - -@final -class Left(Either[Any, _ErrorType], Monad[_ErrorType]): - _inner_value: _ErrorType - - def __init__(self, inner_value: _ErrorType) -> None: - ... - - def fmap(self, function) -> 'Left[_ErrorType]': - ... - - def bind(self, function) -> 'Left[_ErrorType]': - ... - - def efmap( - self, - function: Callable[[_ErrorType], NewValueType], - ) -> 'Right[NewValueType]': - ... - - def ebind(self, function: Callable[[_ErrorType], _MonadType]) -> _MonadType: - ... - - def value_or(self, default_value: NewValueType) -> NewValueType: - ... - - def unwrap(self) -> NoReturn: - ... - - def failure(self) -> _ErrorType: - ... - - -@final -class Right(Either[ValueType, Any], Monad[ValueType]): - _inner_value: ValueType - - def __init__(self, inner_value: ValueType) -> None: - ... - - def fmap( - self, - function: Callable[[ValueType], NewValueType], - ) -> 'Right[NewValueType]': - ... - - def bind( - self, - function: Callable[[ValueType], _MonadType], - ) -> _MonadType: - ... - - def efmap(self, function) -> 'Right[ValueType]': - ... - - def ebind(self, function) -> 'Right[ValueType]': - ... - - def value_or(self, default_value: NewValueType) -> ValueType: - ... - - def unwrap(self) -> ValueType: - ... - - def failure(self) -> NoReturn: - ... - - -# Useful aliases for end users: - -Result = Either -Success = Right -Failure = Left diff --git a/returns/functions.py b/returns/functions.py index 4d989921e..81ab6ed51 100644 --- a/returns/functions.py +++ b/returns/functions.py @@ -2,9 +2,8 @@ from functools import wraps -from returns.either import Either, Failure, Success from returns.primitives.exceptions import UnwrapFailedError -from returns.primitives.types import MonadType +from returns.result import Failure, Success def is_successful(monad): @@ -19,7 +18,7 @@ def is_successful(monad): def safe(function): """ - Decorator to covert exception throwing function to 'Either' monad. + Decorator to covert exception throwing function to 'Result' monad. Show be used with care, since it only catches 'Exception' subclasses. It does not catch 'BaseException' subclasses. diff --git a/returns/functions.pyi b/returns/functions.pyi index bc91b263e..ff54902c1 100644 --- a/returns/functions.pyi +++ b/returns/functions.pyi @@ -2,8 +2,8 @@ from typing import Callable, TypeVar -from returns.either import Either from returns.primitives.types import MonadType +from returns.result import Result _ReturnType = TypeVar('_ReturnType') @@ -14,5 +14,5 @@ def is_successful(monad: MonadType) -> bool: def safe( function: Callable[..., _ReturnType], -) -> Callable[..., Either[_ReturnType, Exception]]: +) -> Callable[..., Result[_ReturnType, Exception]]: ... diff --git a/returns/maybe.py b/returns/maybe.py index 6bfb87442..dd84416f9 100644 --- a/returns/maybe.py +++ b/returns/maybe.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- from abc import ABCMeta -from typing import Callable, NoReturn, Union, overload +from typing import Generic, TypeVar -from typing_extensions import Literal, final +from typing_extensions import Literal from returns.primitives.exceptions import UnwrapFailedError -from returns.primitives.monad import Monad, NewValueType, ValueType -from returns.primitives.types import MonadType +from returns.primitives.monad import Monad +_ValueType = TypeVar('_ValueType') -class Maybe(Monad, metaclass=ABCMeta): + +class Maybe(Generic[_ValueType], Monad[_ValueType], metaclass=ABCMeta): """ Represents a result of a series of commutation that can return ``None``. @@ -27,10 +28,10 @@ def new(cls, inner_value): return Some(inner_value) -class Nothing(Maybe): +class Nothing(Maybe[Literal[None]]): """Represents an empty state.""" - def __init__(self, inner_value = None): + def __init__(self, inner_value=None): """ Wraps the given value in the Container. @@ -62,7 +63,7 @@ def ebind(self, function): Applies 'function' to the result of a previous calculation. 'function' should accept a single "normal" (non-monad) argument - and return either a 'Nothing' or 'Some' type object. + and return Result a 'Nothing' or 'Some' type object. """ return function(self._inner_value) @@ -79,7 +80,7 @@ def failure(self): return self._inner_value -class Some(Maybe): +class Some(Maybe[_ValueType]): """ Represents a calculation which has succeeded and contains the result. @@ -102,7 +103,7 @@ def bind(self, function): Applies 'function' to the result of a previous calculation. 'function' should accept a single "normal" (non-monad) argument - and return either a 'Nothing' or 'Some' type object. + and return Result a 'Nothing' or 'Some' type object. """ return function(self._inner_value) diff --git a/returns/maybe.pyi b/returns/maybe.pyi index 798be2342..5e66405d4 100644 --- a/returns/maybe.pyi +++ b/returns/maybe.pyi @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- from abc import ABCMeta -from typing import Callable, NoReturn, overload +from typing import Any, Callable, NoReturn, TypeVar, Union, overload from typing_extensions import Literal, final -from returns.primitives.monad import Monad, NewValueType, ValueType -from returns.primitives.types import MonadType +from returns.primitives.monad import Monad +_MonadType = TypeVar('_MonadType', bound='Monad') +_ValueType = TypeVar('_ValueType') +_NewValueType = TypeVar('_NewValueType') -class Maybe(Monad[ValueType], metaclass=ABCMeta): + +class Maybe(Monad[_ValueType], metaclass=ABCMeta): @overload @classmethod def new(cls, inner_value: Literal[None]) -> 'Nothing': # type: ignore @@ -17,74 +20,104 @@ class Maybe(Monad[ValueType], metaclass=ABCMeta): @overload # noqa: F811 @classmethod - def new(cls, inner_value: ValueType) -> 'Some[ValueType]': + def new(cls, inner_value: _ValueType) -> 'Some[_ValueType]': + ... + + def fmap( + self, + function: Callable[[_ValueType], _NewValueType], + ) -> _MonadType: + ... + + def bind( + self, + function: Callable[[_ValueType], _MonadType], + ) -> _MonadType: + ... + + def efmap(self, function) -> 'Some[_NewValueType]': + ... + + def ebind(self, function) -> _MonadType: + ... + + def value_or( + self, + default_value: _NewValueType, + ) -> Union[_ValueType, _NewValueType]: + ... + + def unwrap(self) -> Union[NoReturn, _ValueType]: + ... + + def failure(self) -> Union[NoReturn, Literal[None]]: ... @final -class Nothing(Maybe[Literal[None]]): +class Nothing(Maybe[Any]): _inner_value: Literal[None] def __init__(self, inner_value: Literal[None] = ...) -> None: ... - def fmap(self, function) -> 'Nothing': + def fmap(self, function) -> 'Nothing': # type: ignore ... - def bind(self, function) -> 'Nothing': + def bind(self, function) -> 'Nothing': # type: ignore ... def efmap( self, - function: Callable[[Literal[None]], 'NewValueType'], - ) -> 'Some[NewValueType]': + function: Callable[[Literal[None]], '_NewValueType'], + ) -> 'Some[_NewValueType]': ... def ebind( self, - function: Callable[[Literal[None]], MonadType], - ) -> MonadType: + function: Callable[[Literal[None]], _MonadType], + ) -> _MonadType: ... - def value_or(self, default_value: NewValueType) -> NewValueType: + def value_or(self, default_value: _NewValueType) -> _NewValueType: ... def unwrap(self) -> NoReturn: ... - def failure(self) -> None: + def failure(self) -> Literal[None]: ... @final -class Some(Maybe[ValueType]): - _inner_value: ValueType +class Some(Maybe[_ValueType]): + _inner_value: _ValueType - def __init__(self, inner_value: ValueType) -> None: + def __init__(self, inner_value: _ValueType) -> None: ... - def fmap( + def fmap( # type: ignore self, - function: Callable[[ValueType], NewValueType], - ) -> 'Some[NewValueType]': + function: Callable[[_ValueType], _NewValueType], + ) -> 'Some[_NewValueType]': ... def bind( self, - function: Callable[[ValueType], MonadType], - ) -> MonadType: + function: Callable[[_ValueType], _MonadType], + ) -> _MonadType: ... - def efmap(self, function) -> 'Some[ValueType]': + def efmap(self, function) -> 'Some[_ValueType]': # type: ignore ... - def ebind(self, function) -> 'Some[ValueType]': + def ebind(self, function) -> 'Some[_ValueType]': # type: ignore ... - def value_or(self, default_value: NewValueType) -> ValueType: + def value_or(self, default_value: _NewValueType) -> _ValueType: ... - def unwrap(self) -> ValueType: + def unwrap(self) -> _ValueType: ... def failure(self) -> NoReturn: diff --git a/returns/primitives/monad.py b/returns/primitives/monad.py index fbcc70a1f..0b55f4d78 100644 --- a/returns/primitives/monad.py +++ b/returns/primitives/monad.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- from abc import ABCMeta, abstractmethod +from typing import Generic, TypeVar from returns.primitives.exceptions import ImmutableStateError +_ValueType = TypeVar('_ValueType') -class _BaseMonad(object, metaclass=ABCMeta): + +class _BaseMonad(Generic[_ValueType], metaclass=ABCMeta): """Utility class to provide all needed magic methods to the contest.""" __slots__ = ('_inner_value',) @@ -42,7 +45,7 @@ def __eq__(self, other): return self._inner_value == other._inner_value # noqa: Z441 -class Monad(_BaseMonad): +class Monad(_BaseMonad[_ValueType]): """ Represents a "context" in which calculations can be executed. diff --git a/returns/primitives/monad.pyi b/returns/primitives/monad.pyi index d04650cc8..97bc848b2 100644 --- a/returns/primitives/monad.pyi +++ b/returns/primitives/monad.pyi @@ -4,18 +4,18 @@ from abc import ABCMeta, abstractmethod from typing import Any, Generic, NoReturn, TypeVar # These type variables are widely used in our source code. -ValueType = TypeVar('ValueType') # noqa: Y001 -NewValueType = TypeVar('NewValueType') # noqa: Y001 +_ValueType = TypeVar('_ValueType') +_NewValueType = TypeVar('_NewValueType') -class _BaseMonad(Generic[ValueType], metaclass=ABCMeta): +class _BaseMonad(Generic[_ValueType], metaclass=ABCMeta): __slots__ = ('_inner_value',) _inner_value: Any - def __setattr__(self, attr_name, attr_value) -> NoReturn: + def __setattr__(self, attr_name: str, attr_value) -> NoReturn: ... - def __delattr__(self, attr_name) -> NoReturn: # noqa: Z434 + def __delattr__(self, attr_name: str) -> NoReturn: # noqa: Z434 ... def __str__(self) -> str: @@ -25,7 +25,7 @@ class _BaseMonad(Generic[ValueType], metaclass=ABCMeta): ... -class Monad(_BaseMonad[ValueType]): +class Monad(_BaseMonad[_ValueType]): @abstractmethod def fmap(self, function): ... @@ -47,7 +47,7 @@ class Monad(_BaseMonad[ValueType]): ... @abstractmethod - def unwrap(self) -> ValueType: + def unwrap(self) -> _ValueType: ... @abstractmethod diff --git a/returns/primitives/types.py b/returns/primitives/types.py index eee892383..2bdd82f2b 100644 --- a/returns/primitives/types.py +++ b/returns/primitives/types.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, TypeVar, Union if TYPE_CHECKING: # pragma: no cover - from returns.either import Either # noqa: Z435, F401 + from returns.result import Result # noqa: Z435, F401 from returns.primitives.monad import Monad # noqa: Z435, F401 # We need to have this ugly type because there is no other way around it: -MonadType = TypeVar('MonadType', bound=Union['Monad', 'Either']) +MonadType = TypeVar('MonadType', bound=Union['Monad', 'Result']) diff --git a/returns/either.py b/returns/result.py similarity index 63% rename from returns/either.py rename to returns/result.py index 434899baf..6ba53b564 100644 --- a/returns/either.py +++ b/returns/result.py @@ -1,16 +1,20 @@ # -*- coding: utf-8 -*- from abc import ABCMeta +from typing import Any, Generic, TypeVar from returns.primitives.exceptions import UnwrapFailedError from returns.primitives.monad import Monad +_ValueType = TypeVar('_ValueType') +_ErrorType = TypeVar('_ErrorType') -class Either(Monad, metaclass=ABCMeta): - """Base class for Left and Right.""" +class Result(Generic[_ValueType, _ErrorType], Monad, metaclass=ABCMeta): + """Base class for Failure and Success.""" -class Left(Either): + +class Failure(Result[Any, _ErrorType]): """ Represents a calculation which has failed. @@ -19,35 +23,35 @@ class Left(Either): """ def fmap(self, function): - """Returns the 'Left' instance that was used to call the method.""" + """Returns the 'Failure' instance that was used to call the method.""" return self def bind(self, function): - """Returns the 'Left' instance that was used to call the method.""" + """Returns the 'Failure' instance that was used to call the method.""" return self def efmap(self, function): """ Applies function to the inner value. - Applies 'function' to the contents of the 'Right' instance - and returns a new 'Right' object containing the result. + Applies 'function' to the contents of the 'Success' instance + and returns a new 'Success' object containing the result. 'function' should accept a single "normal" (non-monad) argument and return a non-monad result. """ - return Right(function(self._inner_value)) + return Success(function(self._inner_value)) def ebind(self, function): """ Applies 'function' to the result of a previous calculation. 'function' should accept a single "normal" (non-monad) argument - and return either a 'Left' or 'Right' type object. + and return Result a 'Failure' or 'Success' type object. """ return function(self._inner_value) def value_or(self, default_value): - """Returns the value if we deal with 'Right' or default if 'Left'.""" + """Returns the value if we deal with 'Success' or default otherwise.""" return default_value def unwrap(self): @@ -59,7 +63,7 @@ def failure(self): return self._inner_value -class Right(Either): +class Success(Result[_ValueType, Any]): """ Represents a calculation which has succeeded and contains the result. @@ -70,32 +74,32 @@ def fmap(self, function): """ Applies function to the inner value. - Applies 'function' to the contents of the 'Right' instance - and returns a new 'Right' object containing the result. + Applies 'function' to the contents of the 'Success' instance + and returns a new 'Success' object containing the result. 'function' should accept a single "normal" (non-monad) argument and return a non-monad result. """ - return Right(function(self._inner_value)) + return Success(function(self._inner_value)) def bind(self, function): """ Applies 'function' to the result of a previous calculation. 'function' should accept a single "normal" (non-monad) argument - and return either a 'Left' or 'Right' type object. + and return Result a 'Failure' or 'Success' type object. """ return function(self._inner_value) def efmap(self, function): - """Returns the 'Right' instance that was used to call the method.""" + """Returns the 'Success' instance that was used to call the method.""" return self def ebind(self, function): - """Returns the 'Right' instance that was used to call the method.""" + """Returns the 'Success' instance that was used to call the method.""" return self def value_or(self, default_value): - """Returns the value if we deal with 'Right' or default if 'Left'.""" + """Returns the value if we deal with 'Success' or default otherwise.""" return self._inner_value def unwrap(self): @@ -105,10 +109,3 @@ def unwrap(self): def failure(self): """Raises an exception, since it does not have an error inside.""" raise UnwrapFailedError(self) - - -# Useful aliases for end users: - -Result = Either -Success = Right -Failure = Left diff --git a/returns/result.pyi b/returns/result.pyi new file mode 100644 index 000000000..282e52b48 --- /dev/null +++ b/returns/result.pyi @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +from abc import ABCMeta +from typing import Any, Callable, Generic, NoReturn, TypeVar, Union + +from typing_extensions import final + +from returns.primitives.monad import Monad + +# There's a wierd bug with mypy when we remove this line and use import: +_MonadType = TypeVar('_MonadType', bound='Monad') + +# Regular type vars, work correctly: +_ValueType = TypeVar('_ValueType') +_NewValueType = TypeVar('_NewValueType') +_ErrorType = TypeVar('_ErrorType') + + +# That's the ugliest part. +# We need to express `Result` with two type parameters and +# Failure and Success with just one parameter. +# And that's how we do it. Any other and more cleaner ways are appreciated. +class Result(Generic[_ValueType, _ErrorType], Monad, metaclass=ABCMeta): + _inner_value: Union[_ValueType, _ErrorType] + + def fmap( + self, + function: Callable[[_ValueType], _NewValueType], + ) -> _MonadType: + ... + + def bind( + self, + function: Callable[[_ValueType], _MonadType], + ) -> _MonadType: + ... + + def efmap( + self, + function: Callable[[_ErrorType], _NewValueType], + ) -> 'Success[_NewValueType]': + ... + + def ebind( + self, + function: Callable[[_ErrorType], _MonadType], + ) -> _MonadType: + ... + + def value_or( + self, + default_value: _NewValueType, + ) -> Union[_NewValueType, _ValueType]: + ... + + def unwrap(self) -> Union[NoReturn, _ValueType]: + ... + + def failure(self) -> Union[NoReturn, _ErrorType]: + ... + + +@final +class Failure(Result[Any, _ErrorType], Monad[_ErrorType]): + _inner_value: _ErrorType + + def __init__(self, inner_value: _ErrorType) -> None: + ... + + def fmap(self, function) -> 'Failure[_ErrorType]': # type: ignore + ... + + def bind(self, function) -> 'Failure[_ErrorType]': # type: ignore + ... + + def efmap( + self, + function: Callable[[_ErrorType], _NewValueType], + ) -> 'Success[_NewValueType]': + ... + + def ebind( + self, function: Callable[[_ErrorType], _MonadType], + ) -> _MonadType: + ... + + def value_or(self, default_value: _NewValueType) -> _NewValueType: + ... + + def unwrap(self) -> NoReturn: + ... + + def failure(self) -> _ErrorType: + ... + + +@final +class Success(Result[_ValueType, Any], Monad[_ValueType]): + _inner_value: _ValueType + + def __init__(self, inner_value: _ValueType) -> None: + ... + + def fmap( # type: ignore + self, + function: Callable[[_ValueType], _NewValueType], + ) -> 'Success[_NewValueType]': + ... + + def bind( + self, + function: Callable[[_ValueType], _MonadType], + ) -> _MonadType: + ... + + def efmap(self, function) -> 'Success[_ValueType]': # type: ignore + ... + + def ebind(self, function) -> 'Success[_ValueType]': # type: ignore + ... + + def value_or(self, default_value: _NewValueType) -> _ValueType: + ... + + def unwrap(self) -> _ValueType: + ... + + def failure(self) -> NoReturn: + ... diff --git a/setup.cfg b/setup.cfg index c74a248e2..8aea75ee8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ per-file-ignores = **/*.pyi: D100, D401, X100 **/*.py: D100, D401, X100 # Disable some `.pyi` specific warings: - **/*.pyi: D101, D102, D103, D107, Z444, Z452 + **/*.pyi: D101, D102, D103, D107, Z214, Z444, Z452 # TODO: fix check and remove it from ignores **/*.py: Z454 **/*.pyi: Z454 diff --git a/test.py b/test.py deleted file mode 100644 index 21a0c2427..000000000 --- a/test.py +++ /dev/null @@ -1,14 +0,0 @@ -from returns.functions import is_successful, safe -from returns.either import Right, Left, Either - - -@safe -def test() -> int: - return 1 - -reveal_type(test()) - -def monad_function(value: int) -> Either[int, str]: - if value: - return Right('asd') - return Left(False) diff --git a/tests/test_do_notation/test_do_notation.py b/tests/test_do_notation/test_do_notation.py index 27e6470a1..feb4c0777 100644 --- a/tests/test_do_notation/test_do_notation.py +++ b/tests/test_do_notation/test_do_notation.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from returns.do_notation import do_notation -from returns.either import Either, Failure, Success +from returns.result import Failure, Result, Success @do_notation -def _example1(number: int) -> Either[int, str]: +def _example1(number: int) -> Result[int, str]: first = Success(1).unwrap() second = Success(number).unwrap() if number else Failure('E').unwrap() return Success(first + second) diff --git a/tests/test_either/test_either_aliases.py b/tests/test_either/test_either_aliases.py deleted file mode 100644 index 70067aee2..000000000 --- a/tests/test_either/test_either_aliases.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- - -from returns.either import Either, Failure, Left, Result, Right, Success - - -def test_aliases(): - """Ensures that aliases are correct.""" - assert Right is Success - assert Left is Failure - assert Either is Result diff --git a/tests/test_either/test_either_bind.py b/tests/test_either/test_either_bind.py deleted file mode 100644 index 773bbe621..000000000 --- a/tests/test_either/test_either_bind.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -from returns.either import Left, Right - - -def test_left_identity_success(): - """Ensures that left identity works for Right monad.""" - def factory(inner_value: int) -> Right[int]: - return Right(inner_value * 2) - - input_value = 5 - bound = Right(input_value).bind(factory) - - assert bound == factory(input_value) - assert str(bound) == 'Right: 10' - - -def test_left_identity_failure(): - """Ensures that left identity works for Right monad.""" - def factory(inner_value: int) -> Left[TypeError]: - return Left(TypeError()) - - input_value = 5 - bound = Left(input_value).bind(factory) - - assert bound == Left(input_value) - assert str(bound) == 'Left: 5' - - -def test_ebind_success(): - """Ensures that ebind works for Right monad.""" - def factory(inner_value: int) -> Right[int]: - return Right(inner_value * 2) - - bound = Right(5).ebind(factory) - - assert bound == Right(5) - assert str(bound) == 'Right: 5' - - -def test_ebind_failure(): - """Ensures that ebind works for Right monad.""" - def factory(inner_value: int) -> Left[float]: - return Left(float(inner_value + 1)) - - expected = 6.0 - bound = Left(5).ebind(factory) - - assert bound == Left(expected) - assert str(bound) == 'Left: 6.0' diff --git a/tests/test_either/test_either_failure.py b/tests/test_either/test_either_failure.py deleted file mode 100644 index b69525972..000000000 --- a/tests/test_either/test_either_failure.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest - -from returns.either import Left, Right -from returns.primitives.exceptions import UnwrapFailedError - - -def test_unwrap_success(): - """Ensures that unwrap works for Right monad.""" - with pytest.raises(UnwrapFailedError): - assert Right(5).failure() - - -def test_unwrap_failure(): - """Ensures that unwrap works for Left monad.""" - assert Left(5).failure() == 5 diff --git a/tests/test_either/test_either_fmap.py b/tests/test_either/test_either_fmap.py deleted file mode 100644 index fa389cb6a..000000000 --- a/tests/test_either/test_either_fmap.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- - -from returns.either import Left, Right - - -def test_fmap_success(): - """Ensures that left identity works for Right monad.""" - assert Right(5).fmap(str) == Right('5') - - -def test_fmap_failure(): - """Ensures that left identity works for Right monad.""" - assert Left(5).fmap(str) == Left(5) - - -def test_efmap_success(): - """Ensures that left identity works for Right monad.""" - assert Right(5).efmap(str) == Right(5) - - -def test_efmap_failure(): - """Ensures that left identity works for Right monad.""" - assert Left(5).efmap(str) == Right('5') diff --git a/tests/test_either/test_either_unwrap.py b/tests/test_either/test_either_unwrap.py deleted file mode 100644 index 847533703..000000000 --- a/tests/test_either/test_either_unwrap.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest - -from returns.either import Left, Right -from returns.primitives.exceptions import UnwrapFailedError - - -def test_unwrap_success(): - """Ensures that unwrap works for Right monad.""" - assert Right(5).unwrap() == 5 - - -def test_unwrap_failure(): - """Ensures that unwrap works for Left monad.""" - with pytest.raises(UnwrapFailedError): - assert Left(5).unwrap() diff --git a/tests/test_functions/test_is_successful.py b/tests/test_functions/test_is_successful.py index 388c36944..44ba2dd4b 100644 --- a/tests/test_functions/test_is_successful.py +++ b/tests/test_functions/test_is_successful.py @@ -2,9 +2,9 @@ import pytest -from returns.either import Failure, Success from returns.functions import is_successful from returns.maybe import Nothing, Some +from returns.result import Failure, Success @pytest.mark.parametrize('monad, correct_result', [ diff --git a/tests/test_functions/test_safe.py b/tests/test_functions/test_safe.py index 7312d7b4f..140ec0778 100644 --- a/tests/test_functions/test_safe.py +++ b/tests/test_functions/test_safe.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from returns.either import Failure, Success from returns.functions import safe +from returns.result import Failure, Success @safe diff --git a/tests/test_maybe/test_maybe_bind.py b/tests/test_maybe/test_maybe_bind.py index 273e1fc4b..33df2e8ad 100644 --- a/tests/test_maybe/test_maybe_bind.py +++ b/tests/test_maybe/test_maybe_bind.py @@ -4,7 +4,7 @@ def test_bind_some(): - """Ensures that left identity works for Some monad.""" + """Ensures that Failure identity works for Some monad.""" def factory(inner_value: int) -> Some[int]: return Some(inner_value * 2) @@ -16,7 +16,7 @@ def factory(inner_value: int) -> Some[int]: def test_bind_nothing(): - """Ensures that left identity works for Some monad.""" + """Ensures that Failure identity works for Some monad.""" def factory(inner_value: None) -> Some[int]: return Some(1) @@ -27,7 +27,7 @@ def factory(inner_value: None) -> Some[int]: def test_ebind_some(): - """Ensures that left identity works for Some monad.""" + """Ensures that Failure identity works for Some monad.""" def factory(inner_value: int) -> Some[int]: return Some(inner_value * 2) @@ -38,7 +38,7 @@ def factory(inner_value: int) -> Some[int]: def test_ebind_nothing(): - """Ensures that left identity works for Some monad.""" + """Ensures that Failure identity works for Some monad.""" def factory(inner_value: None) -> Some[int]: return Some(1) diff --git a/tests/test_result/test_result_bind.py b/tests/test_result/test_result_bind.py new file mode 100644 index 000000000..29a80c3db --- /dev/null +++ b/tests/test_result/test_result_bind.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +from returns.result import Failure, Success + + +def test_left_identity_success(): + """Ensures that Failure identity works for Success monad.""" + def factory(inner_value: int) -> Success[int]: + return Success(inner_value * 2) + + input_value = 5 + bound = Success(input_value).bind(factory) + + assert bound == factory(input_value) + assert str(bound) == 'Success: 10' + + +def test_left_identity_failure(): + """Ensures that Failure identity works for Success monad.""" + def factory(inner_value: int) -> Failure[TypeError]: + return Failure(TypeError()) + + input_value = 5 + bound = Failure(input_value).bind(factory) + + assert bound == Failure(input_value) + assert str(bound) == 'Failure: 5' + + +def test_ebind_success(): + """Ensures that ebind works for Success monad.""" + def factory(inner_value: int) -> Success[int]: + return Success(inner_value * 2) + + bound = Success(5).ebind(factory) + + assert bound == Success(5) + assert str(bound) == 'Success: 5' + + +def test_ebind_failure(): + """Ensures that ebind works for Success monad.""" + def factory(inner_value: int) -> Failure[float]: + return Failure(float(inner_value + 1)) + + expected = 6.0 + bound = Failure(5).ebind(factory) + + assert bound == Failure(expected) + assert str(bound) == 'Failure: 6.0' diff --git a/tests/test_either/test_either_equality.py b/tests/test_result/test_result_equality.py similarity index 57% rename from tests/test_either/test_either_equality.py rename to tests/test_result/test_result_equality.py index 04fa7dafb..2b3065287 100644 --- a/tests/test_either/test_either_equality.py +++ b/tests/test_result/test_result_equality.py @@ -2,54 +2,54 @@ import pytest -from returns.either import Left, Right from returns.primitives.exceptions import ImmutableStateError +from returns.result import Failure, Success def test_nonequality(): """Ensures that monads are not compared to regular values.""" input_value = 5 - assert Left(input_value) != input_value - assert Right(input_value) != input_value - assert Left(input_value) != Right(input_value) + assert Failure(input_value) != input_value + assert Success(input_value) != input_value + assert Failure(input_value) != Success(input_value) def test_is_compare(): """Ensures that `is` operator works correctly.""" - left = Left(1) - right = Right(1) + left = Failure(1) + right = Success(1) assert left.bind(lambda state: state) is left assert right.ebind(lambda state: state) is right - assert right is not Right(1) + assert right is not Success(1) def test_immutability_failure(): """Ensures that Failure monad is immutable.""" with pytest.raises(ImmutableStateError): - Left(0)._inner_state = 1 # noqa: Z441 + Failure(0)._inner_state = 1 # noqa: Z441 with pytest.raises(ImmutableStateError): - Left(1).missing = 2 + Failure(1).missing = 2 with pytest.raises(ImmutableStateError): - del Left(0)._inner_state # type: ignore # noqa: Z420, Z441 + del Failure(0)._inner_state # type: ignore # noqa: Z420, Z441 with pytest.raises(AttributeError): - Left(1).missing # type: ignore # noqa: Z444 + Failure(1).missing # type: ignore # noqa: Z444 def test_immutability_success(): """Ensures that Success monad is immutable.""" with pytest.raises(ImmutableStateError): - Right(0)._inner_state = 1 # noqa: Z441 + Success(0)._inner_state = 1 # noqa: Z441 with pytest.raises(ImmutableStateError): - Right(1).missing = 2 + Success(1).missing = 2 with pytest.raises(ImmutableStateError): - del Right(0)._inner_state # type: ignore # noqa: Z420, Z441 + del Success(0)._inner_state # type: ignore # noqa: Z420, Z441 with pytest.raises(AttributeError): - Right(1).missing # type: ignore # noqa: Z444 + Success(1).missing # type: ignore # noqa: Z444 diff --git a/tests/test_result/test_result_failure.py b/tests/test_result/test_result_failure.py new file mode 100644 index 000000000..116bd1898 --- /dev/null +++ b/tests/test_result/test_result_failure.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import pytest + +from returns.primitives.exceptions import UnwrapFailedError +from returns.result import Failure, Success + + +def test_unwrap_success(): + """Ensures that unwrap works for Success monad.""" + with pytest.raises(UnwrapFailedError): + assert Success(5).failure() + + +def test_unwrap_failure(): + """Ensures that unwrap works for Failure monad.""" + assert Failure(5).failure() == 5 diff --git a/tests/test_result/test_result_fmap.py b/tests/test_result/test_result_fmap.py new file mode 100644 index 000000000..e672906ee --- /dev/null +++ b/tests/test_result/test_result_fmap.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from returns.result import Failure, Success + + +def test_fmap_success(): + """Ensures that Failure identity works for Success monad.""" + assert Success(5).fmap(str) == Success('5') + + +def test_fmap_failure(): + """Ensures that Failure identity works for Success monad.""" + assert Failure(5).fmap(str) == Failure(5) + + +def test_efmap_success(): + """Ensures that Failure identity works for Success monad.""" + assert Success(5).efmap(str) == Success(5) + + +def test_efmap_failure(): + """Ensures that Failure identity works for Success monad.""" + assert Failure(5).efmap(str) == Success('5') diff --git a/tests/test_result/test_result_unwrap.py b/tests/test_result/test_result_unwrap.py new file mode 100644 index 000000000..9af0149af --- /dev/null +++ b/tests/test_result/test_result_unwrap.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import pytest + +from returns.primitives.exceptions import UnwrapFailedError +from returns.result import Failure, Success + + +def test_unwrap_success(): + """Ensures that unwrap works for Success monad.""" + assert Success(5).unwrap() == 5 + + +def test_unwrap_failure(): + """Ensures that unwrap works for Failure monad.""" + with pytest.raises(UnwrapFailedError): + assert Failure(5).unwrap() diff --git a/tests/test_either/test_either_value_or.py b/tests/test_result/test_result_value_or.py similarity index 66% rename from tests/test_either/test_either_value_or.py rename to tests/test_result/test_result_value_or.py index d406753d0..02a47b932 100644 --- a/tests/test_either/test_either_value_or.py +++ b/tests/test_result/test_result_value_or.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -from returns.either import Left, Right +from returns.result import Failure, Success def test_success_value(): """Ensures that value is fetch correctly from the Success.""" - bound = Right(5).value_or(None) + bound = Success(5).value_or(None) assert bound == 5 def test_failure_value(): """Ensures that value is fetch correctly from the Failure.""" - bound = Left(1).value_or(default_value=None) + bound = Failure(1).value_or(default_value=None) assert bound is None