diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e81d9b08..81fc7d48c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ See [0Ver](https://0ver.org/). - Make `hypothesis` plugin test laws from user-defined interfaces too - Make `hypothesis` plugin accept user-defined strategies +- Allow users to override the `hypothesis` plugin's strategies for types, such + as `TypeVar` and `Callable`. ### Bugfixes diff --git a/docs/pages/contrib/hypothesis_plugins.rst b/docs/pages/contrib/hypothesis_plugins.rst index cbfcabe69..b979cc178 100644 --- a/docs/pages/contrib/hypothesis_plugins.rst +++ b/docs/pages/contrib/hypothesis_plugins.rst @@ -32,40 +32,8 @@ So, you don't have to. Example: assert st.from_type(Result).example() -This is a convenience thing only. - - -strategy_from_container ------------------------ - -We provide a utility function -to create ``hypothesis`` strategy from any container. - -You can use it to easily register your own containers. - -.. code:: python - - from hypothesis import strategies as st - from returns.contrib.hypothesis.containers import strategy_from_container - - st.register_type_strategy( - YourContainerClass, - strategy_from_container(YourContainerClass), - ) - -You can also pass ``use_init`` keyword argument -if you wish to use ``__init__`` method to instantiate your containers. -Turned off by default. -Example: - -.. code:: python - - st.register_type_strategy( - YourContainerClass, - strategy_from_container(YourContainerClass, use_init=True), - ) - -Or you can write your own ``hypothesis`` strategy. It is also fine. +This means you can use ``Result``, ``Maybe``, etc. in your own property tests, +and ``hypothesis`` will generate values for them as expected. check_all_laws @@ -140,8 +108,26 @@ like ``Future``, ``ReaderFutureResult``, etc that have complex ``__init__`` signatures. And we don't want to mess with them. -You can also register a custom strategy to be used when running your -container's laws: +Warning:: + Checking laws is not compatible with ``pytest-xdist``, + because we use a lot of global mutable state there. + Please, use ``returns_lawful`` marker + to exclude them from ``pytest-xdist`` execution plan. + + +Registering Custom Strategies when Checking Laws +------------------------------------------------ + +``hypothesis`` works by looking up strategies for the provided type +annotations. Given that the types provided by ``returns`` are very complicated +and not really native to Python, they may not be understood by ``hypothesis``, +and you may get runtime exceptions such as ``ResolutionFailed``. + +In such cases, you may want to register custom strategies for types for which +``hypothesis`` does not find any strategies. + +The main use case is registering a custom strategy to generate your container +when running its laws: .. code:: python @@ -150,22 +136,63 @@ container's laws: check_all_laws(Number, container_strategy=st.builds(Number, st.integers())) -The ``container_strategy`` will be used only when running the tests generated -by the ``check_all_laws`` call above. It will have no effect on any other -property tests that involve ``Number``. You cannot use this argument together -with ``use_init``. +You can also register strategies for other types: + +.. code:: python + + from hypothesis import strategies as st + + check_all_laws( + Number, + container_strategy=st.builds(Number, st.integers()), + type_strategies={Foo: st.builds(Foo, st.text())}, + ) + +These custom strategies will be used only when running the tests generated by +the ``check_all_laws`` call above. They will have no effect on any other +property tests that involve the same types. You cannot use this argument +together with ``use_init``. + + +Registering Custom Strategies outside Law Tests +----------------------------------------------- + +We provide a utility function +to create ``hypothesis`` strategy from any container: +``strategy_from_container``. + +You can use it to register your own containers. + +.. code:: python + + from hypothesis import strategies as st + from returns.contrib.hypothesis.containers import strategy_from_container + + st.register_type_strategy( + YourContainerClass, + strategy_from_container(YourContainerClass), + ) + +You can also pass ``use_init`` keyword argument +if you wish to use ``__init__`` method to instantiate your containers. +Turned off by default. +Example: + +.. code:: python + + st.register_type_strategy( + YourContainerClass, + strategy_from_container(YourContainerClass, use_init=True), + ) + +Or you can write your own ``hypothesis`` strategy. It is also fine. Warning:: Avoid directly registering your container's strategy with ``hypothesis`` using ``st.register_type_strategy``. Because of the way we emulate - higher-kinded types, ``hypothesis`` may mistakenly use the strategy - for other incompatible containers and cause spurious test failures. - -Warning:: - Checking laws is not compatible with ``pytest-xdist``, - because we use a lot of global mutable state there. - Please, use ``returns_lawful`` marker - to exclude them from ``pytest-xdist`` execution plan. + higher-kinded types, ``hypothesis`` may mistakenly use the strategy for + other incompatible containers and cause spurious test failures. We specify + how to do it just in case you need it and you know what you're doing. Further reading diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index bc84818a2..feb59fec7 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -1,33 +1,54 @@ import dataclasses import inspect -from collections.abc import Callable, Iterator, Mapping +from collections.abc import Callable, Iterator from contextlib import ExitStack, contextmanager -from typing import Any, TypeGuard, TypeVar, final, overload +from typing import Any, TypeVar, final, overload import pytest from hypothesis import given from hypothesis import settings as hypothesis_settings from hypothesis import strategies as st from hypothesis.strategies._internal import types # noqa: PLC2701 +from typing_extensions import Self from returns.contrib.hypothesis.containers import strategy_from_container from returns.contrib.hypothesis.type_resolver import ( StrategyFactory, strategies_for_types, ) -from returns.primitives.laws import LAWS_ATTRIBUTE, Law, Lawful +from returns.primitives.laws import Law, Lawful Example_co = TypeVar('Example_co', covariant=True) @final @dataclasses.dataclass(frozen=True) -class _Settings: - """Settings that we provide to an end user.""" +class Settings: + """Settings for the law tests. + This sets the context for each generated law test, by temporarily + registering strategies for various types and passing any ``hypothesis`` + settings. + + Any settings passed by the user will override the value from + :func:`default_settings`. + """ + + #: Settings directly passed on to `hypothesis`. We support all kwargs from + #: ``@settings``, see `@settings docs + #: `_. settings_kwargs: dict[str, Any] + #: Whether to create examples using ``__init__`` instead of the default . use_init: bool + #: Strategy for generating the container. By default, we generate examples + #: of a container using: + #: :func:`returns.contrib.hypothesis.containers.strategy_from_container`. container_strategy: StrategyFactory | None + #: Strategies for generating values of types other than the container and + #: its lawful interfaces. This can be useful for overriding ``TypeVar``, + #: ``Callable``, etc. in case you use certain types that ``hypothesis`` is + #: unable to find. + type_strategies: dict[type[object], StrategyFactory] def __post_init__(self) -> None: """Check that the settings are mutually compatible.""" @@ -37,13 +58,53 @@ def __post_init__(self) -> None: ' `container_strategy` to be truthy' ) + def __or__(self, other: Self) -> Self: + """Merge the two settings, preferring values from `other`.""" + return Settings( + settings_kwargs=self.settings_kwargs | other.settings_kwargs, + use_init=self.use_init | other.use_init, + container_strategy=self.container_strategy + if other.container_strategy is None + else other.container_strategy, + type_strategies=self.type_strategies | other.type_strategies, + ) + + +def default_settings(container_type: type[Lawful]) -> Settings: + """Return default settings for creating law tests. + + We use some special strategies by default, but + they can be overridden by the user if needed: + + + `TypeVar`: We need to make sure that the values generated behave + sensibly when tested for equality. + + + `collections.abc.Callable`: We need to generate pure functions, which + are not the default. + + Note that this is `collections.abc.Callable`, NOT `typing.Callable`. This + is because, at runtime, `typing.get_origin(Callable[[int], str])` is + `collections.abc.Callable`. So, this is the type we should register with + `hypothesis`. + """ + return Settings( + settings_kwargs={}, + use_init=False, + container_strategy=None, + type_strategies={ + TypeVar: type_vars_factory, # type: ignore[dict-item] + Callable: pure_functions_factory, # type: ignore[dict-item] + }, + ) + @overload def check_all_laws( container_type: type[Lawful[Example_co]], *, + container_strategy: StrategyFactory[Example_co], settings_kwargs: dict[str, Any] | None = None, - container_strategy: StrategyFactory[Example_co] | None = None, + type_strategies: dict[type[object], StrategyFactory] | None = None, ) -> None: ... @@ -62,6 +123,7 @@ def check_all_laws( settings_kwargs: dict[str, Any] | None = None, use_init: bool = False, container_strategy: StrategyFactory[Example_co] | None = None, + type_strategies: dict[type[object], StrategyFactory] | None = None, ) -> None: """ Function to check all defined mathematical laws in a specified container. @@ -89,10 +151,11 @@ def check_all_laws( - https://mmhaskell.com/blog/2017/3/13/obey-the-type-laws """ - settings = _Settings( + settings = default_settings(container_type) | Settings( settings_kwargs or {}, use_init, container_strategy, + type_strategies=type_strategies or {}, ) for interface, laws in container_type.laws().items(): @@ -105,101 +168,32 @@ def check_all_laws( ) -@contextmanager -def interface_strategies( - container_type: type[Lawful], - *, - settings: _Settings, -) -> Iterator[None]: - """ - Make all interfaces of a container resolve to the container's strategy. - - For example, let's say we have ``Result`` type. - It is a subtype of ``ContainerN``, ``MappableN``, ``BindableN``, etc. - When we check this type, we need ``MappableN`` to resolve to ``Result``. - - Can be used independently from other functions. - """ - mapping: Mapping[type[object], StrategyFactory] = { - interface: _strategy_for_container(container_type, settings) - for interface in lawful_interfaces(container_type) - } - with strategies_for_types(mapping): - yield - - -@contextmanager -def register_container( - container_type: type['Lawful'], - *, - settings: _Settings, -) -> Iterator[None]: - """Temporary registers a container if it is not registered yet.""" - with strategies_for_types({ - container_type: _strategy_for_container(container_type, settings) - }): - yield - - -@contextmanager -def pure_functions() -> Iterator[None]: - """ - Context manager to resolve all ``Callable`` as pure functions. - - It is not a default in ``hypothesis``. - """ - - def factory(thing) -> st.SearchStrategy: - like = ( - (lambda: None) - if len(thing.__args__) == 1 - else (lambda *args, **kwargs: None) - ) - return_type = thing.__args__[-1] - return st.functions( - like=like, - returns=st.from_type( - type(None) if return_type is None else return_type, - ), - pure=True, - ) - - used = types._global_type_lookup[Callable] # type: ignore[index] # noqa: SLF001 - st.register_type_strategy(Callable, factory) # type: ignore[arg-type] - - try: - yield - finally: - types._global_type_lookup.pop(Callable) # type: ignore[call-overload] # noqa: SLF001 - st.register_type_strategy(Callable, used) # type: ignore[arg-type] - - -@contextmanager -def type_vars() -> Iterator[None]: - """ - Our custom ``TypeVar`` handling. +def pure_functions_factory(thing) -> st.SearchStrategy: + """Factory to create pure functions.""" + like = ( + (lambda: None) + if len(thing.__args__) == 1 + else (lambda *args, **kwargs: None) + ) + return_type = thing.__args__[-1] + return st.functions( + like=like, + returns=st.from_type( + type(None) if return_type is None else return_type, + ), + pure=True, + ) - There are several noticeable differences: - 1. We add mutable types to the tests: like ``list`` and ``dict`` - 2. We ensure that values inside strategies are self-equal, - for example, ``nan`` does not work for us +def type_vars_factory(thing: type[object]) -> StrategyFactory: + """Strategy factory for ``TypeVar``s. + We ensure that values inside strategies are self-equal. For example, + ``nan`` does not work for us. """ - - def factory(thing): - return types.resolve_TypeVar(thing).filter( - lambda inner: inner == inner, # noqa: PLR0124, WPS312 - ) - - used = types._global_type_lookup.pop(TypeVar) # noqa: SLF001 - st.register_type_strategy(TypeVar, factory) - - try: - yield - finally: - types._global_type_lookup.pop(TypeVar) # noqa: SLF001 - st.register_type_strategy(TypeVar, used) + return types.resolve_TypeVar(thing).filter( # type: ignore[no-any-return] + lambda inner: inner == inner, # noqa: PLR0124, WPS312 + ) @contextmanager @@ -228,71 +222,16 @@ 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 -def _strategy_for_container( - container_type: type[Lawful], - settings: _Settings, -) -> StrategyFactory: - return ( - strategy_from_container(container_type, use_init=settings.use_init) - if settings.container_strategy is None - else settings.container_strategy - ) - - -def _run_law( - container_type: type[Lawful], - law: Law, - *, - settings: _Settings, -) -> Callable[[st.DataObject], None]: - def factory(source: st.DataObject) -> None: - with ExitStack() as stack: - stack.enter_context(clean_plugin_context()) - stack.enter_context(type_vars()) - stack.enter_context(pure_functions()) - stack.enter_context( - interface_strategies(container_type, settings=settings), - ) - stack.enter_context( - register_container(container_type, settings=settings), - ) - source.draw(st.builds(law.definition)) - - return factory - - def _create_law_test_case( container_type: type[Lawful], interface: type[Lawful], law: Law, *, - settings: _Settings, + settings: Settings, ) -> None: test_function = given(st.data())( hypothesis_settings(**settings.settings_kwargs)( @@ -322,3 +261,55 @@ def _create_law_test_case( pytest.mark.returns_lawful(test_function), ), ) + + +def _run_law( + container_type: type[Lawful], + law: Law, + *, + settings: Settings, +) -> Callable[[st.DataObject], None]: + def factory(source: st.DataObject) -> None: + with ExitStack() as stack: + stack.enter_context(clean_plugin_context()) + stack.enter_context( + strategies_for_types( + _types_to_strategies(container_type, settings) + ) + ) + source.draw(st.builds(law.definition)) + + return factory + + +def _types_to_strategies( + container_type: type[Lawful], + settings: Settings, +) -> dict[type[object], StrategyFactory]: + """Return a mapping from type to `hypothesis` strategy.""" + return settings.type_strategies | _container_mapping( + container_type, settings + ) + + +def _container_mapping( + container_type: type[Lawful], + settings: Settings, +) -> dict[type[object], StrategyFactory]: + """Map `container_type` and its interfaces to the container strategy.""" + container_strategy = _strategy_for_container(container_type, settings) + return { + **dict.fromkeys(container_type.laws(), container_strategy), + container_type: container_strategy, + } + + +def _strategy_for_container( + container_type: type[Lawful], + settings: Settings, +) -> StrategyFactory: + return ( + strategy_from_container(container_type, use_init=settings.use_init) + if settings.container_strategy is None + else settings.container_strategy + ) diff --git a/tests/test_contrib/test_hypothesis/test_interface_resolution.py b/tests/test_contrib/test_hypothesis/test_interface_resolution.py deleted file mode 100644 index 73861ba96..000000000 --- a/tests/test_contrib/test_hypothesis/test_interface_resolution.py +++ /dev/null @@ -1,42 +0,0 @@ -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) == [ - "", - "", - "", - "", - "", - "", - "", - "", - ] - - -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) == [ - "", - "", - ] - - -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) == [ - "" - ] diff --git a/tests/test_contrib/test_hypothesis/test_laws/test_custom_strategy_for_callable.py b/tests/test_contrib/test_hypothesis/test_laws/test_custom_strategy_for_callable.py new file mode 100644 index 000000000..852873308 --- /dev/null +++ b/tests/test_contrib/test_hypothesis/test_laws/test_custom_strategy_for_callable.py @@ -0,0 +1,161 @@ +""" +Test that we can register a custom strategy for callables. + +We use the strategy to generates `Maybe`s and `Result`s for +`Kind1[_F_Applicative, _SecondType]`. + +Without the custom strategy, we would simply get instances of `_Wrapper`, even +though it is not an `Applicative`, because `_Wrapper` is a subtype of `KindN` +and `hypothesis` doesn't know about the fact that `KindN` is just emulating +HKTs. +""" + +from abc import abstractmethod +from collections.abc import Callable, Sequence +from typing import ( + Any, + ClassVar, + Generic, + TypeAlias, + TypeVar, + final, + get_args, + get_origin, +) + +from hypothesis import strategies as st +from typing_extensions import Never + +from returns.contrib.hypothesis.containers import strategy_from_container +from returns.contrib.hypothesis.laws import check_all_laws +from returns.contrib.hypothesis.type_resolver import StrategyFactory +from returns.interfaces.applicative import ApplicativeN +from returns.interfaces.specific.maybe import MaybeLike2 +from returns.maybe import Maybe +from returns.primitives.asserts import assert_equal +from returns.primitives.container import BaseContainer +from returns.primitives.hkt import Kind1, KindN, SupportsKind1 +from returns.primitives.laws import ( + Law, + Law2, + Lawful, + LawSpecDef, + law_definition, +) +from returns.result import Result + +_FirstType = TypeVar('_FirstType') +_SecondType = TypeVar('_SecondType') +_ThirdType = TypeVar('_ThirdType') + +_ValueType = TypeVar('_ValueType') +_F_Applicative = TypeVar('_F_Applicative', bound=ApplicativeN) + + +@final +class _LawSpec(LawSpecDef): + """Contrived laws for `_SomeIdentityN`.""" + + __slots__ = () + + @law_definition + def idempotence_law( + some_identity: '_SomeIdentityN', + make_container: Callable[[], Kind1[_F_Applicative, _SecondType]], + ) -> None: + """Applying twice should give the same value as applying once. + + NOTE: We use a `Callable` to generate the container so that we can + override the strategy easily. Overriding the strategy directly for + `KindN` is not currently possible. + """ + container = make_container() + assert_equal( + some_identity.do_nothing(some_identity.do_nothing(container)), + some_identity.do_nothing(container), + ) + + +class _SomeIdentityN( + Lawful['_SomeIdentityN[_FirstType, _SecondType, _ThirdType]'], + Generic[_FirstType, _SecondType, _ThirdType], +): + """Dummy interface that does nothing to an `Applicative` container.""" + + __slots__ = () + + _laws: ClassVar[Sequence[Law]] = (Law2(_LawSpec.idempotence_law),) + + @abstractmethod # noqa: WPS125 + def do_nothing( + self, + container: Kind1[_F_Applicative, _ValueType], + ) -> Kind1[_F_Applicative, _ValueType]: + """No-op method that returns the container.""" + + +_SomeIdentity1: TypeAlias = _SomeIdentityN[_FirstType, Never, Never] + + +class _Wrapper( + BaseContainer, + SupportsKind1['_Wrapper', _FirstType], + _SomeIdentity1[_FirstType], +): + """Simple instance of `_SomeIdentityN`.""" + + _inner_value: _FirstType + + def __init__(self, inner_value: _FirstType) -> None: + super().__init__(inner_value) + + def do_nothing( + self, + container: Kind1[_F_Applicative, _ValueType], + ) -> Kind1[_F_Applicative, _ValueType]: + """No-op method that returns the container.""" + return container + + +def _callable_strategy( + arg1: type[object], arg2: type[object] +) -> StrategyFactory[Callable]: + type_arg1 = int if arg1 == Any else arg1 # type: ignore[comparison-overlap] + type_arg2 = int if arg2 == Any else arg2 # type: ignore[comparison-overlap] + return_results = st.functions( + pure=True, + returns=strategy_from_container(Result)(Result[type_arg1, type_arg2]), # type: ignore[valid-type] + ) + return_maybes = st.functions( + pure=True, + returns=strategy_from_container(Maybe)( + MaybeLike2[type_arg1, type_arg2] # type: ignore[valid-type] + ), + ) + return st.one_of(return_results, return_maybes) + + +def _callable_factory(thing: type[object]) -> StrategyFactory[Callable]: + if get_origin(thing) != Callable: + raise NotImplementedError + + match get_args(thing): + case [[], return_type] if ( + get_origin(return_type) == KindN + and get_args(return_type)[0] == _F_Applicative + ): + type1, type2, *_ = get_args(return_type)[1:] + return _callable_strategy(type1, type2) + case _: + raise NotImplementedError + + +type_strategies: dict[type[object], StrategyFactory] = { + Callable: _callable_factory # type: ignore[dict-item] +} + +check_all_laws( + _Wrapper, + container_strategy=st.builds(_Wrapper, st.integers()), + type_strategies=type_strategies, +) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 282237c69..8b54aa61d 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -1,5 +1,5 @@ -from collections.abc import Sequence -from typing import Any, Final +from collections.abc import Callable, Sequence +from typing import Any, TypeVar import pytest from hypothesis import given @@ -16,23 +16,22 @@ RequiresContextResultE, ) from returns.contrib.hypothesis.laws import ( - _Settings, # noqa: PLC2701 - interface_strategies, - lawful_interfaces, - register_container, + Settings, + _types_to_strategies, # noqa: PLC2701 + default_settings, ) from returns.contrib.hypothesis.type_resolver import ( StrategyFactory, apply_strategy, look_up_strategy, - strategies_for_types, ) from returns.future import Future, FutureResult +from returns.interfaces.applicative import ApplicativeN from returns.io import IO, IOResult, IOResultE from returns.maybe import Maybe from returns.pipeline import is_successful from returns.primitives.laws import Lawful -from returns.result import Result, ResultE, Success +from returns.result import Result, ResultE from test_hypothesis.test_laws import test_custom_type_applicative _all_containers: Sequence[type[Lawful]] = ( @@ -115,124 +114,162 @@ def test_custom_readerresult_types_resolve( assert isinstance(real_result.failure(), str) -DEFAULT_RESULT_STRATEGY: Final = ( - "one_of(builds(from_value, shared(sampled_from([, " - ", , , , " - "]), key='typevar=~_FirstType').flatmap(from_type)), " - "builds(from_failure, shared(sampled_from([, " - ", , , , " - "]), " - "key='typevar=~_SecondType').flatmap(from_type)))" -) - - -def test_register_container_with_no_strategy() -> None: - """Check that a container without a strategy gets a strategy.""" - container_type = Result - - with register_container( - container_type, - settings=_Settings( - settings_kwargs={}, use_init=False, container_strategy=None - ), - ): - strategy_factory = look_up_strategy(container_type) - - assert ( - _strategy_string(strategy_factory, container_type) - == DEFAULT_RESULT_STRATEGY +def test_merge_settings() -> None: + """Check that each part of the settings can be overridden by users.""" + settings1 = Settings( + settings_kwargs={'a': 1, 'b': 2}, + use_init=False, + container_strategy=st.integers(), + type_strategies={int: st.integers(max_value=10), str: st.text('abc')}, + ) + settings2 = Settings( + settings_kwargs={'a': 1, 'c': 3}, + use_init=False, + container_strategy=st.integers(max_value=20), + type_strategies={int: st.integers(max_value=30), bool: st.booleans()}, ) + result = settings1 | settings2 + + assert result == Settings( + settings_kwargs={'a': 1, 'b': 2, 'c': 3}, + use_init=False, + container_strategy=st.integers(max_value=20), + type_strategies={ + int: st.integers(max_value=30), + bool: st.booleans(), + str: st.text('abc'), + }, + ) -def test_register_container_with_strategy() -> None: - """Check that when a container has an existing strategy, we drop it.""" - container_type = Result - with ( - strategies_for_types({ - container_type: st.builds(container_type, st.integers()) - }), - register_container( - container_type, - settings=_Settings( - settings_kwargs={}, use_init=False, container_strategy=None - ), - ), - ): - strategy_factory = look_up_strategy(container_type) +def test_merge_use_init() -> None: + """Check that `use_init` can be set to `True` by users. - assert ( - _strategy_string(strategy_factory, container_type) - == DEFAULT_RESULT_STRATEGY + Note: They can't set a `True` to `False`, since we use `|` to merge. + However, the default value is `False`, so this should not be a problem. + """ + settings1 = Settings( + settings_kwargs={}, + use_init=False, + container_strategy=None, + type_strategies={}, + ) + settings2 = Settings( + settings_kwargs={}, + use_init=True, + container_strategy=None, + type_strategies={}, ) + result = settings1 | settings2 -def test_register_container_with_setting() -> None: - """Check that we prefer a strategy given in settings.""" - container_type = Result + assert result == Settings( + settings_kwargs={}, + use_init=True, + container_strategy=None, + type_strategies={}, + ) - with register_container( - container_type, - settings=_Settings( - settings_kwargs={}, - use_init=False, - container_strategy=st.builds(Success, st.integers()), - ), - ): - strategy_factory = look_up_strategy(container_type) - assert ( - _strategy_string(strategy_factory, container_type) - == 'builds(Success, integers())' - ) +_ValueType = TypeVar('_ValueType') -def test_interface_strategies() -> None: - """Check that ancestor interfaces get resolved to the concrete container.""" +def test_types_to_strategies_default() -> None: # noqa: WPS210 + """Check the default strategies for types.""" container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 + # NOTE: There is a type error because `Callable` is a + # special form, not a type. + callable_type: type[object] = Callable # type: ignore[assignment] - with interface_strategies( + result = _types_to_strategies( container_type, - settings=_Settings( - settings_kwargs={}, use_init=False, container_strategy=None - ), - ): - strategy_factories_inside = _interface_factories(container_type) + default_settings(container_type), + ) - assert _strategy_strings(strategy_factories_inside, container_type) == [ + wrapper_strategy = ( "builds(from_value, shared(sampled_from([," " , , , ," - " ]), key='typevar=~_FirstType').flatmap(from_type))", - "builds(from_value, shared(sampled_from([," - " , , , ," - " ]), key='typevar=~_FirstType').flatmap(from_type))", + " ]), key='typevar=~_FirstType').flatmap(from_type))" + ) + assert ( + _strategy_string(result[container_type], container_type) + == wrapper_strategy + ) + assert _strategy_strings( + [result[interface] for interface in container_type.laws()], + container_type, + ) == [ + wrapper_strategy, + wrapper_strategy, ] + assert ( + _strategy_string(result[callable_type], Callable[[int, str], bool]) + == 'functions(like=lambda *args, **kwargs: ,' + ' returns=booleans(), pure=True)' + ) + assert ( + _strategy_string(result[callable_type], Callable[[], None]) + == 'functions(like=lambda: None, returns=none(), pure=True)' + ) + assert ( + _strategy_string(result[TypeVar], _ValueType) + == "shared(sampled_from([, ," + " , , , ])," + " key='typevar=~_ValueType').flatmap(from_type).filter(lambda" + ' inner: inner == inner)' + ) -def test_interface_strategies_with_settings() -> None: - """Check that we prefer the strategy in the settings.""" +def test_types_to_strategies_overrides() -> None: # noqa: WPS210 + """Check that we allow the user to override all strategies.""" container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 + # NOTE: There is a type error because `Callable` is a + # special form, not a type. + callable_type: type[object] = Callable # type: ignore[assignment] - with interface_strategies( + result = _types_to_strategies( container_type, - settings=_Settings( + Settings( settings_kwargs={}, use_init=False, container_strategy=st.builds(container_type, st.integers()), + type_strategies={ + TypeVar: st.text(), + callable_type: st.functions(returns=st.booleans()), + # This strategy does not get used, because we use + # the given `container_strategy` for all interfaces of the + # container type. + ApplicativeN: st.tuples(st.integers()), + }, ), - ): - strategy_factories_inside = _interface_factories(container_type) + ) - assert _strategy_strings(strategy_factories_inside, container_type) == [ - 'builds(_Wrapper, integers())', - 'builds(_Wrapper, integers())', + wrapper_strategy = 'builds(_Wrapper, integers())' + assert ( + _strategy_string(result[container_type], container_type) + == wrapper_strategy + ) + assert _strategy_strings( + [result[interface] for interface in container_type.laws()], + container_type, + ) == [ + wrapper_strategy, + wrapper_strategy, ] + assert ( + _strategy_string(result[callable_type], Callable[[int, str], bool]) + == 'functions(returns=booleans())' + ) + assert ( + _strategy_string(result[callable_type], Callable[[], None]) + == 'functions(returns=booleans())' + ) + assert _strategy_string(result[TypeVar], _ValueType) == 'text()' def _interface_factories(type_: type[Lawful]) -> list[StrategyFactory | None]: - return [ - look_up_strategy(interface) for interface in lawful_interfaces(type_) - ] + return [look_up_strategy(interface) for interface in type_.laws()] def _strategy_strings( @@ -245,7 +282,7 @@ def _strategy_strings( def _strategy_string( - strategy_factory: StrategyFactory | None, type_: type[object] + strategy_factory: StrategyFactory | None, type_: Any ) -> str: """Return an easily testable string representation.""" return ( diff --git a/tests/test_primitives/test_laws/test_lawful/test_laws_resolution.py b/tests/test_primitives/test_laws/test_lawful/test_laws_resolution.py index f48b9792b..186c6fbd7 100644 --- a/tests/test_primitives/test_laws/test_lawful/test_laws_resolution.py +++ b/tests/test_primitives/test_laws/test_lawful/test_laws_resolution.py @@ -1,4 +1,8 @@ import pytest +from test_hypothesis.test_laws import ( + test_custom_interface_with_laws, + test_custom_type_applicative, +) from returns.context import ( RequiresContext, @@ -34,3 +38,39 @@ def test_laws_resolution(container: type[Lawful]): for laws in container.laws().values(): all_laws.extend(laws) assert len(all_laws) == len(set(all_laws)) + + +def test_container_defined_in_returns() -> None: + """Check that it returns all interfaces for a container in `returns`.""" + result = Result.laws() + + assert sorted(str(interface) for interface in result) == [ + "", + "", + "", + "", + "", + "", + "", + "", + ] + + +def test_container_defined_outside_returns() -> None: + """Check container defined outside `returns`.""" + result = test_custom_type_applicative._Wrapper.laws() # noqa: SLF001 + + assert sorted(str(interface) for interface in result) == [ + "", + "", + ] + + +def test_interface_defined_outside_returns() -> None: + """Check container with interface defined outside `returns`.""" + result = test_custom_interface_with_laws._Wrapper.laws() # noqa: SLF001 + + assert sorted(str(interface) for interface in result) == [ + "" + ] diff --git a/typesafety/test_contrib/test_hypothesis/test_laws/test_check_all_laws.yml b/typesafety/test_contrib/test_hypothesis/test_laws/test_check_all_laws.yml index 05bc40fa5..60a0191a3 100644 --- a/typesafety/test_contrib/test_hypothesis/test_laws/test_check_all_laws.yml +++ b/typesafety/test_contrib/test_hypothesis/test_laws/test_check_all_laws.yml @@ -41,6 +41,34 @@ check_all_laws(Result) check_all_laws(Result, use_init=True) check_all_laws(Result, container_strategy=st.builds(Success, st.integers())) - check_all_laws( # E: No overload variant of "check_all_laws" matches argument types "Type[Result[_ValueType_co, _ErrorType_co]]", "bool", "SearchStrategy[Success[Any]]" [call-overload] # N: Possible overload variants: # N: def [Example_co] check_all_laws(container_type: Type[Lawful[Example_co]], *, settings_kwargs: Optional[Dict[str, Any]] = ..., container_strategy: Optional[Union[SearchStrategy[Example_co], Callable[[Type[Example_co]], SearchStrategy[Example_co]]]] = ...) -> None # N: def [Example_co] check_all_laws(container_type: Type[Lawful[Example_co]], *, settings_kwargs: Optional[Dict[str, Any]] = ..., use_init: bool = ...) -> None + check_all_laws( Result, use_init=True, container_strategy=st.builds(Success, st.integers()) ) + + out: | + main:8: error: No overload variant of "check_all_laws" matches argument types "Type[Result[_ValueType_co, _ErrorType_co]]", "bool", "SearchStrategy[Success[Any]]" [call-overload] + main:8: note: Possible overload variants: + main:8: note: def [Example_co] check_all_laws(container_type: Type[Lawful[Example_co]], *, container_strategy: Union[SearchStrategy[Example_co], Callable[[Type[Example_co]], SearchStrategy[Example_co]]], settings_kwargs: Optional[Dict[str, Any]] = ..., type_strategies: Optional[Dict[Type[object], Union[SearchStrategy[Any], Callable[[Type[Any]], SearchStrategy[Any]]]]] = ...) -> None + main:8: note: def [Example_co] check_all_laws(container_type: Type[Lawful[Example_co]], *, settings_kwargs: Optional[Dict[str, Any]] = ..., use_init: bool = ...) -> None + +- case: test_all_laws_requires_container_strategy + disable_cache: false + # TODO: remove this config after + # mypy/typeshed/stdlib/unittest/mock.pyi:120: + # error: Class cannot subclass "Any" (has type "Any") + # is fixed. + mypy_config: + disallow_subclassing_any = False + main: | + from hypothesis import strategies as st + from returns.contrib.hypothesis.laws import check_all_laws + from returns.result import Result, Success + + check_all_laws(Result, container_strategy=st.builds(Success, st.integers()), type_strategies={int: st.integers()}) + check_all_laws(Result, type_strategies={int: st.integers()}) + + out: | + main:6: error: No overload variant of "check_all_laws" matches argument types "Type[Result[_ValueType_co, _ErrorType_co]]", "Dict[Type[int], SearchStrategy[int]]" [call-overload] + main:6: note: Possible overload variants: + main:6: note: def [Example_co] check_all_laws(container_type: Type[Lawful[Example_co]], *, container_strategy: Union[SearchStrategy[Example_co], Callable[[Type[Example_co]], SearchStrategy[Example_co]]], settings_kwargs: Optional[Dict[str, Any]] = ..., type_strategies: Optional[Dict[Type[object], Union[SearchStrategy[Any], Callable[[Type[Any]], SearchStrategy[Any]]]]] = ...) -> None + main:6: note: def [Example_co] check_all_laws(container_type: Type[Lawful[Example_co]], *, settings_kwargs: Optional[Dict[str, Any]] = ..., use_init: bool = ...) -> None