diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a53cd77..42c9a10d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ See [0Ver](https://0ver.org/). ## 0.16.0 WIP +## Features + +- Adds `LazyIO` container +- Marks `IO` as `@final` + ### Misc - Makes `_Nothing` a singleton diff --git a/returns/contrib/mypy/_consts.py b/returns/contrib/mypy/_consts.py index 5a2e1b23d..257b533dd 100644 --- a/returns/contrib/mypy/_consts.py +++ b/returns/contrib/mypy/_consts.py @@ -6,8 +6,8 @@ #: Set of full names of our decorators. TYPED_DECORATORS: Final = frozenset(( 'returns.result.safe', - 'returns.io.impure', - 'returns.io.impure_safe', + 'returns.io.io.impure', + 'returns.io.ioresult.impure_safe', 'returns.maybe.maybe', 'returns.future.future', 'returns.future.asyncify', diff --git a/returns/contrib/pytest/plugin.py b/returns/contrib/pytest/plugin.py index 0d178d581..d43aa5b8f 100644 --- a/returns/contrib/pytest/plugin.py +++ b/returns/contrib/pytest/plugin.py @@ -127,7 +127,8 @@ def _trace_function( arg: Any, ) -> None: is_desired_type_call = ( - event == 'call' and frame.f_code is trace_type.__code__ + event == 'call' and + frame.f_code is trace_type.__code__ ) if is_desired_type_call: current_call_stack = inspect.stack() @@ -152,7 +153,7 @@ def containers_to_patch(cls) -> tuple: RequiresContextFutureResult, ) from returns.future import FutureResult - from returns.io import _IOFailure, _IOSuccess + from returns.io.ioresult import _IOFailure, _IOSuccess from returns.result import _Failure, _Success return ( diff --git a/returns/io/__init__.py b/returns/io/__init__.py new file mode 100644 index 000000000..c4459c798 --- /dev/null +++ b/returns/io/__init__.py @@ -0,0 +1,7 @@ +from returns.io.io import IO as IO +from returns.io.io import impure as impure +from returns.io.ioresult import IOFailure as IOFailure +from returns.io.ioresult import IOResult as IOResult +from returns.io.ioresult import IOResultE as IOResultE +from returns.io.ioresult import IOSuccess as IOSuccess +from returns.io.ioresult import impure_safe as impure_safe diff --git a/returns/io/io.py b/returns/io/io.py new file mode 100644 index 000000000..832e59507 --- /dev/null +++ b/returns/io/io.py @@ -0,0 +1,228 @@ +from functools import wraps +from typing import TYPE_CHECKING, Callable, TypeVar + +from typing_extensions import final + +from returns.interfaces.specific import io +from returns.primitives.container import BaseContainer, container_equality +from returns.primitives.hkt import Kind1, SupportsKind1, dekind + +if TYPE_CHECKING: + from returns.io.ioresult import IOResult + +_ValueType = TypeVar('_ValueType', covariant=True) +_NewValueType = TypeVar('_NewValueType') + +# Result related: +_ErrorType = TypeVar('_ErrorType', covariant=True) +_NewErrorType = TypeVar('_NewErrorType') + +# Helpers: +_FirstType = TypeVar('_FirstType') +_SecondType = TypeVar('_SecondType') + + +@final +class IO( + BaseContainer, + SupportsKind1['IO', _ValueType], + io.IOLike1[_ValueType], +): + """ + Explicit container for impure function results. + + We also sometimes call it "marker" since once it is marked, + it cannot be ever unmarked. + There's no way to directly get its internal value. + + Note that ``IO`` represents a computation that never fails. + + Examples of such computations are: + + - read / write to localStorage + - get the current time + - write to the console + - get a random number + + Use ``IOResult[...]`` for operations that might fail. + Like DB access or network operations. + + See also: + - https://dev.to/gcanti/getting-started-with-fp-ts-io-36p6 + - https://gist.github.com/chris-taylor/4745921 + + """ + + _inner_value: _ValueType + + #: Typesafe equality comparison with other `Result` objects. + equals = container_equality + + def __init__(self, inner_value: _ValueType) -> None: + """ + Public constructor for this type. Also required for typing. + + .. code:: python + + >>> from returns.io import IO + >>> assert str(IO(1)) == '' + + """ + super().__init__(inner_value) + + def map( # noqa: WPS125 + self, + function: Callable[[_ValueType], _NewValueType], + ) -> 'IO[_NewValueType]': + """ + Applies function to the inner value. + + Applies 'function' to the contents of the IO instance + and returns a new IO object containing the result. + 'function' should accept a single "normal" (non-container) argument + and return a non-container result. + + .. code:: python + + >>> def mappable(string: str) -> str: + ... return string + 'b' + + >>> assert IO('a').map(mappable) == IO('ab') + + """ + return IO(function(self._inner_value)) + + def apply( + self, + container: Kind1['IO', Callable[[_ValueType], _NewValueType]], + ) -> 'IO[_NewValueType]': + """ + Calls a wrapped function in a container on this container. + + .. code:: python + + >>> from returns.io import IO + >>> assert IO('a').apply(IO(lambda inner: inner + 'b')) == IO('ab') + + Or more complex example that shows how we can work + with regular functions and multiple ``IO`` arguments: + + .. code:: python + + >>> from returns.curry import curry + + >>> @curry + ... def appliable(first: str, second: str) -> str: + ... return first + second + + >>> assert IO('b').apply(IO('a').apply(IO(appliable))) == IO('ab') + + """ + return self.map(dekind(container)._inner_value) # noqa: WPS437 + + def bind( + self, + function: Callable[[_ValueType], Kind1['IO', _NewValueType]], + ) -> 'IO[_NewValueType]': + """ + Applies 'function' to the result of a previous calculation. + + 'function' should accept a single "normal" (non-container) argument + and return ``IO`` type object. + + .. code:: python + + >>> def bindable(string: str) -> IO[str]: + ... return IO(string + 'b') + + >>> assert IO('a').bind(bindable) == IO('ab') + + """ + return dekind(function(self._inner_value)) + + #: Alias for `bind` method. Part of the `IOLikeN` interface. + bind_io = bind + + @classmethod + def from_value(cls, inner_value: _NewValueType) -> 'IO[_NewValueType]': + """ + Unit function to construct new ``IO`` values. + + Is the same as regular constructor: + + .. code:: python + + >>> from returns.io import IO + >>> assert IO(1) == IO.from_value(1) + + Part of the :class:`returns.interfaces.applicative.ApplicativeN` + interface. + """ + return IO(inner_value) + + @classmethod + def from_io(cls, inner_value: 'IO[_NewValueType]') -> 'IO[_NewValueType]': + """ + Unit function to construct new ``IO`` values from existing ``IO``. + + .. code:: python + + >>> from returns.io import IO + >>> assert IO(1) == IO.from_io(IO(1)) + + Part of the :class:`returns.interfaces.specific.IO.IOLikeN` interface. + + """ + return inner_value + + @classmethod + def from_ioresult( + cls, + inner_value: 'IOResult[_NewValueType, _NewErrorType]', + ) -> 'IO[Result[_NewValueType, _NewErrorType]]': + """ + Converts ``IOResult[a, b]`` back to ``IO[Result[a, b]]``. + + Can be really helpful for composition. + + .. code:: python + + >>> from returns.io import IO, IOSuccess + >>> from returns.result import Success + >>> assert IO.from_ioresult(IOSuccess(1)) == IO(Success(1)) + + Is the reverse of :meth:`returns.io.IOResult.from_typecast`. + """ + return IO(inner_value._inner_value) # noqa: WPS437 + + +# Helper functions: + +def impure( + function: Callable[..., _NewValueType], +) -> Callable[..., IO[_NewValueType]]: + """ + Decorator to mark function that it returns :class:`~IO` container. + + If you need to mark ``async`` function as impure, + use :func:`returns.future.future` instead. + This decorator only works with sync functions. Example: + + .. code:: python + + >>> from returns.io import IO, impure + + >>> @impure + ... def function(arg: int) -> int: + ... return arg + 1 # this action is pure, just an example + ... + + >>> assert function(1) == IO(2) + + Requires our :ref:`mypy plugin `. + + """ + @wraps(function) + def decorator(*args, **kwargs): + return IO(function(*args, **kwargs)) + return decorator diff --git a/returns/io.py b/returns/io/ioresult.py similarity index 78% rename from returns/io.py rename to returns/io/ioresult.py index cc813a046..ccc58f607 100644 --- a/returns/io.py +++ b/returns/io/ioresult.py @@ -1,21 +1,28 @@ from abc import ABCMeta from functools import wraps from inspect import FrameInfo -from typing import Any, Callable, ClassVar, List, Optional, Type, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + List, + Optional, + Type, + TypeVar, + Union, +) from typing_extensions import final -from returns.interfaces.specific import io, ioresult +from returns.interfaces.specific import ioresult from returns.primitives.container import BaseContainer, container_equality -from returns.primitives.hkt import ( - Kind1, - Kind2, - SupportsKind1, - SupportsKind2, - dekind, -) +from returns.primitives.hkt import Kind2, SupportsKind2, dekind from returns.result import Failure, Result, Success +if TYPE_CHECKING: + from returns.io.io import IO + _ValueType = TypeVar('_ValueType', covariant=True) _NewValueType = TypeVar('_NewValueType') @@ -28,211 +35,6 @@ _SecondType = TypeVar('_SecondType') -class IO( - BaseContainer, - SupportsKind1['IO', _ValueType], - io.IOLike1[_ValueType], -): - """ - Explicit container for impure function results. - - We also sometimes call it "marker" since once it is marked, - it cannot be ever unmarked. - There's no way to directly get its internal value. - - Note that ``IO`` represents a computation that never fails. - - Examples of such computations are: - - - read / write to localStorage - - get the current time - - write to the console - - get a random number - - Use ``IOResult[...]`` for operations that might fail. - Like DB access or network operations. - - See also: - - https://dev.to/gcanti/getting-started-with-fp-ts-io-36p6 - - https://gist.github.com/chris-taylor/4745921 - - """ - - _inner_value: _ValueType - - #: Typesafe equality comparison with other `Result` objects. - equals = container_equality - - def __init__(self, inner_value: _ValueType) -> None: - """ - Public constructor for this type. Also required for typing. - - .. code:: python - - >>> from returns.io import IO - >>> assert str(IO(1)) == '' - - """ - super().__init__(inner_value) - - def map( # noqa: WPS125 - self, - function: Callable[[_ValueType], _NewValueType], - ) -> 'IO[_NewValueType]': - """ - Applies function to the inner value. - - Applies 'function' to the contents of the IO instance - and returns a new IO object containing the result. - 'function' should accept a single "normal" (non-container) argument - and return a non-container result. - - .. code:: python - - >>> def mappable(string: str) -> str: - ... return string + 'b' - - >>> assert IO('a').map(mappable) == IO('ab') - - """ - return IO(function(self._inner_value)) - - def apply( - self, - container: Kind1['IO', Callable[[_ValueType], _NewValueType]], - ) -> 'IO[_NewValueType]': - """ - Calls a wrapped function in a container on this container. - - .. code:: python - - >>> from returns.io import IO - >>> assert IO('a').apply(IO(lambda inner: inner + 'b')) == IO('ab') - - Or more complex example that shows how we can work - with regular functions and multiple ``IO`` arguments: - - .. code:: python - - >>> from returns.curry import curry - - >>> @curry - ... def appliable(first: str, second: str) -> str: - ... return first + second - - >>> assert IO('b').apply(IO('a').apply(IO(appliable))) == IO('ab') - - """ - return self.map(dekind(container)._inner_value) # noqa: WPS437 - - def bind( - self, - function: Callable[[_ValueType], Kind1['IO', _NewValueType]], - ) -> 'IO[_NewValueType]': - """ - Applies 'function' to the result of a previous calculation. - - 'function' should accept a single "normal" (non-container) argument - and return ``IO`` type object. - - .. code:: python - - >>> def bindable(string: str) -> IO[str]: - ... return IO(string + 'b') - - >>> assert IO('a').bind(bindable) == IO('ab') - - """ - return dekind(function(self._inner_value)) - - #: Alias for `bind` method. Part of the `IOLikeN` interface. - bind_io = bind - - @classmethod - def from_value(cls, inner_value: _NewValueType) -> 'IO[_NewValueType]': - """ - Unit function to construct new ``IO`` values. - - Is the same as regular constructor: - - .. code:: python - - >>> from returns.io import IO - >>> assert IO(1) == IO.from_value(1) - - Part of the :class:`returns.interfaces.applicative.ApplicativeN` - interface. - """ - return IO(inner_value) - - @classmethod - def from_io(cls, inner_value: 'IO[_NewValueType]') -> 'IO[_NewValueType]': - """ - Unit function to construct new ``IO`` values from existing ``IO``. - - .. code:: python - - >>> from returns.io import IO - >>> assert IO(1) == IO.from_io(IO(1)) - - Part of the :class:`returns.interfaces.specific.IO.IOLikeN` interface. - - """ - return inner_value - - @classmethod - def from_ioresult( - cls, - inner_value: 'IOResult[_NewValueType, _NewErrorType]', - ) -> 'IO[Result[_NewValueType, _NewErrorType]]': - """ - Converts ``IOResult[a, b]`` back to ``IO[Result[a, b]]``. - - Can be really helpful for composition. - - .. code:: python - - >>> from returns.io import IO, IOSuccess - >>> from returns.result import Success - >>> assert IO.from_ioresult(IOSuccess(1)) == IO(Success(1)) - - Is the reverse of :meth:`returns.io.IOResult.from_typecast`. - """ - return IO(inner_value._inner_value) # noqa: WPS437 - - -# Helper functions: - -def impure( - function: Callable[..., _NewValueType], -) -> Callable[..., IO[_NewValueType]]: - """ - Decorator to mark function that it returns :class:`~IO` container. - - If you need to mark ``async`` function as impure, - use :func:`returns.future.future` instead. - This decorator only works with sync functions. Example: - - .. code:: python - - >>> from returns.io import IO, impure - - >>> @impure - ... def function(arg: int) -> int: - ... return arg + 1 # this action is pure, just an example - ... - - >>> assert function(1) == IO(2) - - Requires our :ref:`mypy plugin `. - - """ - @wraps(function) - def decorator(*args, **kwargs): - return IO(function(*args, **kwargs)) - return decorator - - # IO and Result: class IOResult( diff --git a/returns/io/lazyio.py b/returns/io/lazyio.py new file mode 100644 index 000000000..ea1da2564 --- /dev/null +++ b/returns/io/lazyio.py @@ -0,0 +1,88 @@ +from functools import wraps +from typing import TYPE_CHECKING, Callable, TypeVar + +from typing_extensions import final + +from returns.interfaces.specific import io +from returns.primitives.container import BaseContainer, container_equality +from returns.primitives.hkt import Kind1, SupportsKind1, dekind +from returns.io.io import IO + +_ValueType = TypeVar('_ValueType', covariant=True) +_NewValueType = TypeVar('_NewValueType') + +# Result related: +_ErrorType = TypeVar('_ErrorType', covariant=True) +_NewErrorType = TypeVar('_NewErrorType') + +# Helpers: +_FirstType = TypeVar('_FirstType') +_SecondType = TypeVar('_SecondType') + + +@final +class LazyIO( + BaseContainer, + SupportsKind1['LazyIO', _ValueType], + io.IOLike1[_ValueType], +): + """ + + + """ + + _inner_value: Callable[['LazyIO'], IO[_ValueType]] + + #: Typesafe equality comparison with other `Result` objects. + equals = container_equality + + def __init__(self, inner_value: Callable[[], IO[_ValueType]]) -> None: + """ + Public constructor for this type. Also required for typing. + + .. code:: python + + >>> from returns.io import LazyIO + >>> assert LazyIO(lambda: 1)() == 1 + + """ + super().__init__(inner_value) + + def __call__(self) -> IO[_ValueType]: + """ + Executes the wrapped ``IO`` action. + """ + return self._inner_value() + + def map( # noqa: WPS125 + self, + function: Callable[[_ValueType], _NewValueType], + ) -> 'LazyIO[_NewValueType]': + return LazyIO(lambda: IO(function(self()._inner_value))) + + def apply( + self, + container: Kind1['LazyIO', Callable[[_ValueType], _NewValueType]], + ) -> 'LazyIO[_NewValueType]': + return self.map(dekind(container)()._inner_value) # noqa: WPS437 + + def bind( + self, + function: Callable[[_ValueType], Kind1['LazyIO', _NewValueType]], + ) -> 'LazyIO[_NewValueType]': + return function(self()._inner_value) + + + +# Helper functions: + +def impure_lazy( + function: Callable[..., _NewValueType], +) -> Callable[..., LazyIO[_NewValueType]]: + """ + + """ + @wraps(function) + def decorator(*args, **kwargs): + return LazyIO(lambda: IO(function(*args, **kwargs))) + return decorator diff --git a/returns/io/lazyio_result.py b/returns/io/lazyio_result.py new file mode 100644 index 000000000..e69de29bb diff --git a/setup.cfg b/setup.cfg index 95a320768..04230bb76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,10 +54,10 @@ per-file-ignores = # We allow reexport: returns/pointfree/__init__.py: F401, WPS201 returns/methods/__init__.py: F401, WPS201 - returns/pipeline.py: F401 returns/context/__init__.py: F401, WPS201 + returns/io/__init__.py: F401 + returns/pipeline.py: F401 # Disable some quality checks for the most heavy parts: - returns/io.py: WPS402 returns/iterables.py: WPS234 # Interfaces and asserts can have assert statements: returns/interfaces/*.py: S101