Skip to content

Commit cfc58fc

Browse files
committed
hypothesis: split settings into default and override.
1 parent 9e7d609 commit cfc58fc

2 files changed

Lines changed: 122 additions & 20 deletions

File tree

returns/contrib/hypothesis/laws.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dataclasses
22
import inspect
3-
from collections.abc import Callable, Iterator, Mapping
3+
from collections.abc import Callable, Iterator
44
from contextlib import ExitStack, contextmanager
55
from typing import Any, TypeVar, final, overload
66

@@ -9,6 +9,7 @@
99
from hypothesis import settings as hypothesis_settings
1010
from hypothesis import strategies as st
1111
from hypothesis.strategies._internal import types # noqa: PLC2701
12+
from typing_extensions import Self
1213

1314
from returns.contrib.hypothesis.containers import strategy_from_container
1415
from returns.contrib.hypothesis.type_resolver import (
@@ -28,6 +29,19 @@ class _Settings:
2829
settings_kwargs: dict[str, Any]
2930
use_init: bool
3031
container_strategy: StrategyFactory | None
32+
other_strategies: dict[type[object], StrategyFactory] = dataclasses.field(
33+
default_factory=dict
34+
)
35+
36+
def __or__(self, other: Self) -> Self:
37+
return _Settings(
38+
settings_kwargs=self.settings_kwargs | other.settings_kwargs,
39+
use_init=self.use_init | other.use_init,
40+
container_strategy=self.container_strategy
41+
if other.container_strategy is None
42+
else other.container_strategy,
43+
other_strategies=self.other_strategies | other.other_strategies,
44+
)
3145

3246
def __post_init__(self) -> None:
3347
"""Check that the settings are mutually compatible."""
@@ -38,6 +52,23 @@ def __post_init__(self) -> None:
3852
)
3953

4054

55+
def _default_settings(container_type: type[Lawful]) -> _Settings:
56+
"""Return default settings for creating law tests.
57+
58+
We use special strategies for `TypeVar` and `Callable` by default, but
59+
they can be overriden by the user if needed.
60+
"""
61+
return _Settings(
62+
settings_kwargs={},
63+
use_init=False,
64+
container_strategy=None,
65+
other_strategies={
66+
TypeVar: type_vars_factory, # type: ignore[dict-item]
67+
Callable: pure_functions_factory, # type: ignore[dict-item]
68+
},
69+
)
70+
71+
4172
@overload
4273
def check_all_laws(
4374
container_type: type[Lawful[Example_co]],
@@ -222,15 +253,26 @@ def factory(source: st.DataObject) -> None:
222253
def _types_to_strategies(
223254
container_type: type[Lawful],
224255
settings: _Settings,
225-
) -> Mapping[type[object], StrategyFactory]:
256+
) -> dict[type[object], StrategyFactory]:
257+
"""Return a mapping from type to `hypothesis` strategy.
258+
259+
We override the default settings with the user-provided `settings`.
260+
"""
261+
merged_settings = _default_settings(container_type) | settings
262+
return merged_settings.other_strategies | _container_mapping(
263+
container_type, merged_settings
264+
)
265+
266+
267+
def _container_mapping(
268+
container_type: type[Lawful],
269+
settings: _Settings,
270+
) -> dict[type[object], StrategyFactory]:
271+
"""Map `container_type` and its interfaces to the container strategy."""
272+
container_strategy = _strategy_for_container(container_type, settings)
226273
return {
227-
TypeVar: type_vars_factory, # type: ignore[dict-item]
228-
Callable: pure_functions_factory, # type: ignore[dict-item]
229-
**{
230-
interface: _strategy_for_container(container_type, settings)
231-
for interface in container_type.laws()
232-
},
233-
container_type: _strategy_for_container(container_type, settings),
274+
**dict.fromkeys(container_type.laws(), container_strategy),
275+
container_type: container_strategy,
234276
}
235277

236278

