From 2be1352304383e9e167bef1bbd42c178c56172d3 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 14:19:26 +0530 Subject: [PATCH 01/20] hypothesis: get lawful ancestors using the `laws()` classmethod. Did not know about this earlier. Move in the tests to `test_laws_resolution.py`, since they are testing `laws`, not anything about `hypothesis`. --- returns/contrib/hypothesis/laws.py | 28 ++----------- .../test_interface_resolution.py | 42 ------------------- .../test_hypothesis/test_type_resolution.py | 5 +-- .../test_lawful/test_laws_resolution.py | 40 ++++++++++++++++++ 4 files changed, 44 insertions(+), 71 deletions(-) delete mode 100644 tests/test_contrib/test_hypothesis/test_interface_resolution.py diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index bc84818a2..d9781afb3 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -2,7 +2,7 @@ import inspect from collections.abc import Callable, Iterator, Mapping 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 @@ -15,7 +15,7 @@ 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) @@ -122,7 +122,7 @@ def interface_strategies( """ mapping: Mapping[type[object], StrategyFactory] = { interface: _strategy_for_container(container_type, settings) - for interface in lawful_interfaces(container_type) + for interface in container_type.laws() } with strategies_for_types(mapping): yield @@ -228,28 +228,6 @@ 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 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_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 282237c69..7f64f8048 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -18,7 +18,6 @@ from returns.contrib.hypothesis.laws import ( _Settings, # noqa: PLC2701 interface_strategies, - lawful_interfaces, register_container, ) from returns.contrib.hypothesis.type_resolver import ( @@ -230,9 +229,7 @@ def test_interface_strategies_with_settings() -> None: 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( 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) == [ + "" + ] From 16bc8153b10a73ee96faa9d5de354d713815984b Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 15:30:40 +0530 Subject: [PATCH 02/20] hypothesis: extract function for registering strategies. Want to make this pure and user-overridable. --- returns/contrib/hypothesis/laws.py | 24 ++++-- .../test_hypothesis/test_type_resolution.py | 85 ++++++++++++++++++- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index d9781afb3..fe8574bba 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -252,19 +252,27 @@ def _run_law( 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), - ) + _enter_hypothesis_context(stack, container_type, settings) source.draw(st.builds(law.definition)) return factory +def _enter_hypothesis_context( + stack: ExitStack, + container_type: type[Lawful], + settings: _Settings, +) -> None: + 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), + ) + + def _create_law_test_case( container_type: type[Lawful], interface: type[Lawful], diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 7f64f8048..7fbf1f972 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -1,5 +1,6 @@ -from collections.abc import Sequence -from typing import Any, Final +from collections.abc import Callable, Sequence +from contextlib import ExitStack +from typing import Any, Final, TypeVar import pytest from hypothesis import given @@ -16,6 +17,7 @@ RequiresContextResultE, ) from returns.contrib.hypothesis.laws import ( + _enter_hypothesis_context, # noqa: PLC2701 _Settings, # noqa: PLC2701 interface_strategies, register_container, @@ -186,6 +188,83 @@ def test_register_container_with_setting() -> None: ) +_ValueType = TypeVar('_ValueType') + + +def test_enter_hypothesis_context() -> None: # noqa: WPS210 + """Check that strategies are registered correctly.""" + 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] + + container_strategy_outside = look_up_strategy(container_type) + interface_strategies_outside = _interface_factories(container_type) + pure_functions_strategy_outside = look_up_strategy(callable_type) + type_var_strategy_outside = look_up_strategy(TypeVar) + + with ExitStack() as stack: + _enter_hypothesis_context( + stack, + container_type, + settings=_Settings( + settings_kwargs={}, use_init=False, container_strategy=None + ), + ) + container_strategy = look_up_strategy(container_type) + interface_strategies = _interface_factories(container_type) + pure_functions_strategy = look_up_strategy(callable_type) + type_var_strategy = look_up_strategy(TypeVar) + + assert ( + _strategy_string(container_strategy_outside, container_type) == 'None' + ) + assert _strategy_strings(interface_strategies_outside, container_type) == [ + 'None', + 'None', + ] + assert ( + _strategy_string( + pure_functions_strategy_outside, Callable[[int, str], bool] + ) + == 'functions(like=lambda *a, **k: None, returns=booleans())' + ) + assert ( + _strategy_string(type_var_strategy_outside, _ValueType) + == "shared(sampled_from([, ," + " , , , ])," + " key='typevar=~_ValueType').flatmap(from_type)" + ) + wrapper_strategy = ( + "builds(from_value, shared(sampled_from([," + " , , , ," + " ]), key='typevar=~_FirstType').flatmap(from_type))" + ) + assert ( + _strategy_string(container_strategy, container_type) == wrapper_strategy + ) + assert _strategy_strings(interface_strategies, container_type) == [ + wrapper_strategy, + wrapper_strategy, + ] + assert ( + _strategy_string(pure_functions_strategy, Callable[[int, str], bool]) + == 'functions(like=lambda *args, **kwargs: ,' + ' returns=booleans(), pure=True)' + ) + assert ( + _strategy_string(pure_functions_strategy, Callable[[], None]) + == 'functions(like=lambda: None, returns=none(), pure=True)' + ) + assert ( + _strategy_string(type_var_strategy, _ValueType) + == "shared(sampled_from([, ," + " , , , ])," + " key='typevar=~_ValueType').flatmap(from_type).filter(lambda" + ' inner: inner == inner)' + ) + + def test_interface_strategies() -> None: """Check that ancestor interfaces get resolved to the concrete container.""" container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 @@ -242,7 +321,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 ( From 02a4f508ff5613e45cfcdd267f76e31800563381 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:02:00 +0530 Subject: [PATCH 03/20] hypothesis: replace `hypothesis` mutation with context manager. --- returns/contrib/hypothesis/laws.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index fe8574bba..4e4e2a406 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -164,14 +164,8 @@ def factory(thing) -> st.SearchStrategy: pure=True, ) - used = types._global_type_lookup[Callable] # type: ignore[index] # noqa: SLF001 - st.register_type_strategy(Callable, factory) # type: ignore[arg-type] - - try: + with strategies_for_types({Callable: factory}): # type: ignore[dict-item] 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 @@ -192,14 +186,8 @@ def factory(thing): lambda inner: inner == inner, # noqa: PLR0124, WPS312 ) - used = types._global_type_lookup.pop(TypeVar) # noqa: SLF001 - st.register_type_strategy(TypeVar, factory) - - try: + with strategies_for_types({TypeVar: factory}): yield - finally: - types._global_type_lookup.pop(TypeVar) # noqa: SLF001 - st.register_type_strategy(TypeVar, used) @contextmanager From 9f5ee668ae7eb8cfaf88b98f20bcc61015df4afb Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:07:33 +0530 Subject: [PATCH 04/20] hypothesis: extract functions for strategy factories. --- returns/contrib/hypothesis/laws.py | 54 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 4e4e2a406..9df1db135 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -141,6 +141,23 @@ def register_container( yield +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, + ) + + @contextmanager def pure_functions() -> Iterator[None]: """ @@ -148,26 +165,21 @@ def pure_functions() -> Iterator[None]: 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, - ) - - with strategies_for_types({Callable: factory}): # type: ignore[dict-item] + with strategies_for_types({Callable: pure_functions_factory}): # type: ignore[dict-item] yield +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. + """ + return types.resolve_TypeVar(thing).filter( # type: ignore[no-any-return] + lambda inner: inner == inner, # noqa: PLR0124, WPS312 + ) + + @contextmanager def type_vars() -> Iterator[None]: """ @@ -180,13 +192,7 @@ def type_vars() -> Iterator[None]: for example, ``nan`` does not work for us """ - - def factory(thing): - return types.resolve_TypeVar(thing).filter( - lambda inner: inner == inner, # noqa: PLR0124, WPS312 - ) - - with strategies_for_types({TypeVar: factory}): + with strategies_for_types({TypeVar: type_vars_factory}): # type: ignore[dict-item] yield From 448ae84582abccfb52d2e1ab08ee3c88f0563b38 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:13:48 +0530 Subject: [PATCH 05/20] hypothesis: inline context managers for pure functions and type vars. --- returns/contrib/hypothesis/laws.py | 33 ++++-------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 9df1db135..91fc987fc 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -158,17 +158,6 @@ def pure_functions_factory(thing) -> st.SearchStrategy: ) -@contextmanager -def pure_functions() -> Iterator[None]: - """ - Context manager to resolve all ``Callable`` as pure functions. - - It is not a default in ``hypothesis``. - """ - with strategies_for_types({Callable: pure_functions_factory}): # type: ignore[dict-item] - yield - - def type_vars_factory(thing: type[object]) -> StrategyFactory: """Strategy factory for ``TypeVar``s. @@ -180,22 +169,6 @@ def type_vars_factory(thing: type[object]) -> StrategyFactory: ) -@contextmanager -def type_vars() -> Iterator[None]: - """ - Our custom ``TypeVar`` handling. - - 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 - - """ - with strategies_for_types({TypeVar: type_vars_factory}): # type: ignore[dict-item] - yield - - @contextmanager def clean_plugin_context() -> Iterator[None]: """ @@ -257,8 +230,10 @@ def _enter_hypothesis_context( container_type: type[Lawful], settings: _Settings, ) -> None: - stack.enter_context(type_vars()) - stack.enter_context(pure_functions()) + stack.enter_context(strategies_for_types({TypeVar: type_vars_factory})) # type: ignore[dict-item] + stack.enter_context( + strategies_for_types({Callable: pure_functions_factory}) # type: ignore[dict-item] + ) stack.enter_context( interface_strategies(container_type, settings=settings), ) From 96106b130171532d82402182884ed89a4ab33e79 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:19:51 +0530 Subject: [PATCH 06/20] hypothesis: add test for hypothesis state with settings. --- .../test_hypothesis/test_type_resolution.py | 91 ++++++++++++++++--- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 7fbf1f972..ed2207e89 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -191,8 +191,12 @@ def test_register_container_with_setting() -> None: _ValueType = TypeVar('_ValueType') -def test_enter_hypothesis_context() -> None: # noqa: WPS210 - """Check that strategies are registered correctly.""" +def test_hypothesis_state_outside_context() -> None: # noqa: WPS210 + """Check values of strategies before we register them. + + This is mostly useful as a baseline to compare the the values when we do + register them. + """ container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 # NOTE: There is a type error because `Callable` is a # special form, not a type. @@ -203,19 +207,6 @@ def test_enter_hypothesis_context() -> None: # noqa: WPS210 pure_functions_strategy_outside = look_up_strategy(callable_type) type_var_strategy_outside = look_up_strategy(TypeVar) - with ExitStack() as stack: - _enter_hypothesis_context( - stack, - container_type, - settings=_Settings( - settings_kwargs={}, use_init=False, container_strategy=None - ), - ) - container_strategy = look_up_strategy(container_type) - interface_strategies = _interface_factories(container_type) - pure_functions_strategy = look_up_strategy(callable_type) - type_var_strategy = look_up_strategy(TypeVar) - assert ( _strategy_string(container_strategy_outside, container_type) == 'None' ) @@ -235,6 +226,28 @@ def test_enter_hypothesis_context() -> None: # noqa: WPS210 " , , , ])," " key='typevar=~_ValueType').flatmap(from_type)" ) + + +def test_hypothesis_state_inside_context() -> None: # noqa: WPS210 + """Check that strategies are registered correctly.""" + 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 ExitStack() as stack: + _enter_hypothesis_context( + stack, + container_type, + settings=_Settings( + settings_kwargs={}, use_init=False, container_strategy=None + ), + ) + container_strategy = look_up_strategy(container_type) + interface_strategies = _interface_factories(container_type) + pure_functions_strategy = look_up_strategy(callable_type) + type_var_strategy = look_up_strategy(TypeVar) + wrapper_strategy = ( "builds(from_value, shared(sampled_from([," " , , , ," @@ -265,6 +278,54 @@ def test_enter_hypothesis_context() -> None: # noqa: WPS210 ) +def test_hypothesis_state_with_setting() -> None: # noqa: WPS210 + """Check that we prefer the strategies in settings.""" + 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 ExitStack() as stack: + _enter_hypothesis_context( + stack, + container_type, + settings=_Settings( + settings_kwargs={}, + use_init=False, + container_strategy=st.builds(container_type, st.integers()), + ), + ) + container_strategy = look_up_strategy(container_type) + interface_strategies = _interface_factories(container_type) + pure_functions_strategy = look_up_strategy(callable_type) + type_var_strategy = look_up_strategy(TypeVar) + + wrapper_strategy = 'builds(_Wrapper, integers())' + assert ( + _strategy_string(container_strategy, container_type) == wrapper_strategy + ) + assert _strategy_strings(interface_strategies, container_type) == [ + wrapper_strategy, + wrapper_strategy, + ] + assert ( + _strategy_string(pure_functions_strategy, Callable[[int, str], bool]) + == 'functions(like=lambda *args, **kwargs: ,' + ' returns=booleans(), pure=True)' + ) + assert ( + _strategy_string(pure_functions_strategy, Callable[[], None]) + == 'functions(like=lambda: None, returns=none(), pure=True)' + ) + assert ( + _strategy_string(type_var_strategy, _ValueType) + == "shared(sampled_from([, ," + " , , , ])," + " key='typevar=~_ValueType').flatmap(from_type).filter(lambda" + ' inner: inner == inner)' + ) + + def test_interface_strategies() -> None: """Check that ancestor interfaces get resolved to the concrete container.""" container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 From f476ef8defdac8ceca59865796b294bd448c5ffc Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:23:09 +0530 Subject: [PATCH 07/20] hypothesis: inline context managers for container and interfaces. --- returns/contrib/hypothesis/laws.py | 47 ++----- .../test_hypothesis/test_type_resolution.py | 123 +----------------- 2 files changed, 11 insertions(+), 159 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 91fc987fc..da97d6aa6 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -1,6 +1,6 @@ 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, TypeVar, final, overload @@ -105,42 +105,6 @@ 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 container_type.laws() - } - 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 - - def pure_functions_factory(thing) -> st.SearchStrategy: """Factory to create pure functions.""" like = ( @@ -235,10 +199,15 @@ def _enter_hypothesis_context( strategies_for_types({Callable: pure_functions_factory}) # type: ignore[dict-item] ) stack.enter_context( - interface_strategies(container_type, settings=settings), + strategies_for_types({ + interface: _strategy_for_container(container_type, settings) + for interface in container_type.laws() + }), ) stack.enter_context( - register_container(container_type, settings=settings), + strategies_for_types({ + container_type: _strategy_for_container(container_type, settings) + }), ) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index ed2207e89..6c7959fbf 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -1,6 +1,6 @@ from collections.abc import Callable, Sequence from contextlib import ExitStack -from typing import Any, Final, TypeVar +from typing import Any, TypeVar import pytest from hypothesis import given @@ -19,21 +19,18 @@ from returns.contrib.hypothesis.laws import ( _enter_hypothesis_context, # noqa: PLC2701 _Settings, # noqa: PLC2701 - interface_strategies, - register_container, -) + ) from returns.contrib.hypothesis.type_resolver import ( StrategyFactory, apply_strategy, look_up_strategy, - strategies_for_types, ) from returns.future import Future, FutureResult 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]] = ( @@ -116,78 +113,6 @@ 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_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) - - assert ( - _strategy_string(strategy_factory, container_type) - == DEFAULT_RESULT_STRATEGY - ) - - -def test_register_container_with_setting() -> None: - """Check that we prefer a strategy given in settings.""" - container_type = Result - - 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') @@ -326,48 +251,6 @@ def test_hypothesis_state_with_setting() -> None: # noqa: WPS210 ) -def test_interface_strategies() -> None: - """Check that ancestor interfaces get resolved to the concrete container.""" - container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 - - with interface_strategies( - container_type, - settings=_Settings( - settings_kwargs={}, use_init=False, container_strategy=None - ), - ): - strategy_factories_inside = _interface_factories(container_type) - - assert _strategy_strings(strategy_factories_inside, container_type) == [ - "builds(from_value, shared(sampled_from([," - " , , , ," - " ]), key='typevar=~_FirstType').flatmap(from_type))", - "builds(from_value, shared(sampled_from([," - " , , , ," - " ]), key='typevar=~_FirstType').flatmap(from_type))", - ] - - -def test_interface_strategies_with_settings() -> None: - """Check that we prefer the strategy in the settings.""" - container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 - - with interface_strategies( - container_type, - settings=_Settings( - settings_kwargs={}, - use_init=False, - container_strategy=st.builds(container_type, st.integers()), - ), - ): - strategy_factories_inside = _interface_factories(container_type) - - assert _strategy_strings(strategy_factories_inside, container_type) == [ - 'builds(_Wrapper, integers())', - 'builds(_Wrapper, integers())', - ] - - def _interface_factories(type_: type[Lawful]) -> list[StrategyFactory | None]: return [look_up_strategy(interface) for interface in type_.laws()] From 2957d5e8db09b4e46cb3729a6847069a58a7b9a6 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:27:26 +0530 Subject: [PATCH 08/20] hypothesis: merge the context mappings. --- returns/contrib/hypothesis/laws.py | 20 ++++++++----------- .../test_hypothesis/test_type_resolution.py | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index da97d6aa6..0838d59f2 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -194,20 +194,16 @@ def _enter_hypothesis_context( container_type: type[Lawful], settings: _Settings, ) -> None: - stack.enter_context(strategies_for_types({TypeVar: type_vars_factory})) # type: ignore[dict-item] - stack.enter_context( - strategies_for_types({Callable: pure_functions_factory}) # type: ignore[dict-item] - ) - stack.enter_context( - strategies_for_types({ - interface: _strategy_for_container(container_type, settings) - for interface in container_type.laws() - }), - ) stack.enter_context( strategies_for_types({ - container_type: _strategy_for_container(container_type, settings) - }), + TypeVar: type_vars_factory, # type: ignore[dict-item] + Callable: pure_functions_factory, # type: ignore[dict-item] + **{ + interface: _strategy_for_container(container_type, settings) + for interface in container_type.laws() + }, + container_type: _strategy_for_container(container_type, settings), + }) ) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 6c7959fbf..d67236da5 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -19,7 +19,7 @@ from returns.contrib.hypothesis.laws import ( _enter_hypothesis_context, # noqa: PLC2701 _Settings, # noqa: PLC2701 - ) +) from returns.contrib.hypothesis.type_resolver import ( StrategyFactory, apply_strategy, From e0932479c9f07c26bce3eeb27f6882f1cc58d9e0 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:35:57 +0530 Subject: [PATCH 09/20] hypothesis: rearrange functions in calling order. This package seems to put callers above callees. --- returns/contrib/hypothesis/laws.py | 76 +++++++++++++++--------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 0838d59f2..0ccd95cf4 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -163,14 +163,40 @@ def _clean_caches() -> None: st.from_type.__clear_cache() # type: ignore[attr-defined] # noqa: SLF001 -def _strategy_for_container( +def _create_law_test_case( container_type: type[Lawful], + interface: type[Lawful], + law: Law, + *, settings: _Settings, -) -> StrategyFactory: - return ( - strategy_from_container(container_type, use_init=settings.use_init) - if settings.container_strategy is None - else settings.container_strategy +) -> None: + test_function = given(st.data())( + hypothesis_settings(**settings.settings_kwargs)( + _run_law(container_type, law, settings=settings), + ), + ) + + called_from = inspect.stack()[2] + module = inspect.getmodule(called_from[0]) + + template = 'test_{container}_{interface}_{name}' + test_function.__name__ = template.format( # noqa: WPS125 + container=container_type.__qualname__.lower(), + interface=interface.__qualname__.lower(), + name=law.name, + ) + + setattr( + module, + test_function.__name__, + pytest.mark.filterwarnings( + # We ignore multiple warnings about unused coroutines and stuff: + 'ignore::pytest.PytestUnraisableExceptionWarning', + )( + # We mark all tests with `returns_lawful` marker, + # so users can easily skip them if needed. + pytest.mark.returns_lawful(test_function), + ), ) @@ -207,38 +233,12 @@ def _enter_hypothesis_context( ) -def _create_law_test_case( +def _strategy_for_container( container_type: type[Lawful], - interface: type[Lawful], - law: Law, - *, settings: _Settings, -) -> None: - test_function = given(st.data())( - hypothesis_settings(**settings.settings_kwargs)( - _run_law(container_type, law, settings=settings), - ), - ) - - called_from = inspect.stack()[2] - module = inspect.getmodule(called_from[0]) - - template = 'test_{container}_{interface}_{name}' - test_function.__name__ = template.format( # noqa: WPS125 - container=container_type.__qualname__.lower(), - interface=interface.__qualname__.lower(), - name=law.name, - ) - - setattr( - module, - test_function.__name__, - pytest.mark.filterwarnings( - # We ignore multiple warnings about unused coroutines and stuff: - 'ignore::pytest.PytestUnraisableExceptionWarning', - )( - # We mark all tests with `returns_lawful` marker, - # so users can easily skip them if needed. - pytest.mark.returns_lawful(test_function), - ), +) -> StrategyFactory: + return ( + strategy_from_container(container_type, use_init=settings.use_init) + if settings.container_strategy is None + else settings.container_strategy ) From 9e7d6096a6055b1acdbd34a2c75f0fb1e9c91818 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 16:48:48 +0530 Subject: [PATCH 10/20] hypothesis: extract a pure function for types-to-strategies. Phew. Finally. --- returns/contrib/hypothesis/laws.py | 33 ++--- .../test_hypothesis/test_type_resolution.py | 114 ++++++------------ 2 files changed, 54 insertions(+), 93 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 0ccd95cf4..255bcda14 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -1,6 +1,6 @@ import dataclasses import inspect -from collections.abc import Callable, Iterator +from collections.abc import Callable, Iterator, Mapping from contextlib import ExitStack, contextmanager from typing import Any, TypeVar, final, overload @@ -209,28 +209,29 @@ def _run_law( def factory(source: st.DataObject) -> None: with ExitStack() as stack: stack.enter_context(clean_plugin_context()) - _enter_hypothesis_context(stack, container_type, settings) + stack.enter_context( + strategies_for_types( + _types_to_strategies(container_type, settings) + ) + ) source.draw(st.builds(law.definition)) return factory -def _enter_hypothesis_context( - stack: ExitStack, +def _types_to_strategies( container_type: type[Lawful], settings: _Settings, -) -> None: - stack.enter_context( - strategies_for_types({ - TypeVar: type_vars_factory, # type: ignore[dict-item] - Callable: pure_functions_factory, # type: ignore[dict-item] - **{ - interface: _strategy_for_container(container_type, settings) - for interface in container_type.laws() - }, - container_type: _strategy_for_container(container_type, settings), - }) - ) +) -> Mapping[type[object], StrategyFactory]: + return { + TypeVar: type_vars_factory, # type: ignore[dict-item] + Callable: pure_functions_factory, # type: ignore[dict-item] + **{ + interface: _strategy_for_container(container_type, settings) + for interface in container_type.laws() + }, + container_type: _strategy_for_container(container_type, settings), + } def _strategy_for_container( diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index d67236da5..c4a3412eb 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -1,5 +1,4 @@ from collections.abc import Callable, Sequence -from contextlib import ExitStack from typing import Any, TypeVar import pytest @@ -17,8 +16,8 @@ RequiresContextResultE, ) from returns.contrib.hypothesis.laws import ( - _enter_hypothesis_context, # noqa: PLC2701 _Settings, # noqa: PLC2701 + _types_to_strategies, # noqa: PLC2701 ) from returns.contrib.hypothesis.type_resolver import ( StrategyFactory, @@ -116,86 +115,49 @@ def test_custom_readerresult_types_resolve( _ValueType = TypeVar('_ValueType') -def test_hypothesis_state_outside_context() -> None: # noqa: WPS210 - """Check values of strategies before we register them. - - This is mostly useful as a baseline to compare the the values when we do - register them. - """ +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] - container_strategy_outside = look_up_strategy(container_type) - interface_strategies_outside = _interface_factories(container_type) - pure_functions_strategy_outside = look_up_strategy(callable_type) - type_var_strategy_outside = look_up_strategy(TypeVar) - - assert ( - _strategy_string(container_strategy_outside, container_type) == 'None' - ) - assert _strategy_strings(interface_strategies_outside, container_type) == [ - 'None', - 'None', - ] - assert ( - _strategy_string( - pure_functions_strategy_outside, Callable[[int, str], bool] - ) - == 'functions(like=lambda *a, **k: None, returns=booleans())' - ) - assert ( - _strategy_string(type_var_strategy_outside, _ValueType) - == "shared(sampled_from([, ," - " , , , ])," - " key='typevar=~_ValueType').flatmap(from_type)" + result = _types_to_strategies( + container_type, + _Settings( + settings_kwargs={}, + use_init=False, + container_strategy=None, + ), ) - -def test_hypothesis_state_inside_context() -> None: # noqa: WPS210 - """Check that strategies are registered correctly.""" - 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 ExitStack() as stack: - _enter_hypothesis_context( - stack, - container_type, - settings=_Settings( - settings_kwargs={}, use_init=False, container_strategy=None - ), - ) - container_strategy = look_up_strategy(container_type) - interface_strategies = _interface_factories(container_type) - pure_functions_strategy = look_up_strategy(callable_type) - type_var_strategy = look_up_strategy(TypeVar) - wrapper_strategy = ( "builds(from_value, shared(sampled_from([," " , , , ," " ]), key='typevar=~_FirstType').flatmap(from_type))" ) assert ( - _strategy_string(container_strategy, container_type) == wrapper_strategy + _strategy_string(result[container_type], container_type) + == wrapper_strategy ) - assert _strategy_strings(interface_strategies, container_type) == [ + assert _strategy_strings( + [result[interface] for interface in container_type.laws()], + container_type, + ) == [ wrapper_strategy, wrapper_strategy, ] assert ( - _strategy_string(pure_functions_strategy, Callable[[int, str], bool]) + _strategy_string(result[callable_type], Callable[[int, str], bool]) == 'functions(like=lambda *args, **kwargs: ,' ' returns=booleans(), pure=True)' ) assert ( - _strategy_string(pure_functions_strategy, Callable[[], None]) + _strategy_string(result[callable_type], Callable[[], None]) == 'functions(like=lambda: None, returns=none(), pure=True)' ) assert ( - _strategy_string(type_var_strategy, _ValueType) + _strategy_string(result[TypeVar], _ValueType) == "shared(sampled_from([, ," " , , , ])," " key='typevar=~_ValueType').flatmap(from_type).filter(lambda" @@ -203,47 +165,45 @@ def test_hypothesis_state_inside_context() -> None: # noqa: WPS210 ) -def test_hypothesis_state_with_setting() -> None: # noqa: WPS210 +def test_types_to_strategies_overrides() -> None: # noqa: WPS210 """Check that we prefer the strategies in settings.""" 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 ExitStack() as stack: - _enter_hypothesis_context( - stack, - container_type, - settings=_Settings( - settings_kwargs={}, - use_init=False, - container_strategy=st.builds(container_type, st.integers()), - ), - ) - container_strategy = look_up_strategy(container_type) - interface_strategies = _interface_factories(container_type) - pure_functions_strategy = look_up_strategy(callable_type) - type_var_strategy = look_up_strategy(TypeVar) + result = _types_to_strategies( + container_type, + _Settings( + settings_kwargs={}, + use_init=False, + container_strategy=st.builds(container_type, st.integers()), + ), + ) wrapper_strategy = 'builds(_Wrapper, integers())' assert ( - _strategy_string(container_strategy, container_type) == wrapper_strategy + _strategy_string(result[container_type], container_type) + == wrapper_strategy ) - assert _strategy_strings(interface_strategies, container_type) == [ + assert _strategy_strings( + [result[interface] for interface in container_type.laws()], + container_type, + ) == [ wrapper_strategy, wrapper_strategy, ] assert ( - _strategy_string(pure_functions_strategy, Callable[[int, str], bool]) + _strategy_string(result[callable_type], Callable[[int, str], bool]) == 'functions(like=lambda *args, **kwargs: ,' ' returns=booleans(), pure=True)' ) assert ( - _strategy_string(pure_functions_strategy, Callable[[], None]) + _strategy_string(result[callable_type], Callable[[], None]) == 'functions(like=lambda: None, returns=none(), pure=True)' ) assert ( - _strategy_string(type_var_strategy, _ValueType) + _strategy_string(result[TypeVar], _ValueType) == "shared(sampled_from([, ," " , , , ])," " key='typevar=~_ValueType').flatmap(from_type).filter(lambda" From cfc58fc663515cb89ac74567febd2fc8918a3ef2 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 18:46:36 +0530 Subject: [PATCH 11/20] hypothesis: split settings into default and override. --- returns/contrib/hypothesis/laws.py | 60 ++++++++++++-- .../test_hypothesis/test_type_resolution.py | 82 ++++++++++++++++--- 2 files changed, 122 insertions(+), 20 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 255bcda14..2f523ebfe 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -1,6 +1,6 @@ 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, TypeVar, final, overload @@ -9,6 +9,7 @@ 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 ( @@ -28,6 +29,19 @@ class _Settings: settings_kwargs: dict[str, Any] use_init: bool container_strategy: StrategyFactory | None + other_strategies: dict[type[object], StrategyFactory] = dataclasses.field( + default_factory=dict + ) + + def __or__(self, other: Self) -> Self: + 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, + other_strategies=self.other_strategies | other.other_strategies, + ) def __post_init__(self) -> None: """Check that the settings are mutually compatible.""" @@ -38,6 +52,23 @@ def __post_init__(self) -> None: ) +def _default_settings(container_type: type[Lawful]) -> _Settings: + """Return default settings for creating law tests. + + We use special strategies for `TypeVar` and `Callable` by default, but + they can be overriden by the user if needed. + """ + return _Settings( + settings_kwargs={}, + use_init=False, + container_strategy=None, + other_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]], @@ -222,15 +253,26 @@ def factory(source: st.DataObject) -> None: def _types_to_strategies( container_type: type[Lawful], settings: _Settings, -) -> Mapping[type[object], StrategyFactory]: +) -> dict[type[object], StrategyFactory]: + """Return a mapping from type to `hypothesis` strategy. + + We override the default settings with the user-provided `settings`. + """ + merged_settings = _default_settings(container_type) | settings + return merged_settings.other_strategies | _container_mapping( + container_type, merged_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 { - TypeVar: type_vars_factory, # type: ignore[dict-item] - Callable: pure_functions_factory, # type: ignore[dict-item] - **{ - interface: _strategy_for_container(container_type, settings) - for interface in container_type.laws() - }, - container_type: _strategy_for_container(container_type, settings), + **dict.fromkeys(container_type.laws(), container_strategy), + container_type: container_strategy, } diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index c4a3412eb..aa63a1524 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -25,6 +25,7 @@ look_up_strategy, ) 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 @@ -112,6 +113,64 @@ def test_custom_readerresult_types_resolve( assert isinstance(real_result.failure(), str) +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(), + other_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), + other_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), + other_strategies={ + int: st.integers(max_value=30), + bool: st.booleans(), + str: st.text('abc'), + }, + ) + + +def test_merge_use_init() -> None: + """Check that `use_init` can be set to `True` by users. + + 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, + other_strategies={}, + ) + settings2 = _Settings( + settings_kwargs={}, + use_init=True, + container_strategy=None, + other_strategies={}, + ) + + result = settings1 | settings2 + + assert result == _Settings( + settings_kwargs={}, + use_init=True, + container_strategy=None, + other_strategies={}, + ) + + _ValueType = TypeVar('_ValueType') @@ -166,7 +225,7 @@ def test_types_to_strategies_default() -> None: # noqa: WPS210 def test_types_to_strategies_overrides() -> None: # noqa: WPS210 - """Check that we prefer the strategies in settings.""" + """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. @@ -178,6 +237,14 @@ def test_types_to_strategies_overrides() -> None: # noqa: WPS210 settings_kwargs={}, use_init=False, container_strategy=st.builds(container_type, st.integers()), + other_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()), + }, ), ) @@ -195,20 +262,13 @@ def test_types_to_strategies_overrides() -> None: # noqa: WPS210 ] assert ( _strategy_string(result[callable_type], Callable[[int, str], bool]) - == 'functions(like=lambda *args, **kwargs: ,' - ' returns=booleans(), pure=True)' + == 'functions(returns=booleans())' ) 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)' + == 'functions(returns=booleans())' ) + assert _strategy_string(result[TypeVar], _ValueType) == 'text()' def _interface_factories(type_: type[Lawful]) -> list[StrategyFactory | None]: From bdccc04e605f160eb8f5d541ffdcf33074d0f0ba Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Tue, 11 Mar 2025 19:17:39 +0530 Subject: [PATCH 12/20] hypothesis: accept other strategies in `check_all_laws`. --- returns/contrib/hypothesis/laws.py | 9 +++++---- .../test_contrib/test_hypothesis/test_type_resolution.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 2f523ebfe..d2acfcd5b 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -29,9 +29,7 @@ class _Settings: settings_kwargs: dict[str, Any] use_init: bool container_strategy: StrategyFactory | None - other_strategies: dict[type[object], StrategyFactory] = dataclasses.field( - default_factory=dict - ) + other_strategies: dict[type[object], StrategyFactory] def __or__(self, other: Self) -> Self: return _Settings( @@ -73,8 +71,9 @@ def _default_settings(container_type: type[Lawful]) -> _Settings: 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, + other_strategies: dict[type[object], StrategyFactory] | None = None, ) -> None: ... @@ -93,6 +92,7 @@ def check_all_laws( settings_kwargs: dict[str, Any] | None = None, use_init: bool = False, container_strategy: StrategyFactory[Example_co] | None = None, + other_strategies: dict[type[object], StrategyFactory] | None = None, ) -> None: """ Function to check all defined mathematical laws in a specified container. @@ -124,6 +124,7 @@ def check_all_laws( settings_kwargs or {}, use_init, container_strategy, + other_strategies=other_strategies or {}, ) for interface, laws in container_type.laws().items(): diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index aa63a1524..368a9dba9 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -187,6 +187,7 @@ def test_types_to_strategies_default() -> None: # noqa: WPS210 settings_kwargs={}, use_init=False, container_strategy=None, + other_strategies={}, ), ) From d57d6b096dfa4faeffc7f2a946da0575f8de6c4a Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Wed, 12 Mar 2025 13:27:31 +0530 Subject: [PATCH 13/20] hypothesis: add end-to-end test for registering callable. --- .../test_custom_strategy_for_callable.py | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/test_contrib/test_hypothesis/test_laws/test_custom_strategy_for_callable.py 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..19dfc9567 --- /dev/null +++ b/tests/test_contrib/test_hypothesis/test_laws/test_custom_strategy_for_callable.py @@ -0,0 +1,160 @@ +""" +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 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 + + +other_strategies: dict[type[object], StrategyFactory] = { + Callable: _callable_factory # type: ignore[dict-item] +} + +check_all_laws( + _Wrapper, + container_strategy=st.builds(_Wrapper, st.integers()), + other_strategies=other_strategies, +) From db141baf1d008fad5522c632aadd61f06f7e11a1 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Wed, 12 Mar 2025 15:08:48 +0530 Subject: [PATCH 14/20] hypothesis: merge settings in `check_all_laws`. This way, the deeper code doesn't need to know about defaults and overrides. It just deals with `Settings`. --- returns/contrib/hypothesis/laws.py | 46 +++++++++++-------- .../test_hypothesis/test_type_resolution.py | 8 +--- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index d2acfcd5b..486b80735 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -31,7 +31,16 @@ class _Settings: container_strategy: StrategyFactory | None other_strategies: dict[type[object], StrategyFactory] + def __post_init__(self) -> None: + """Check that the settings are mutually compatible.""" + if self.use_init and self.container_strategy is not None: + raise AssertionError( + 'Expected only one of `use_init` and' + ' `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, @@ -41,20 +50,23 @@ def __or__(self, other: Self) -> Self: other_strategies=self.other_strategies | other.other_strategies, ) - def __post_init__(self) -> None: - """Check that the settings are mutually compatible.""" - if self.use_init and self.container_strategy is not None: - raise AssertionError( - 'Expected only one of `use_init` and' - ' `container_strategy` to be truthy' - ) - -def _default_settings(container_type: type[Lawful]) -> _Settings: +def default_settings(container_type: type[Lawful]) -> _Settings: """Return default settings for creating law tests. - We use special strategies for `TypeVar` and `Callable` by default, but - they can be overriden by the user if needed. + We use some special strategies by default, but + they can be overriden 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={}, @@ -120,7 +132,7 @@ 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, @@ -255,13 +267,9 @@ def _types_to_strategies( container_type: type[Lawful], settings: _Settings, ) -> dict[type[object], StrategyFactory]: - """Return a mapping from type to `hypothesis` strategy. - - We override the default settings with the user-provided `settings`. - """ - merged_settings = _default_settings(container_type) | settings - return merged_settings.other_strategies | _container_mapping( - container_type, merged_settings + """Return a mapping from type to `hypothesis` strategy.""" + return settings.other_strategies | _container_mapping( + container_type, settings ) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 368a9dba9..9c61e0a1f 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -18,6 +18,7 @@ from returns.contrib.hypothesis.laws import ( _Settings, # noqa: PLC2701 _types_to_strategies, # noqa: PLC2701 + default_settings, ) from returns.contrib.hypothesis.type_resolver import ( StrategyFactory, @@ -183,12 +184,7 @@ def test_types_to_strategies_default() -> None: # noqa: WPS210 result = _types_to_strategies( container_type, - _Settings( - settings_kwargs={}, - use_init=False, - container_strategy=None, - other_strategies={}, - ), + default_settings(container_type), ) wrapper_strategy = ( From 31669c2fc39ed0f1ad3341eca9b70330a3c84bca Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Wed, 12 Mar 2025 15:29:39 +0530 Subject: [PATCH 15/20] hypothesis: make `Settings` public and document it inline. --- returns/contrib/hypothesis/laws.py | 36 +++++++++++++------ .../test_hypothesis/test_type_resolution.py | 16 ++++----- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 486b80735..447f7d435 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -23,12 +23,26 @@ @final @dataclasses.dataclass(frozen=True) -class _Settings: - """Settings that we provide to an end user.""" +class Settings: + """Settings for the law tests. + 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 other types. This can be useful for + #: overriding ``TypeVar``, ``Callable``, etc. in case you use certain + #: types that ``hypothesis`` is unable to find. other_strategies: dict[type[object], StrategyFactory] def __post_init__(self) -> None: @@ -41,7 +55,7 @@ def __post_init__(self) -> None: def __or__(self, other: Self) -> Self: """Merge the two settings, preferring values from `other`.""" - return _Settings( + return Settings( settings_kwargs=self.settings_kwargs | other.settings_kwargs, use_init=self.use_init | other.use_init, container_strategy=self.container_strategy @@ -51,7 +65,7 @@ def __or__(self, other: Self) -> Self: ) -def default_settings(container_type: type[Lawful]) -> _Settings: +def default_settings(container_type: type[Lawful]) -> Settings: """Return default settings for creating law tests. We use some special strategies by default, but @@ -68,7 +82,7 @@ def default_settings(container_type: type[Lawful]) -> _Settings: `collections.abc.Callable`. So, this is the type we should register with `hypothesis`. """ - return _Settings( + return Settings( settings_kwargs={}, use_init=False, container_strategy=None, @@ -132,7 +146,7 @@ def check_all_laws( - https://mmhaskell.com/blog/2017/3/13/obey-the-type-laws """ - settings = default_settings(container_type) | _Settings( + settings = default_settings(container_type) | Settings( settings_kwargs or {}, use_init, container_strategy, @@ -212,7 +226,7 @@ def _create_law_test_case( interface: type[Lawful], law: Law, *, - settings: _Settings, + settings: Settings, ) -> None: test_function = given(st.data())( hypothesis_settings(**settings.settings_kwargs)( @@ -248,7 +262,7 @@ def _run_law( container_type: type[Lawful], law: Law, *, - settings: _Settings, + settings: Settings, ) -> Callable[[st.DataObject], None]: def factory(source: st.DataObject) -> None: with ExitStack() as stack: @@ -265,7 +279,7 @@ def factory(source: st.DataObject) -> None: def _types_to_strategies( container_type: type[Lawful], - settings: _Settings, + settings: Settings, ) -> dict[type[object], StrategyFactory]: """Return a mapping from type to `hypothesis` strategy.""" return settings.other_strategies | _container_mapping( @@ -275,7 +289,7 @@ def _types_to_strategies( def _container_mapping( container_type: type[Lawful], - settings: _Settings, + settings: Settings, ) -> dict[type[object], StrategyFactory]: """Map `container_type` and its interfaces to the container strategy.""" container_strategy = _strategy_for_container(container_type, settings) @@ -287,7 +301,7 @@ def _container_mapping( def _strategy_for_container( container_type: type[Lawful], - settings: _Settings, + settings: Settings, ) -> StrategyFactory: return ( strategy_from_container(container_type, use_init=settings.use_init) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 9c61e0a1f..cb36f7d50 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -16,7 +16,7 @@ RequiresContextResultE, ) from returns.contrib.hypothesis.laws import ( - _Settings, # noqa: PLC2701 + Settings, _types_to_strategies, # noqa: PLC2701 default_settings, ) @@ -116,13 +116,13 @@ def test_custom_readerresult_types_resolve( def test_merge_settings() -> None: """Check that each part of the settings can be overridden by users.""" - settings1 = _Settings( + settings1 = Settings( settings_kwargs={'a': 1, 'b': 2}, use_init=False, container_strategy=st.integers(), other_strategies={int: st.integers(max_value=10), str: st.text('abc')}, ) - settings2 = _Settings( + settings2 = Settings( settings_kwargs={'a': 1, 'c': 3}, use_init=False, container_strategy=st.integers(max_value=20), @@ -131,7 +131,7 @@ def test_merge_settings() -> None: result = settings1 | settings2 - assert result == _Settings( + assert result == Settings( settings_kwargs={'a': 1, 'b': 2, 'c': 3}, use_init=False, container_strategy=st.integers(max_value=20), @@ -149,13 +149,13 @@ def test_merge_use_init() -> None: 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( + settings1 = Settings( settings_kwargs={}, use_init=False, container_strategy=None, other_strategies={}, ) - settings2 = _Settings( + settings2 = Settings( settings_kwargs={}, use_init=True, container_strategy=None, @@ -164,7 +164,7 @@ def test_merge_use_init() -> None: result = settings1 | settings2 - assert result == _Settings( + assert result == Settings( settings_kwargs={}, use_init=True, container_strategy=None, @@ -230,7 +230,7 @@ def test_types_to_strategies_overrides() -> None: # noqa: WPS210 result = _types_to_strategies( container_type, - _Settings( + Settings( settings_kwargs={}, use_init=False, container_strategy=st.builds(container_type, st.integers()), From f465cc8d618fd13ba54159f3c5137ac6466212c6 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Wed, 12 Mar 2025 15:40:13 +0530 Subject: [PATCH 16/20] hypothesis: add type test. --- .../test_laws/test_check_all_laws.yml | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) 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..c64ffa4d4 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]] = ..., other_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()), other_strategies={int: st.integers()}) + check_all_laws(Result, other_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]] = ..., other_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 From 17c39eea38538039284a45c5511c10af6066e94e Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Wed, 12 Mar 2025 16:56:26 +0530 Subject: [PATCH 17/20] hypothesis: add docs. --- CHANGELOG.md | 2 + docs/pages/contrib/hypothesis_plugins.rst | 124 +++++++++++------- .../test_custom_strategy_for_callable.py | 3 +- 3 files changed, 80 insertions(+), 49 deletions(-) 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..33492d035 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,64 @@ 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()), + other_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/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 index 19dfc9567..c0dca8110 100644 --- 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 @@ -6,7 +6,8 @@ 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 that `KindN` is just emulating HKTs. +and `hypothesis` doesn't know about the fact that `KindN` is just emulating +HKTs. """ from abc import abstractmethod From ab6a3628c5430da810a0ce6c9f09d7d06be59cdc Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Wed, 12 Mar 2025 17:14:09 +0530 Subject: [PATCH 18/20] hypothesis: fix typo. --- returns/contrib/hypothesis/laws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 447f7d435..90299b1bb 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -69,7 +69,7 @@ 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 overriden by the user if needed: + 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. From 40da087f2edb4129fef302e60886c51bde340e03 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Wed, 12 Mar 2025 19:28:51 +0530 Subject: [PATCH 19/20] hypothesis: rename attribute and parameter for type strategies. --- docs/pages/contrib/hypothesis_plugins.rst | 2 +- returns/contrib/hypothesis/laws.py | 25 +++++++++++-------- .../test_custom_strategy_for_callable.py | 4 +-- .../test_hypothesis/test_type_resolution.py | 14 +++++------ .../test_laws/test_check_all_laws.yml | 8 +++--- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/docs/pages/contrib/hypothesis_plugins.rst b/docs/pages/contrib/hypothesis_plugins.rst index 33492d035..b459e1aee 100644 --- a/docs/pages/contrib/hypothesis_plugins.rst +++ b/docs/pages/contrib/hypothesis_plugins.rst @@ -146,7 +146,7 @@ You can also register strategies for other types: check_all_laws( Number, container_strategy=st.builds(Number, st.integers()), - other_strategies={Foo: st.builds(Foo, st.text())}, + type_strategies={Foo: st.builds(Foo, st.text())}, ) These custom strategies will be used only when running the tests generated by diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 90299b1bb..feb59fec7 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -26,6 +26,10 @@ 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`. """ @@ -40,10 +44,11 @@ class Settings: #: of a container using: #: :func:`returns.contrib.hypothesis.containers.strategy_from_container`. container_strategy: StrategyFactory | None - #: Strategies for generating values of other types. This can be useful for - #: overriding ``TypeVar``, ``Callable``, etc. in case you use certain - #: types that ``hypothesis`` is unable to find. - other_strategies: dict[type[object], StrategyFactory] + #: 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.""" @@ -61,7 +66,7 @@ def __or__(self, other: Self) -> Self: container_strategy=self.container_strategy if other.container_strategy is None else other.container_strategy, - other_strategies=self.other_strategies | other.other_strategies, + type_strategies=self.type_strategies | other.type_strategies, ) @@ -86,7 +91,7 @@ def default_settings(container_type: type[Lawful]) -> Settings: settings_kwargs={}, use_init=False, container_strategy=None, - other_strategies={ + type_strategies={ TypeVar: type_vars_factory, # type: ignore[dict-item] Callable: pure_functions_factory, # type: ignore[dict-item] }, @@ -99,7 +104,7 @@ def check_all_laws( *, container_strategy: StrategyFactory[Example_co], settings_kwargs: dict[str, Any] | None = None, - other_strategies: dict[type[object], StrategyFactory] | None = None, + type_strategies: dict[type[object], StrategyFactory] | None = None, ) -> None: ... @@ -118,7 +123,7 @@ def check_all_laws( settings_kwargs: dict[str, Any] | None = None, use_init: bool = False, container_strategy: StrategyFactory[Example_co] | None = None, - other_strategies: dict[type[object], StrategyFactory] | None = None, + type_strategies: dict[type[object], StrategyFactory] | None = None, ) -> None: """ Function to check all defined mathematical laws in a specified container. @@ -150,7 +155,7 @@ def check_all_laws( settings_kwargs or {}, use_init, container_strategy, - other_strategies=other_strategies or {}, + type_strategies=type_strategies or {}, ) for interface, laws in container_type.laws().items(): @@ -282,7 +287,7 @@ def _types_to_strategies( settings: Settings, ) -> dict[type[object], StrategyFactory]: """Return a mapping from type to `hypothesis` strategy.""" - return settings.other_strategies | _container_mapping( + return settings.type_strategies | _container_mapping( container_type, settings ) 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 index c0dca8110..852873308 100644 --- 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 @@ -150,12 +150,12 @@ def _callable_factory(thing: type[object]) -> StrategyFactory[Callable]: raise NotImplementedError -other_strategies: dict[type[object], StrategyFactory] = { +type_strategies: dict[type[object], StrategyFactory] = { Callable: _callable_factory # type: ignore[dict-item] } check_all_laws( _Wrapper, container_strategy=st.builds(_Wrapper, st.integers()), - other_strategies=other_strategies, + 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 cb36f7d50..8b54aa61d 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -120,13 +120,13 @@ def test_merge_settings() -> None: settings_kwargs={'a': 1, 'b': 2}, use_init=False, container_strategy=st.integers(), - other_strategies={int: st.integers(max_value=10), str: st.text('abc')}, + 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), - other_strategies={int: st.integers(max_value=30), bool: st.booleans()}, + type_strategies={int: st.integers(max_value=30), bool: st.booleans()}, ) result = settings1 | settings2 @@ -135,7 +135,7 @@ def test_merge_settings() -> None: settings_kwargs={'a': 1, 'b': 2, 'c': 3}, use_init=False, container_strategy=st.integers(max_value=20), - other_strategies={ + type_strategies={ int: st.integers(max_value=30), bool: st.booleans(), str: st.text('abc'), @@ -153,13 +153,13 @@ def test_merge_use_init() -> None: settings_kwargs={}, use_init=False, container_strategy=None, - other_strategies={}, + type_strategies={}, ) settings2 = Settings( settings_kwargs={}, use_init=True, container_strategy=None, - other_strategies={}, + type_strategies={}, ) result = settings1 | settings2 @@ -168,7 +168,7 @@ def test_merge_use_init() -> None: settings_kwargs={}, use_init=True, container_strategy=None, - other_strategies={}, + type_strategies={}, ) @@ -234,7 +234,7 @@ def test_types_to_strategies_overrides() -> None: # noqa: WPS210 settings_kwargs={}, use_init=False, container_strategy=st.builds(container_type, st.integers()), - other_strategies={ + type_strategies={ TypeVar: st.text(), callable_type: st.functions(returns=st.booleans()), # This strategy does not get used, because we use 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 c64ffa4d4..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 @@ -48,7 +48,7 @@ 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]] = ..., other_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]], *, 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 @@ -64,11 +64,11 @@ 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()), other_strategies={int: st.integers()}) - check_all_laws(Result, other_strategies={int: st.integers()}) + 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]] = ..., other_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]], *, 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 From c1b78f2adbacb6ca1e6b2ba520db513bf7ab89ac Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 12 Mar 2025 20:24:39 +0300 Subject: [PATCH 20/20] Update docs/pages/contrib/hypothesis_plugins.rst --- docs/pages/contrib/hypothesis_plugins.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/pages/contrib/hypothesis_plugins.rst b/docs/pages/contrib/hypothesis_plugins.rst index b459e1aee..b979cc178 100644 --- a/docs/pages/contrib/hypothesis_plugins.rst +++ b/docs/pages/contrib/hypothesis_plugins.rst @@ -140,7 +140,6 @@ You can also register strategies for other types: .. code:: python - from hypothesis import strategies as st check_all_laws(