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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ See [0Ver](https://0ver.org/).

## 0.24.1

### Features

- Make `hypothesis` plugin test laws from user-defined interfaces too

### Bugfixes

- Add pickling support for `UnwrapFailedError` exception
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/contrib/hypothesis_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ It works in a combination with "Laws as Values" feature we provide in the core.

check_all_laws(YourCustomContainer)

This one line of code will generate ~100 tests for all defined law
This one line of code will generate ~100 tests for all defined laws
in both ``YourCustomContainer`` and all its super types,
including our internal ones.
including our internal ones and user-defined ones.

We also provide a way to configure
the checking process with ``settings_kwargs``:
Expand Down
35 changes: 25 additions & 10 deletions returns/contrib/hypothesis/laws.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import inspect
from collections.abc import Callable, Iterator
from contextlib import ExitStack, contextmanager
from typing import Any, NamedTuple, TypeVar, final
from typing import Any, NamedTuple, TypeGuard, TypeVar, final

import pytest
from hypothesis import given
Expand All @@ -10,7 +10,7 @@
from hypothesis.strategies._internal import types # noqa: PLC2701

from returns.contrib.hypothesis.containers import strategy_from_container
from returns.primitives.laws import Law, Lawful
from returns.primitives.laws import LAWS_ATTRIBUTE, Law, Lawful


@final
Expand Down Expand Up @@ -83,14 +83,7 @@ def container_strategies(

Can be used independently from other functions.
"""
our_interfaces = {
base_type
for base_type in container_type.__mro__
if (
getattr(base_type, '__module__', '').startswith('returns.')
and base_type not in {Lawful, container_type}
)
}
our_interfaces = lawful_interfaces(container_type)
for interface in our_interfaces:
st.register_type_strategy(
interface,
Expand Down Expand Up @@ -221,6 +214,28 @@ def clean_plugin_context() -> Iterator[None]:
st.register_type_strategy(*saved_state)


def lawful_interfaces(container_type: type[Lawful]) -> set[type[Lawful]]:
"""Return ancestors of `container_type` that are lawful interfaces."""
return {
base_type
for base_type in container_type.__mro__
if _is_lawful_interface(base_type)
and base_type not in {Lawful, container_type}
}


def _is_lawful_interface(
interface_type: type[object],
) -> TypeGuard[type[Lawful]]:
return issubclass(interface_type, Lawful) and _has_non_inherited_attribute(
interface_type, LAWS_ATTRIBUTE
)


def _has_non_inherited_attribute(type_: type[object], attribute: str) -> bool:
return attribute in type_.__dict__


def _clean_caches() -> None:
st.from_type.__clear_cache() # type: ignore[attr-defined] # noqa: SLF001

Expand Down
6 changes: 4 additions & 2 deletions returns/primitives/laws.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Callable, Sequence
from typing import ClassVar, Generic, TypeVar, final
from typing import ClassVar, Final, Generic, TypeVar, final

from returns.primitives.types import Immutable

Expand All @@ -12,6 +12,8 @@
#: Special alias to define laws as functions even inside a class
law_definition = staticmethod

LAWS_ATTRIBUTE: Final = '_laws'


class Law(Immutable):
"""
Expand Down Expand Up @@ -132,7 +134,7 @@ def laws(cls) -> dict[type['Lawful'], Sequence[Law]]: # noqa: WPS210

laws = {}
for klass in seen.values():
current_laws = klass.__dict__.get('_laws', ())
current_laws = klass.__dict__.get(LAWS_ATTRIBUTE, ())
if not current_laws:
continue
laws[klass] = current_laws
Expand Down
Empty file.
42 changes: 42 additions & 0 deletions tests/test_contrib/test_hypothesis/test_interface_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from returns.contrib.hypothesis.laws import lawful_interfaces
from returns.result import Result
from test_hypothesis.test_laws import (
test_custom_interface_with_laws,
test_custom_type_applicative,
)


def test_container_defined_in_returns() -> None:
"""Check that it returns all interfaces for a container in `returns`."""
result = lawful_interfaces(Result)

assert sorted(str(interface) for interface in result) == [
"<class 'returns.interfaces.altable.AltableN'>",
"<class 'returns.interfaces.applicative.ApplicativeN'>",
"<class 'returns.interfaces.container.ContainerN'>",
"<class 'returns.interfaces.equable.Equable'>",
"<class 'returns.interfaces.failable.DiverseFailableN'>",
"<class 'returns.interfaces.failable.FailableN'>",
"<class 'returns.interfaces.mappable.MappableN'>",
"<class 'returns.interfaces.swappable.SwappableN'>",
]


def test_container_defined_outside_returns() -> None:
"""Check container defined outside `returns`."""
result = lawful_interfaces(test_custom_type_applicative._Wrapper) # noqa: SLF001

assert sorted(str(interface) for interface in result) == [
"<class 'returns.interfaces.applicative.ApplicativeN'>",
"<class 'returns.interfaces.mappable.MappableN'>",
]


def test_interface_defined_outside_returns() -> None:
"""Check container with interface defined outside `returns`."""
result = lawful_interfaces(test_custom_interface_with_laws._Wrapper) # noqa: SLF001

assert sorted(str(interface) for interface in result) == [
"<class 'test_hypothesis.test_laws.test_custom_interface_with_laws"
"._MappableN'>"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from abc import abstractmethod
from collections.abc import Callable, Sequence
from typing import ClassVar, Generic, TypeAlias, TypeVar, final

from typing_extensions import Never

from returns.contrib.hypothesis.laws import check_all_laws
from returns.functions import compose, identity
from returns.primitives.asserts import assert_equal
from returns.primitives.container import BaseContainer
from returns.primitives.hkt import KindN, SupportsKind1
from returns.primitives.laws import (
Law,
Law1,
Law3,
Lawful,
LawSpecDef,
law_definition,
)

_FirstType = TypeVar('_FirstType')
_SecondType = TypeVar('_SecondType')
_ThirdType = TypeVar('_ThirdType')
_UpdatedType = TypeVar('_UpdatedType')

_MappableType = TypeVar('_MappableType', bound='_MappableN')
_ValueType = TypeVar('_ValueType')
_NewValueType = TypeVar('_NewValueType')

# Used in laws:
_NewType1 = TypeVar('_NewType1')
_NewType2 = TypeVar('_NewType2')


@final
class _LawSpec(LawSpecDef):
"""Copy of the functor laws for `MappableN`."""

__slots__ = ()

@law_definition
def identity_law(
mappable: '_MappableN[_FirstType, _SecondType, _ThirdType]',
) -> None:
"""Mapping identity over a value must return the value unchanged."""
assert_equal(mappable.map(identity), mappable)

@law_definition
def associative_law(
mappable: '_MappableN[_FirstType, _SecondType, _ThirdType]',
first: Callable[[_FirstType], _NewType1],
second: Callable[[_NewType1], _NewType2],
) -> None:
"""Mapping twice or mapping a composition is the same thing."""
assert_equal(
mappable.map(first).map(second),
mappable.map(compose(first, second)),
)


class _MappableN(
Lawful['_MappableN[_FirstType, _SecondType, _ThirdType]'],
Generic[_FirstType, _SecondType, _ThirdType],
):
"""Simple "user-defined" copy of `MappableN`."""

__slots__ = ()

_laws: ClassVar[Sequence[Law]] = (
Law1(_LawSpec.identity_law),
Law3(_LawSpec.associative_law),
)

@abstractmethod # noqa: WPS125
def map(
self: _MappableType,
function: Callable[[_FirstType], _UpdatedType],
) -> KindN[_MappableType, _UpdatedType, _SecondType, _ThirdType]:
"""Allows to run a pure function over a container."""


_Mappable1: TypeAlias = _MappableN[_FirstType, Never, Never]


class _ParentWrapper(_Mappable1[_ValueType]):
"""Class that is an ancestor of `_Wrapper` but has no laws."""


class _Wrapper(
BaseContainer,
SupportsKind1['_Wrapper', _ValueType],
_ParentWrapper[_ValueType],
):
"""Simple instance of `_MappableN`."""

_inner_value: _ValueType

def __init__(self, inner_value: _ValueType) -> None:
super().__init__(inner_value)

def map(
self,
function: Callable[[_ValueType], _NewValueType],
) -> '_Wrapper[_NewValueType]':
return _Wrapper(function(self._inner_value))


# We need to use `use_init=True` because `MappableN` does not automatically
# get a strategy from `strategy_from_container`.
check_all_laws(_Wrapper, use_init=True)