tests/test_contrib/test_hypothesis/test_type_resolution.py

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
look_up_strategy,
2626
)
2727
from returns.future import Future, FutureResult
28+
from returns.interfaces.applicative import ApplicativeN
2829
from returns.io import IO, IOResult, IOResultE
2930
from returns.maybe import Maybe
3031
from returns.pipeline import is_successful
@@ -112,6 +113,64 @@ def test_custom_readerresult_types_resolve(
112113
assert isinstance(real_result.failure(), str)
113114

114115

116+
def test_merge_settings() -> None:
117+
"""Check that each part of the settings can be overridden by users."""
118+
settings1 = _Settings(
119+
settings_kwargs={'a': 1, 'b': 2},
120+
use_init=False,
121+
container_strategy=st.integers(),
122+
other_strategies={int: st.integers(max_value=10), str: st.text('abc')},
123+
)
124+
settings2 = _Settings(
125+
settings_kwargs={'a': 1, 'c': 3},
126+
use_init=False,
127+
container_strategy=st.integers(max_value=20),
128+
other_strategies={int: st.integers(max_value=30), bool: st.booleans()},
129+
)
130+
131+
result = settings1 | settings2
132+
133+
assert result == _Settings(
134+
settings_kwargs={'a': 1, 'b': 2, 'c': 3},
135+
use_init=False,
136+
container_strategy=st.integers(max_value=20),
137+
other_strategies={
138+
int: st.integers(max_value=30),
139+
bool: st.booleans(),
140+
str: st.text('abc'),
141+
},
142+
)
143+
144+
145+
def test_merge_use_init() -> None:
146+
"""Check that `use_init` can be set to `True` by users.
147+
148+
Note: They can't set a `True` to `False`, since we use `|` to merge.
149+
However, the default value is `False`, so this should not be a problem.
150+
"""
151+
settings1 = _Settings(
152+
settings_kwargs={},
153+
use_init=False,
154+
container_strategy=None,
155+
other_strategies={},
156+
)
157+
settings2 = _Settings(
158+
settings_kwargs={},
159+
use_init=True,
160+
container_strategy=None,
161+
other_strategies={},
162+
)
163+
164+
result = settings1 | settings2
165+
166+
assert result == _Settings(
167+
settings_kwargs={},
168+
use_init=True,
169+
container_strategy=None,
170+
other_strategies={},
171+
)
172+
173+
115174
_ValueType = TypeVar('_ValueType')
116175

117176

@@ -166,7 +225,7 @@ def test_types_to_strategies_default() -> None: # noqa: WPS210
166225

167226

168227
def test_types_to_strategies_overrides() -> None: # noqa: WPS210
169-
"""Check that we prefer the strategies in settings."""
228+
"""Check that we allow the user to override all strategies."""
170229
container_type = test_custom_type_applicative._Wrapper # noqa: SLF001
171230
# NOTE: There is a type error because `Callable` is a
172231
# special form, not a type.
@@ -178,6 +237,14 @@ def test_types_to_strategies_overrides() -> None: # noqa: WPS210
178237
settings_kwargs={},
179238
use_init=False,
180239
container_strategy=st.builds(container_type, st.integers()),
240+
other_strategies={
241+
TypeVar: st.text(),
242+
callable_type: st.functions(returns=st.booleans()),
243+
# This strategy does not get used, because we use
244+
# the given `container_strategy` for all interfaces of the
245+
# container type.
246+
ApplicativeN: st.tuples(st.integers()),
247+
},
181248
),
182249
)
183250

@@ -195,20 +262,13 @@ def test_types_to_strategies_overrides() -> None: # noqa: WPS210
195262
]
196263
assert (
197264
_strategy_string(result[callable_type], Callable[[int, str], bool])
198-
== 'functions(like=lambda *args, **kwargs: <unknown>,'
199-
' returns=booleans(), pure=True)'
265+
== 'functions(returns=booleans())'
200266
)
201267
assert (
202268
_strategy_string(result[callable_type], Callable[[], None])
203-
== 'functions(like=lambda: None, returns=none(), pure=True)'
204-
)
205-
assert (
206-
_strategy_string(result[TypeVar], _ValueType)
207-
== "shared(sampled_from([<class 'NoneType'>, <class 'bool'>,"
208-
" <class 'int'>, <class 'float'>, <class 'str'>, <class 'bytes'>]),"
209-
" key='typevar=~_ValueType').flatmap(from_type).filter(lambda"
210-
' inner: inner == inner)'
269+
== 'functions(returns=booleans())'
211270
)
271+
assert _strategy_string(result[TypeVar], _ValueType) == 'text()'
212272

213273

214274
def _interface_factories(type_: type[Lawful]) -> list[StrategyFactory | None]:

0 commit comments

Comments
 (0)