From 5f9c4b3b03eb9c44fe0cbac4af7f843f16bf3222 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 13:49:05 +0530 Subject: [PATCH 01/15] hypothesis: add context manager for setting strategy for type. Will use in the next commits. We could generalize this to accept a mapping of types to strategies, but I want to start with something simple, since this is complicated code. --- returns/contrib/hypothesis/type_resolver.py | 51 +++++++++++++++++++ .../test_hypothesis/test_type_resolver.py | 43 ++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 returns/contrib/hypothesis/type_resolver.py create mode 100644 tests/test_contrib/test_hypothesis/test_type_resolver.py diff --git a/returns/contrib/hypothesis/type_resolver.py b/returns/contrib/hypothesis/type_resolver.py new file mode 100644 index 00000000..93d600ef --- /dev/null +++ b/returns/contrib/hypothesis/type_resolver.py @@ -0,0 +1,51 @@ +"""Make `hypothesis` resolve types to the right strategies.""" + +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from typing import TypeAlias + +from hypothesis import strategies as st +from hypothesis.strategies._internal import types # noqa: PLC2701 + +Strategy: TypeAlias = st.SearchStrategy | Callable[[type], st.SearchStrategy] + + +@contextmanager +def strategy_for_type( + type_: type[object], strategy: Strategy +) -> Iterator[None]: + """ + Temporarily register a strategy in `hypothesis`. + + Within this context, `hypothesis` will generate data for `type_` + using `strategy`. Otherwise, it will continue to use the globally + registered strategy for `type_`. + + NOTE: This manually adds and removes strategies from an internal data + structure of `hypothesis`: `types._global_type_lookup`. This is a global + variable used for practically every example generated by `hypothesis`, so + we can easily have unintentional side-effects. We have to be very careful + when modifying it. + """ + previous_strategy = types._global_type_lookup.pop(type_, None) # noqa: SLF001 + st.register_type_strategy(type_, strategy) + + try: + yield + finally: + types._global_type_lookup.pop(type_) # noqa: SLF001 + if previous_strategy: + st.register_type_strategy(type_, previous_strategy) + else: + _clean_caches() + + +def look_up_strategy( + type_: type[object], +) -> st.SearchStrategy | Callable[[type], st.SearchStrategy] | None: + """Return the strategy used by `hypothesis`.""" + return types._global_type_lookup.get(type_) # noqa: SLF001 + + +def _clean_caches() -> None: + st.from_type.__clear_cache() # type: ignore[attr-defined] # noqa: SLF001 diff --git a/tests/test_contrib/test_hypothesis/test_type_resolver.py b/tests/test_contrib/test_hypothesis/test_type_resolver.py new file mode 100644 index 00000000..fe10d453 --- /dev/null +++ b/tests/test_contrib/test_hypothesis/test_type_resolver.py @@ -0,0 +1,43 @@ +from typing import Generic, TypeVar + +from hypothesis import strategies as st + +from returns.contrib.hypothesis.type_resolver import ( + look_up_strategy, + strategy_for_type, +) + +_ValueType = TypeVar('_ValueType') + + +class _Wrapper(Generic[_ValueType]): + _inner_value: _ValueType + + +def test_type_without_strategy() -> None: + """Check that it temporarily resolves a type that has no strategy.""" + strategy_before = look_up_strategy(_Wrapper) + + with strategy_for_type(_Wrapper, st.builds(_Wrapper, st.integers())): + strategy_inside = look_up_strategy(_Wrapper) + + strategy_after = look_up_strategy(_Wrapper) + + assert strategy_before is None + assert str(strategy_inside) == 'builds(_Wrapper, integers())' + assert strategy_after is None + + +def test_type_with_strategy() -> None: + """Check that it restores the original strategy.""" + with strategy_for_type(_Wrapper, st.builds(_Wrapper, st.integers())): + strategy_before = look_up_strategy(_Wrapper) + + with strategy_for_type(_Wrapper, st.builds(_Wrapper, st.text())): + strategy_inside = look_up_strategy(_Wrapper) + + strategy_after = look_up_strategy(_Wrapper) + + assert str(strategy_before) == 'builds(_Wrapper, integers())' + assert str(strategy_inside) == 'builds(_Wrapper, text())' + assert str(strategy_after) == 'builds(_Wrapper, integers())' From df2b5afe3afa33eae3509afe9de07c97a044920b Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 14:16:05 +0530 Subject: [PATCH 02/15] hypothesis: use the new context manager in `register_container`. This should make it easier to test and extend. Added tests for the current behavior when a type has an existing strategy and when it does not. --- returns/contrib/hypothesis/laws.py | 19 ++------ returns/contrib/hypothesis/type_resolver.py | 23 ++++++--- .../test_hypothesis/test_type_resolution.py | 48 ++++++++++++++++++- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 03349ea6..7e468d7a 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -10,6 +10,7 @@ from hypothesis.strategies._internal import types # noqa: PLC2701 from returns.contrib.hypothesis.containers import strategy_from_container +from returns.contrib.hypothesis.type_resolver import strategy_for_type from returns.primitives.laws import LAWS_ATTRIBUTE, Law, Lawful @@ -108,23 +109,9 @@ def register_container( use_init: bool, ) -> Iterator[None]: """Temporary registers a container if it is not registered yet.""" - used = types._global_type_lookup.pop(container_type, None) # noqa: SLF001 - st.register_type_strategy( - container_type, - strategy_from_container( - container_type, - use_init=use_init, - ), - ) - - try: + strategy = strategy_from_container(container_type, use_init=use_init) + with strategy_for_type(container_type, strategy): yield - finally: - types._global_type_lookup.pop(container_type) # noqa: SLF001 - if used: - st.register_type_strategy(container_type, used) - else: - _clean_caches() @contextmanager diff --git a/returns/contrib/hypothesis/type_resolver.py b/returns/contrib/hypothesis/type_resolver.py index 93d600ef..dbf1e1e0 100644 --- a/returns/contrib/hypothesis/type_resolver.py +++ b/returns/contrib/hypothesis/type_resolver.py @@ -7,12 +7,14 @@ from hypothesis import strategies as st from hypothesis.strategies._internal import types # noqa: PLC2701 -Strategy: TypeAlias = st.SearchStrategy | Callable[[type], st.SearchStrategy] +StrategyFactory: TypeAlias = ( + st.SearchStrategy | Callable[[type], st.SearchStrategy] +) @contextmanager def strategy_for_type( - type_: type[object], strategy: Strategy + type_: type[object], strategy: StrategyFactory ) -> Iterator[None]: """ Temporarily register a strategy in `hypothesis`. @@ -34,18 +36,27 @@ def strategy_for_type( yield finally: types._global_type_lookup.pop(type_) # noqa: SLF001 - if previous_strategy: - st.register_type_strategy(type_, previous_strategy) - else: + if previous_strategy is None: _clean_caches() + else: + st.register_type_strategy(type_, previous_strategy) def look_up_strategy( type_: type[object], -) -> st.SearchStrategy | Callable[[type], st.SearchStrategy] | None: +) -> StrategyFactory | None: """Return the strategy used by `hypothesis`.""" return types._global_type_lookup.get(type_) # noqa: SLF001 +def apply_strategy( + strategy: StrategyFactory, type_: type[object] +) -> StrategyFactory: + """Apply `strategy` to `type_`.""" + if isinstance(strategy, st.SearchStrategy): + return strategy + return strategy(type_) + + def _clean_caches() -> None: st.from_type.__clear_cache() # type: ignore[attr-defined] # noqa: SLF001 diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 72cadd05..055e5809 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 +from typing import Any, Final import pytest from hypothesis import given @@ -15,6 +15,12 @@ RequiresContextResult, RequiresContextResultE, ) +from returns.contrib.hypothesis.laws import register_container +from returns.contrib.hypothesis.type_resolver import ( + apply_strategy, + look_up_strategy, + strategy_for_type, +) from returns.future import Future, FutureResult from returns.io import IO, IOResult, IOResultE from returns.maybe import Maybe @@ -100,3 +106,43 @@ def test_custom_readerresult_types_resolve( assert isinstance(real_result.unwrap(), int) else: 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, use_init=False): + strategy_factory = look_up_strategy(container_type) + + assert strategy_factory is not None + strategy = apply_strategy(strategy_factory, container_type) + assert str(strategy) == 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 ( + strategy_for_type( + container_type, st.builds(container_type, st.integers()) + ), + register_container(container_type, use_init=False), + ): + strategy_factory = look_up_strategy(container_type) + + assert strategy_factory is not None + strategy = apply_strategy(strategy_factory, container_type) + assert str(strategy) == DEFAULT_RESULT_STRATEGY From 7d4a9e5ebf3f362e32982fbd932ff37a7068853d Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 14:32:40 +0530 Subject: [PATCH 03/15] hypothesis: make `register_container` accept settings. No functional change yet. I want to pass in a strategy in those settings. I'm making `Settings` public, which should be fine since we mention its fields in the docs. --- returns/contrib/hypothesis/laws.py | 18 ++++++++++-------- .../test_hypothesis/test_type_resolution.py | 11 ++++++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 7e468d7a..65621e29 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -15,7 +15,7 @@ @final -class _Settings(NamedTuple): +class Settings(NamedTuple): """Settings that we provide to an end user.""" settings_kwargs: dict[str, Any] @@ -54,7 +54,7 @@ def check_all_laws( - https://mmhaskell.com/blog/2017/3/13/obey-the-type-laws """ - settings = _Settings( + settings = Settings( settings_kwargs or {}, use_init, ) @@ -73,7 +73,7 @@ def check_all_laws( def container_strategies( container_type: type[Lawful], *, - settings: _Settings, + settings: Settings, ) -> Iterator[None]: """ Registers all types inside a container to resolve to a correct strategy. @@ -106,10 +106,12 @@ def container_strategies( def register_container( container_type: type['Lawful'], *, - use_init: bool, + settings: Settings, ) -> Iterator[None]: """Temporary registers a container if it is not registered yet.""" - strategy = strategy_from_container(container_type, use_init=use_init) + strategy = strategy_from_container( + container_type, use_init=settings.use_init + ) with strategy_for_type(container_type, strategy): yield @@ -231,7 +233,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: @@ -242,7 +244,7 @@ def factory(source: st.DataObject) -> None: container_strategies(container_type, settings=settings), ) stack.enter_context( - register_container(container_type, use_init=settings.use_init), + register_container(container_type, settings=settings), ) source.draw(st.builds(law.definition)) @@ -254,7 +256,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)( diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 055e5809..f7be75ee 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -15,7 +15,7 @@ RequiresContextResult, RequiresContextResultE, ) -from returns.contrib.hypothesis.laws import register_container +from returns.contrib.hypothesis.laws import Settings, register_container from returns.contrib.hypothesis.type_resolver import ( apply_strategy, look_up_strategy, @@ -123,7 +123,9 @@ 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, use_init=False): + with register_container( + container_type, settings=Settings(settings_kwargs={}, use_init=False) + ): strategy_factory = look_up_strategy(container_type) assert strategy_factory is not None @@ -139,7 +141,10 @@ def test_register_container_with_strategy() -> None: strategy_for_type( container_type, st.builds(container_type, st.integers()) ), - register_container(container_type, use_init=False), + register_container( + container_type, + settings=Settings(settings_kwargs={}, use_init=False), + ), ): strategy_factory = look_up_strategy(container_type) From 167923606d2995afa300a3012429606d839ed486 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 14:45:15 +0530 Subject: [PATCH 04/15] hypothesis: accept a strategy in the settings. This currently overrides the strategy for `MyContainer`. However, it does not yet override the strategy for ancestor interfaces. Coming up next. --- returns/contrib/hypothesis/laws.py | 12 ++++++++--- .../test_hypothesis/test_type_resolution.py | 21 ++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 65621e29..eac75401 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -10,7 +10,10 @@ from hypothesis.strategies._internal import types # noqa: PLC2701 from returns.contrib.hypothesis.containers import strategy_from_container -from returns.contrib.hypothesis.type_resolver import strategy_for_type +from returns.contrib.hypothesis.type_resolver import ( + StrategyFactory, + strategy_for_type, +) from returns.primitives.laws import LAWS_ATTRIBUTE, Law, Lawful @@ -20,6 +23,7 @@ class Settings(NamedTuple): settings_kwargs: dict[str, Any] use_init: bool + strategy: StrategyFactory | None = None def check_all_laws( @@ -109,8 +113,10 @@ def register_container( settings: Settings, ) -> Iterator[None]: """Temporary registers a container if it is not registered yet.""" - strategy = strategy_from_container( - container_type, use_init=settings.use_init + strategy = ( + strategy_from_container(container_type, use_init=settings.use_init) + if settings.strategy is None + else settings.strategy ) with strategy_for_type(container_type, strategy): yield diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index f7be75ee..3b71bdca 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -26,7 +26,7 @@ from returns.maybe import Maybe from returns.pipeline import is_successful from returns.primitives.laws import Lawful -from returns.result import Result, ResultE +from returns.result import Result, ResultE, Success _all_containers: Sequence[type[Lawful]] = ( Maybe, @@ -151,3 +151,22 @@ def test_register_container_with_strategy() -> None: assert strategy_factory is not None strategy = apply_strategy(strategy_factory, container_type) assert str(strategy) == 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, + strategy=st.builds(Success, st.integers()), + ), + ): + strategy_factory = look_up_strategy(container_type) + + assert strategy_factory is not None + strategy = apply_strategy(strategy_factory, container_type) + assert str(strategy) == 'builds(Success, integers())' From c0e97e5744c014fe7d0779589b6be6d9bd03c4f1 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 15:12:22 +0530 Subject: [PATCH 05/15] hypothesis: generalize the context manager to accept multiple strategies. --- returns/contrib/hypothesis/laws.py | 4 +- returns/contrib/hypothesis/type_resolver.py | 38 ++++++++------ .../test_hypothesis/test_type_resolution.py | 8 +-- .../test_hypothesis/test_type_resolver.py | 49 ++++++++++++------- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index eac75401..7305c4e2 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -12,7 +12,7 @@ from returns.contrib.hypothesis.containers import strategy_from_container from returns.contrib.hypothesis.type_resolver import ( StrategyFactory, - strategy_for_type, + strategies_for_types, ) from returns.primitives.laws import LAWS_ATTRIBUTE, Law, Lawful @@ -118,7 +118,7 @@ def register_container( if settings.strategy is None else settings.strategy ) - with strategy_for_type(container_type, strategy): + with strategies_for_types({container_type: strategy}): yield diff --git a/returns/contrib/hypothesis/type_resolver.py b/returns/contrib/hypothesis/type_resolver.py index dbf1e1e0..bd311be5 100644 --- a/returns/contrib/hypothesis/type_resolver.py +++ b/returns/contrib/hypothesis/type_resolver.py @@ -1,6 +1,6 @@ """Make `hypothesis` resolve types to the right strategies.""" -from collections.abc import Callable, Iterator +from collections.abc import Callable, Iterator, Mapping from contextlib import contextmanager from typing import TypeAlias @@ -13,15 +13,15 @@ @contextmanager -def strategy_for_type( - type_: type[object], strategy: StrategyFactory +def strategies_for_types( + mapping: Mapping[type[object], StrategyFactory], ) -> Iterator[None]: """ - Temporarily register a strategy in `hypothesis`. + Temporarily register strategies with `hypothesis`. - Within this context, `hypothesis` will generate data for `type_` - using `strategy`. Otherwise, it will continue to use the globally - registered strategy for `type_`. + Within this context, `hypothesis` will generate data for `MyType` + using `mapping[MyType]`, if possible. Otherwise, it will continue to + use the globally registered strategy for `MyType`. NOTE: This manually adds and removes strategies from an internal data structure of `hypothesis`: `types._global_type_lookup`. This is a global @@ -29,17 +29,19 @@ def strategy_for_type( we can easily have unintentional side-effects. We have to be very careful when modifying it. """ - previous_strategy = types._global_type_lookup.pop(type_, None) # noqa: SLF001 - st.register_type_strategy(type_, strategy) + previous_strategies: dict[type[object], StrategyFactory | None] = {} + for type_, strategy in mapping.items(): + previous_strategies[type_] = look_up_strategy(type_) + st.register_type_strategy(type_, strategy) try: yield finally: - types._global_type_lookup.pop(type_) # noqa: SLF001 - if previous_strategy is None: - _clean_caches() - else: - st.register_type_strategy(type_, previous_strategy) + for type_, previous_strategy in previous_strategies.items(): + if previous_strategy is None: + _remove_strategy(type_) + else: + st.register_type_strategy(type_, previous_strategy) def look_up_strategy( @@ -49,6 +51,14 @@ def look_up_strategy( return types._global_type_lookup.get(type_) # noqa: SLF001 +def _remove_strategy( + type_: type[object], +) -> None: + """Remove the strategy registered for `type_`.""" + types._global_type_lookup.pop(type_) # noqa: SLF001 + _clean_caches() + + def apply_strategy( strategy: StrategyFactory, type_: type[object] ) -> StrategyFactory: diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 3b71bdca..98e1a4e2 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.type_resolver import ( apply_strategy, look_up_strategy, - strategy_for_type, + strategies_for_types, ) from returns.future import Future, FutureResult from returns.io import IO, IOResult, IOResultE @@ -138,9 +138,9 @@ def test_register_container_with_strategy() -> None: container_type = Result with ( - strategy_for_type( - container_type, st.builds(container_type, st.integers()) - ), + strategies_for_types({ + container_type: st.builds(container_type, st.integers()) + }), register_container( container_type, settings=Settings(settings_kwargs={}, use_init=False), diff --git a/tests/test_contrib/test_hypothesis/test_type_resolver.py b/tests/test_contrib/test_hypothesis/test_type_resolver.py index fe10d453..baeb8395 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolver.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolver.py @@ -4,40 +4,53 @@ from returns.contrib.hypothesis.type_resolver import ( look_up_strategy, - strategy_for_type, + strategies_for_types, ) _ValueType = TypeVar('_ValueType') -class _Wrapper(Generic[_ValueType]): +class _Wrapper1(Generic[_ValueType]): _inner_value: _ValueType -def test_type_without_strategy() -> None: +class _Wrapper2(Generic[_ValueType]): + _inner_value: _ValueType + + +def test_types_without_strategies() -> None: # noqa: WPS210 """Check that it temporarily resolves a type that has no strategy.""" - strategy_before = look_up_strategy(_Wrapper) + strategy_before1 = look_up_strategy(_Wrapper1) + strategy_before2 = look_up_strategy(_Wrapper2) - with strategy_for_type(_Wrapper, st.builds(_Wrapper, st.integers())): - strategy_inside = look_up_strategy(_Wrapper) + with strategies_for_types({ + _Wrapper1: st.builds(_Wrapper1, st.integers()), + _Wrapper2: st.builds(_Wrapper2, st.text()), + }): + strategy_inside1 = look_up_strategy(_Wrapper1) + strategy_inside2 = look_up_strategy(_Wrapper2) - strategy_after = look_up_strategy(_Wrapper) + strategy_after1 = look_up_strategy(_Wrapper1) + strategy_after2 = look_up_strategy(_Wrapper2) - assert strategy_before is None - assert str(strategy_inside) == 'builds(_Wrapper, integers())' - assert strategy_after is None + assert strategy_before1 is None + assert strategy_before2 is None + assert str(strategy_inside1) == 'builds(_Wrapper1, integers())' + assert str(strategy_inside2) == 'builds(_Wrapper2, text())' + assert strategy_after1 is None + assert strategy_after2 is None def test_type_with_strategy() -> None: """Check that it restores the original strategy.""" - with strategy_for_type(_Wrapper, st.builds(_Wrapper, st.integers())): - strategy_before = look_up_strategy(_Wrapper) + with strategies_for_types({_Wrapper1: st.builds(_Wrapper1, st.integers())}): + strategy_before = look_up_strategy(_Wrapper1) - with strategy_for_type(_Wrapper, st.builds(_Wrapper, st.text())): - strategy_inside = look_up_strategy(_Wrapper) + with strategies_for_types({_Wrapper1: st.builds(_Wrapper1, st.text())}): + strategy_inside = look_up_strategy(_Wrapper1) - strategy_after = look_up_strategy(_Wrapper) + strategy_after = look_up_strategy(_Wrapper1) - assert str(strategy_before) == 'builds(_Wrapper, integers())' - assert str(strategy_inside) == 'builds(_Wrapper, text())' - assert str(strategy_after) == 'builds(_Wrapper, integers())' + assert str(strategy_before) == 'builds(_Wrapper1, integers())' + assert str(strategy_inside) == 'builds(_Wrapper1, text())' + assert str(strategy_after) == 'builds(_Wrapper1, integers())' From 834a9ce4b14d70455134dd5a0de4bfbb89079e63 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 15:44:10 +0530 Subject: [PATCH 06/15] hypothesis: add tests for container strategies. Want to change it to use the strategy from the settings. --- .../test_hypothesis/test_type_resolution.py | 87 ++++++++++++++++--- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 98e1a4e2..21c62ea9 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -15,8 +15,14 @@ RequiresContextResult, RequiresContextResultE, ) -from returns.contrib.hypothesis.laws import Settings, register_container +from returns.contrib.hypothesis.laws import ( + Settings, + container_strategies, + lawful_interfaces, + register_container, +) from returns.contrib.hypothesis.type_resolver import ( + StrategyFactory, apply_strategy, look_up_strategy, strategies_for_types, @@ -27,6 +33,7 @@ from returns.pipeline import is_successful from returns.primitives.laws import Lawful from returns.result import Result, ResultE, Success +from test_hypothesis.test_laws import test_custom_type_applicative _all_containers: Sequence[type[Lawful]] = ( Maybe, @@ -128,9 +135,10 @@ def test_register_container_with_no_strategy() -> None: ): strategy_factory = look_up_strategy(container_type) - assert strategy_factory is not None - strategy = apply_strategy(strategy_factory, container_type) - assert str(strategy) == DEFAULT_RESULT_STRATEGY + assert ( + _strategy_string(strategy_factory, container_type) + == DEFAULT_RESULT_STRATEGY + ) def test_register_container_with_strategy() -> None: @@ -148,9 +156,10 @@ def test_register_container_with_strategy() -> None: ): strategy_factory = look_up_strategy(container_type) - assert strategy_factory is not None - strategy = apply_strategy(strategy_factory, container_type) - assert str(strategy) == DEFAULT_RESULT_STRATEGY + assert ( + _strategy_string(strategy_factory, container_type) + == DEFAULT_RESULT_STRATEGY + ) def test_register_container_with_setting() -> None: @@ -167,6 +176,64 @@ def test_register_container_with_setting() -> None: ): strategy_factory = look_up_strategy(container_type) - assert strategy_factory is not None - strategy = apply_strategy(strategy_factory, container_type) - assert str(strategy) == 'builds(Success, integers())' + assert ( + _strategy_string(strategy_factory, container_type) + == 'builds(Success, integers())' + ) + + +def test_interface_strategies() -> None: + """Check that ancestor interfaces get resolved to the concrete container.""" + container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 + + strategy_factories_before = _interface_factories(container_type) + + with container_strategies( + container_type, settings=Settings(settings_kwargs={}, use_init=False) + ): + strategy_factories_inside = _interface_factories(container_type) + + strategy_factories_after = _interface_factories(container_type) + + assert _strategy_strings(strategy_factories_before, container_type) == [ + 'None', + 'None', + ] + 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))", + ] + assert _strategy_strings(strategy_factories_after, container_type) == [ + 'None', + 'None', + ] + + +def _interface_factories(type_: type[Lawful]) -> list[StrategyFactory | None]: + return [ + look_up_strategy(interface) for interface in lawful_interfaces(type_) + ] + + +def _strategy_strings( + strategy_factories: Sequence[StrategyFactory | None], type_: type[object] +) -> list[str]: + return [ + _strategy_string(strategy_factory, type_) + for strategy_factory in strategy_factories + ] + + +def _strategy_string( + strategy_factory: StrategyFactory | None, type_: type[object] +) -> str: + """Return an easily testable string representation.""" + return ( + str(None) + if strategy_factory is None + else str(apply_strategy(strategy_factory, type_)) + ) From a5c8954d101e9951b08fb9941446042406f2d8e6 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 15:47:33 +0530 Subject: [PATCH 07/15] hypothesis: rename the context manager for interface strategies. --- returns/contrib/hypothesis/laws.py | 6 +++--- tests/test_contrib/test_hypothesis/test_type_resolution.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 7305c4e2..692bfcd4 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -74,13 +74,13 @@ def check_all_laws( @contextmanager -def container_strategies( +def interface_strategies( container_type: type[Lawful], *, settings: Settings, ) -> Iterator[None]: """ - Registers all types inside a container to resolve to a correct strategy. + 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. @@ -247,7 +247,7 @@ def factory(source: st.DataObject) -> None: stack.enter_context(type_vars()) stack.enter_context(pure_functions()) stack.enter_context( - container_strategies(container_type, settings=settings), + interface_strategies(container_type, settings=settings), ) stack.enter_context( register_container(container_type, settings=settings), diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 21c62ea9..a66e260b 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -17,7 +17,7 @@ ) from returns.contrib.hypothesis.laws import ( Settings, - container_strategies, + interface_strategies, lawful_interfaces, register_container, ) @@ -188,7 +188,7 @@ def test_interface_strategies() -> None: strategy_factories_before = _interface_factories(container_type) - with container_strategies( + with interface_strategies( container_type, settings=Settings(settings_kwargs={}, use_init=False) ): strategy_factories_inside = _interface_factories(container_type) From 301f86a5a3567dbd23d03a41b4c29ef1322aba65 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 15:51:21 +0530 Subject: [PATCH 08/15] hypothesis: use the strategy context manager to resolve interfaces. --- returns/contrib/hypothesis/laws.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 692bfcd4..dd5ef8a6 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -1,5 +1,5 @@ import inspect -from collections.abc import Callable, Iterator +from collections.abc import Callable, Iterator, Mapping from contextlib import ExitStack, contextmanager from typing import Any, NamedTuple, TypeGuard, TypeVar, final @@ -88,22 +88,15 @@ def interface_strategies( Can be used independently from other functions. """ - our_interfaces = lawful_interfaces(container_type) - for interface in our_interfaces: - st.register_type_strategy( - interface, - strategy_from_container( - container_type, - use_init=settings.use_init, - ), + mapping: Mapping[type[object], StrategyFactory] = { + interface: strategy_from_container( + container_type, + use_init=settings.use_init, ) - - try: + for interface in lawful_interfaces(container_type) + } + with strategies_for_types(mapping): yield - finally: - for interface in our_interfaces: - types._global_type_lookup.pop(interface) # noqa: SLF001 - _clean_caches() @contextmanager From 2b0abee368ddce0c4e62aaef866cdf2f9fbda175 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 15:59:43 +0530 Subject: [PATCH 09/15] hypothesis: use the settings strategy for ancestor interfaces too. --- returns/contrib/hypothesis/laws.py | 24 ++++++++------ .../test_hypothesis/test_type_resolution.py | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index dd5ef8a6..ed64f207 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -89,10 +89,7 @@ def interface_strategies( Can be used independently from other functions. """ mapping: Mapping[type[object], StrategyFactory] = { - interface: strategy_from_container( - container_type, - use_init=settings.use_init, - ) + interface: _strategy_for_container(container_type, settings) for interface in lawful_interfaces(container_type) } with strategies_for_types(mapping): @@ -106,12 +103,9 @@ def register_container( settings: Settings, ) -> Iterator[None]: """Temporary registers a container if it is not registered yet.""" - strategy = ( - strategy_from_container(container_type, use_init=settings.use_init) - if settings.strategy is None - else settings.strategy - ) - with strategies_for_types({container_type: strategy}): + with strategies_for_types({ + container_type: _strategy_for_container(container_type, settings) + }): yield @@ -228,6 +222,16 @@ 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.strategy is None + else settings.strategy + ) + + def _run_law( container_type: type[Lawful], law: Law, diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index a66e260b..c58b78c9 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -213,6 +213,38 @@ def test_interface_strategies() -> None: ] +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 + + strategy_factories_before = _interface_factories(container_type) + + with interface_strategies( + container_type, + settings=Settings( + settings_kwargs={}, + use_init=False, + strategy=st.builds(container_type, st.integers()), + ), + ): + strategy_factories_inside = _interface_factories(container_type) + + strategy_factories_after = _interface_factories(container_type) + + assert _strategy_strings(strategy_factories_before, container_type) == [ + 'None', + 'None', + ] + assert _strategy_strings(strategy_factories_inside, container_type) == [ + 'builds(_Wrapper, integers())', + 'builds(_Wrapper, integers())', + ] + assert _strategy_strings(strategy_factories_after, container_type) == [ + 'None', + 'None', + ] + + def _interface_factories(type_: type[Lawful]) -> list[StrategyFactory | None]: return [ look_up_strategy(interface) for interface in lawful_interfaces(type_) From 62388bc503b1ba8128d8f4e9703b13d98627453f Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 16:14:50 +0530 Subject: [PATCH 10/15] hypothesis: accept container strategy in `check_all_laws`. I'm calling it `container_strategy` to leave room for future extensions that would allow configuring the pure function strategy, etc. --- returns/contrib/hypothesis/laws.py | 8 ++-- returns/contrib/hypothesis/type_resolver.py | 2 +- .../test_laws/test_user_specified_strategy.py | 11 +++++ .../test_hypothesis/test_type_resolution.py | 42 ++++++------------- 4 files changed, 30 insertions(+), 33 deletions(-) create mode 100644 tests/test_contrib/test_hypothesis/test_laws/test_user_specified_strategy.py diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index ed64f207..a87a9936 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -23,7 +23,7 @@ class Settings(NamedTuple): settings_kwargs: dict[str, Any] use_init: bool - strategy: StrategyFactory | None = None + container_strategy: StrategyFactory | None def check_all_laws( @@ -31,6 +31,7 @@ def check_all_laws( *, settings_kwargs: dict[str, Any] | None = None, use_init: bool = False, + container_strategy: StrategyFactory | None = None, ) -> None: """ Function to check all defined mathematical laws in a specified container. @@ -61,6 +62,7 @@ def check_all_laws( settings = Settings( settings_kwargs or {}, use_init, + container_strategy, ) for interface, laws in container_type.laws().items(): @@ -227,8 +229,8 @@ def _strategy_for_container( ) -> StrategyFactory: return ( strategy_from_container(container_type, use_init=settings.use_init) - if settings.strategy is None - else settings.strategy + if settings.container_strategy is None + else settings.container_strategy ) diff --git a/returns/contrib/hypothesis/type_resolver.py b/returns/contrib/hypothesis/type_resolver.py index bd311be5..31eeed3b 100644 --- a/returns/contrib/hypothesis/type_resolver.py +++ b/returns/contrib/hypothesis/type_resolver.py @@ -20,7 +20,7 @@ def strategies_for_types( Temporarily register strategies with `hypothesis`. Within this context, `hypothesis` will generate data for `MyType` - using `mapping[MyType]`, if possible. Otherwise, it will continue to + using `mapping[MyType]`, if available. Otherwise, it will continue to use the globally registered strategy for `MyType`. NOTE: This manually adds and removes strategies from an internal data diff --git a/tests/test_contrib/test_hypothesis/test_laws/test_user_specified_strategy.py b/tests/test_contrib/test_hypothesis/test_laws/test_user_specified_strategy.py new file mode 100644 index 00000000..f1799210 --- /dev/null +++ b/tests/test_contrib/test_hypothesis/test_laws/test_user_specified_strategy.py @@ -0,0 +1,11 @@ +from hypothesis import strategies as st +from test_hypothesis.test_laws import test_custom_type_applicative + +from returns.contrib.hypothesis.laws import check_all_laws + +container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 + +check_all_laws( + container_type, + container_strategy=st.builds(container_type, st.integers()), +) diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index c58b78c9..2f073ae2 100644 --- a/tests/test_contrib/test_hypothesis/test_type_resolution.py +++ b/tests/test_contrib/test_hypothesis/test_type_resolution.py @@ -131,7 +131,10 @@ def test_register_container_with_no_strategy() -> None: container_type = Result with register_container( - container_type, settings=Settings(settings_kwargs={}, use_init=False) + container_type, + settings=Settings( + settings_kwargs={}, use_init=False, container_strategy=None + ), ): strategy_factory = look_up_strategy(container_type) @@ -151,7 +154,9 @@ def test_register_container_with_strategy() -> None: }), register_container( container_type, - settings=Settings(settings_kwargs={}, use_init=False), + settings=Settings( + settings_kwargs={}, use_init=False, container_strategy=None + ), ), ): strategy_factory = look_up_strategy(container_type) @@ -171,7 +176,7 @@ def test_register_container_with_setting() -> None: settings=Settings( settings_kwargs={}, use_init=False, - strategy=st.builds(Success, st.integers()), + container_strategy=st.builds(Success, st.integers()), ), ): strategy_factory = look_up_strategy(container_type) @@ -186,19 +191,14 @@ def test_interface_strategies() -> None: """Check that ancestor interfaces get resolved to the concrete container.""" container_type = test_custom_type_applicative._Wrapper # noqa: SLF001 - strategy_factories_before = _interface_factories(container_type) - with interface_strategies( - container_type, settings=Settings(settings_kwargs={}, use_init=False) + container_type, + settings=Settings( + settings_kwargs={}, use_init=False, container_strategy=None + ), ): strategy_factories_inside = _interface_factories(container_type) - strategy_factories_after = _interface_factories(container_type) - - assert _strategy_strings(strategy_factories_before, container_type) == [ - 'None', - 'None', - ] assert _strategy_strings(strategy_factories_inside, container_type) == [ "builds(from_value, shared(sampled_from([," " , , , ," @@ -207,42 +207,26 @@ def test_interface_strategies() -> None: " , , , ," " ]), key='typevar=~_FirstType').flatmap(from_type))", ] - assert _strategy_strings(strategy_factories_after, container_type) == [ - 'None', - 'None', - ] 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 - strategy_factories_before = _interface_factories(container_type) - with interface_strategies( container_type, settings=Settings( settings_kwargs={}, use_init=False, - strategy=st.builds(container_type, st.integers()), + container_strategy=st.builds(container_type, st.integers()), ), ): strategy_factories_inside = _interface_factories(container_type) - strategy_factories_after = _interface_factories(container_type) - - assert _strategy_strings(strategy_factories_before, container_type) == [ - 'None', - 'None', - ] assert _strategy_strings(strategy_factories_inside, container_type) == [ 'builds(_Wrapper, integers())', 'builds(_Wrapper, integers())', ] - assert _strategy_strings(strategy_factories_after, container_type) == [ - 'None', - 'None', - ] def _interface_factories(type_: type[Lawful]) -> list[StrategyFactory | None]: From 092e91d03dd62c5368f7e5fbe96daaa258c2870a Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 16:36:24 +0530 Subject: [PATCH 11/15] hypothesis: expect strategy to match the container. --- returns/contrib/hypothesis/laws.py | 6 ++++-- returns/contrib/hypothesis/type_resolver.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index a87a9936..1c8892f7 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -16,6 +16,8 @@ ) from returns.primitives.laws import LAWS_ATTRIBUTE, Law, Lawful +Example_co = TypeVar('Example_co', covariant=True) + @final class Settings(NamedTuple): @@ -27,11 +29,11 @@ class Settings(NamedTuple): def check_all_laws( - container_type: type[Lawful], + container_type: type[Lawful[Example_co]], *, settings_kwargs: dict[str, Any] | None = None, use_init: bool = False, - container_strategy: StrategyFactory | None = None, + container_strategy: StrategyFactory[Example_co] | None = None, ) -> None: """ Function to check all defined mathematical laws in a specified container. diff --git a/returns/contrib/hypothesis/type_resolver.py b/returns/contrib/hypothesis/type_resolver.py index 31eeed3b..0a574a4c 100644 --- a/returns/contrib/hypothesis/type_resolver.py +++ b/returns/contrib/hypothesis/type_resolver.py @@ -2,13 +2,16 @@ from collections.abc import Callable, Iterator, Mapping from contextlib import contextmanager -from typing import TypeAlias +from typing import TypeAlias, TypeVar from hypothesis import strategies as st from hypothesis.strategies._internal import types # noqa: PLC2701 +Example_co = TypeVar('Example_co', covariant=True) + StrategyFactory: TypeAlias = ( - st.SearchStrategy | Callable[[type], st.SearchStrategy] + st.SearchStrategy[Example_co] + | Callable[[type[Example_co]], st.SearchStrategy[Example_co]] ) @@ -45,8 +48,8 @@ def strategies_for_types( def look_up_strategy( - type_: type[object], -) -> StrategyFactory | None: + type_: type[Example_co], +) -> StrategyFactory[Example_co] | None: """Return the strategy used by `hypothesis`.""" return types._global_type_lookup.get(type_) # noqa: SLF001 @@ -60,8 +63,8 @@ def _remove_strategy( def apply_strategy( - strategy: StrategyFactory, type_: type[object] -) -> StrategyFactory: + strategy: StrategyFactory[Example_co], type_: type[Example_co] +) -> StrategyFactory[Example_co]: """Apply `strategy` to `type_`.""" if isinstance(strategy, st.SearchStrategy): return strategy From ecc2262f9c21cfe61490923d5afb123ace3ceb19 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 16:52:25 +0530 Subject: [PATCH 12/15] hypothesis: add docs and overloads. --- CHANGELOG.md | 1 + docs/pages/contrib/hypothesis_plugins.rst | 21 +++++++++++++++++++++ returns/contrib/hypothesis/laws.py | 20 +++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f18ff25..0e81d9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ See [0Ver](https://0ver.org/). ### Features - Make `hypothesis` plugin test laws from user-defined interfaces too +- Make `hypothesis` plugin accept user-defined strategies ### Bugfixes diff --git a/docs/pages/contrib/hypothesis_plugins.rst b/docs/pages/contrib/hypothesis_plugins.rst index e0be685e..cbfcabe6 100644 --- a/docs/pages/contrib/hypothesis_plugins.rst +++ b/docs/pages/contrib/hypothesis_plugins.rst @@ -140,6 +140,27 @@ 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: + +.. code:: python + + + from hypothesis import strategies as st + + 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``. + +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. diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 1c8892f7..973969c7 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -1,7 +1,7 @@ import inspect from collections.abc import Callable, Iterator, Mapping from contextlib import ExitStack, contextmanager -from typing import Any, NamedTuple, TypeGuard, TypeVar, final +from typing import Any, NamedTuple, TypeGuard, TypeVar, final, overload import pytest from hypothesis import given @@ -28,6 +28,24 @@ class Settings(NamedTuple): container_strategy: StrategyFactory | None +@overload +def check_all_laws( + container_type: type[Lawful[Example_co]], + *, + settings_kwargs: dict[str, Any] | None = None, + container_strategy: StrategyFactory[Example_co] | None = None, +) -> None: ... + + +@overload +def check_all_laws( + container_type: type[Lawful[Example_co]], + *, + settings_kwargs: dict[str, Any] | None = None, + use_init: bool = False, +) -> None: ... + + def check_all_laws( container_type: type[Lawful[Example_co]], *, From b9cff912b5c44e87869280cba4d3a9995fe6d422 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 17:59:52 +0530 Subject: [PATCH 13/15] hypothesis: fix type test for `check_all_laws`. --- .../test_hypothesis/test_laws/test_check_all_laws.yml | 2 +- 1 file changed, 1 insertion(+), 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 9c63c4fb..49014f6d 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 @@ -37,4 +37,4 @@ main: | from returns.contrib.hypothesis.laws import check_all_laws - reveal_type(check_all_laws) # N: Revealed type is "def (container_type: Type[returns.primitives.laws.Lawful[Any]], *, settings_kwargs: Union[builtins.dict[builtins.str, Any], None] =, use_init: builtins.bool =)" + reveal_type(check_all_laws) # N: Revealed type is "Overload(def [Example_co] (container_type: Type[returns.primitives.laws.Lawful[Example_co`-1]], *, settings_kwargs: Union[builtins.dict[builtins.str, Any], None] =, container_strategy: Union[Union[hypothesis.strategies._internal.strategies.SearchStrategy[Example_co`-1], def (Type[Example_co`-1]) -> hypothesis.strategies._internal.strategies.SearchStrategy[Example_co`-1]], None] =), def [Example_co] (container_type: Type[returns.primitives.laws.Lawful[Example_co`-1]], *, settings_kwargs: Union[builtins.dict[builtins.str, Any], None] =, use_init: builtins.bool =))" From 5b799beeefca1ac0910dbd249380317ec638eed5 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 18:19:31 +0530 Subject: [PATCH 14/15] hypothesis: add a more meaningful type test. No idea how to format this better. --- .../test_laws/test_check_all_laws.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 49014f6d..05bc40fa 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 @@ -25,8 +25,7 @@ x: Type[Lawful] = {{ container }} - -- case: test_all_laws_sig +- case: test_all_laws_accepts_only_one_approach disable_cache: false # TODO: remove this config after # mypy/typeshed/stdlib/unittest/mock.pyi:120: @@ -35,6 +34,13 @@ 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 - reveal_type(check_all_laws) # N: Revealed type is "Overload(def [Example_co] (container_type: Type[returns.primitives.laws.Lawful[Example_co`-1]], *, settings_kwargs: Union[builtins.dict[builtins.str, Any], None] =, container_strategy: Union[Union[hypothesis.strategies._internal.strategies.SearchStrategy[Example_co`-1], def (Type[Example_co`-1]) -> hypothesis.strategies._internal.strategies.SearchStrategy[Example_co`-1]], None] =), def [Example_co] (container_type: Type[returns.primitives.laws.Lawful[Example_co`-1]], *, settings_kwargs: Union[builtins.dict[builtins.str, Any], None] =, use_init: builtins.bool =))" + 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 + Result, use_init=True, container_strategy=st.builds(Success, st.integers()) + ) From b616e823413eaba56f717a11eff273a54daf46f0 Mon Sep 17 00:00:00 2001 From: S Pradeep Kumar Date: Mon, 10 Mar 2025 19:41:48 +0530 Subject: [PATCH 15/15] hypothesis: add post-init check; make settings private. Ideally, we would enforce the mutually exclusive settings in the types, but I'm leaving it at this for now. I'm making settings private again, because `check_all_laws` accepts direct arguments, not a settings object. So, I don't think it makes much sense to document the class in public and make it harder to change in the future. I'd made it public just so that I could use it in tests. --- returns/contrib/hypothesis/laws.py | 27 +++++++++++++------ .../test_hypothesis/test_type_resolution.py | 12 ++++----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/returns/contrib/hypothesis/laws.py b/returns/contrib/hypothesis/laws.py index 973969c7..bc84818a 100644 --- a/returns/contrib/hypothesis/laws.py +++ b/returns/contrib/hypothesis/laws.py @@ -1,7 +1,8 @@ +import dataclasses import inspect from collections.abc import Callable, Iterator, Mapping from contextlib import ExitStack, contextmanager -from typing import Any, NamedTuple, TypeGuard, TypeVar, final, overload +from typing import Any, TypeGuard, TypeVar, final, overload import pytest from hypothesis import given @@ -20,13 +21,22 @@ @final -class Settings(NamedTuple): +@dataclasses.dataclass(frozen=True) +class _Settings: """Settings that we provide to an end user.""" settings_kwargs: dict[str, Any] use_init: bool container_strategy: StrategyFactory | None + 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' + ) + @overload def check_all_laws( @@ -79,7 +89,7 @@ def check_all_laws( - https://mmhaskell.com/blog/2017/3/13/obey-the-type-laws """ - settings = Settings( + settings = _Settings( settings_kwargs or {}, use_init, container_strategy, @@ -99,7 +109,7 @@ def check_all_laws( def interface_strategies( container_type: type[Lawful], *, - settings: Settings, + settings: _Settings, ) -> Iterator[None]: """ Make all interfaces of a container resolve to the container's strategy. @@ -122,7 +132,7 @@ def interface_strategies( def register_container( container_type: type['Lawful'], *, - settings: Settings, + settings: _Settings, ) -> Iterator[None]: """Temporary registers a container if it is not registered yet.""" with strategies_for_types({ @@ -245,7 +255,8 @@ def _clean_caches() -> None: def _strategy_for_container( - container_type: type[Lawful], settings: Settings + container_type: type[Lawful], + settings: _Settings, ) -> StrategyFactory: return ( strategy_from_container(container_type, use_init=settings.use_init) @@ -258,7 +269,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: @@ -281,7 +292,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)( diff --git a/tests/test_contrib/test_hypothesis/test_type_resolution.py b/tests/test_contrib/test_hypothesis/test_type_resolution.py index 2f073ae2..282237c6 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, + _Settings, # noqa: PLC2701 interface_strategies, lawful_interfaces, register_container, @@ -132,7 +132,7 @@ def test_register_container_with_no_strategy() -> None: with register_container( container_type, - settings=Settings( + settings=_Settings( settings_kwargs={}, use_init=False, container_strategy=None ), ): @@ -154,7 +154,7 @@ def test_register_container_with_strategy() -> None: }), register_container( container_type, - settings=Settings( + settings=_Settings( settings_kwargs={}, use_init=False, container_strategy=None ), ), @@ -173,7 +173,7 @@ def test_register_container_with_setting() -> None: with register_container( container_type, - settings=Settings( + settings=_Settings( settings_kwargs={}, use_init=False, container_strategy=st.builds(Success, st.integers()), @@ -193,7 +193,7 @@ def test_interface_strategies() -> None: with interface_strategies( container_type, - settings=Settings( + settings=_Settings( settings_kwargs={}, use_init=False, container_strategy=None ), ): @@ -215,7 +215,7 @@ def test_interface_strategies_with_settings() -> None: with interface_strategies( container_type, - settings=Settings( + settings=_Settings( settings_kwargs={}, use_init=False, container_strategy=st.builds(container_type, st.integers()),