Skip to content

Commit d8120e5

Browse files
authored
Use TypeVarTuple for HKT utils definion (#1893)
1 parent 1a29e55 commit d8120e5

File tree

10 files changed

+159
-109
lines changed

10 files changed

+159
-109
lines changed

poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ classifiers = [
4242
returns = "returns.contrib.pytest.plugin"
4343

4444
[tool.poetry.plugins.hypothesis]
45-
_ = "returns.contrib.hypothesis._entrypoint"
45+
_ = "returns.contrib.hypothesis._entrypoint:_setup_hook"
4646

4747

4848
[tool.poetry.dependencies]

returns/contrib/hypothesis/_entrypoint.py

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,54 @@
88
99
"""
1010

11-
from typing import Sequence, Type
12-
13-
from hypothesis import strategies as st
14-
15-
from returns.context import (
16-
RequiresContext,
17-
RequiresContextFutureResult,
18-
RequiresContextIOResult,
19-
RequiresContextResult,
20-
)
21-
from returns.contrib.hypothesis.containers import strategy_from_container
22-
from returns.future import Future, FutureResult
23-
from returns.io import IO, IOResult
24-
from returns.maybe import Maybe
25-
from returns.primitives.laws import Lawful
26-
from returns.result import Result
27-
28-
#: Our types that we register in hypothesis to be working with ``st.from_type``
29-
REGISTERED_TYPES: Sequence[Type[Lawful]] = (
30-
Result,
31-
Maybe,
32-
IO,
33-
IOResult,
34-
Future,
35-
FutureResult,
36-
RequiresContext,
37-
RequiresContextResult,
38-
RequiresContextIOResult,
39-
RequiresContextFutureResult,
40-
)
41-
42-
for type_ in REGISTERED_TYPES:
43-
st.register_type_strategy(
44-
type_,
45-
strategy_from_container(type_),
11+
from __future__ import annotations
12+
13+
from typing import TYPE_CHECKING, Any, Callable, Sequence, Type, TypeVar
14+
15+
if TYPE_CHECKING:
16+
from returns.primitives.laws import Lawful
17+
18+
_Inst = TypeVar('_Inst', bound='Lawful')
19+
20+
21+
def _setup_hook() -> None:
22+
from hypothesis import strategies as st
23+
24+
from returns.context import (
25+
RequiresContext,
26+
RequiresContextFutureResult,
27+
RequiresContextIOResult,
28+
RequiresContextResult,
4629
)
30+
from returns.future import Future, FutureResult
31+
from returns.io import IO, IOResult
32+
from returns.maybe import Maybe
33+
from returns.result import Result
34+
35+
def factory(
36+
container_type: Type[_Inst],
37+
) -> Callable[[Any], st.SearchStrategy[_Inst]]:
38+
def decorator(thing: Any) -> st.SearchStrategy[_Inst]:
39+
from returns.contrib.hypothesis.containers import (
40+
strategy_from_container,
41+
)
42+
return strategy_from_container(container_type)(thing)
43+
return decorator
44+
45+
#: Our types that we register in hypothesis
46+
#: to be working with ``st.from_type``
47+
registered_types: Sequence[Type[Lawful]] = (
48+
Result,
49+
Maybe,
50+
IO,
51+
IOResult,
52+
Future,
53+
FutureResult,
54+
RequiresContext,
55+
RequiresContextResult,
56+
RequiresContextIOResult,
57+
RequiresContextFutureResult,
58+
)
59+
60+
for type_ in registered_types:
61+
st.register_type_strategy(type_, factory(type_))

returns/contrib/hypothesis/laws.py

Lines changed: 86 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import inspect
2-
from contextlib import contextmanager
2+
from contextlib import ExitStack, contextmanager
33
from typing import (
44
Any,
55
Callable,
@@ -52,7 +52,7 @@ def check_all_laws(
5252
5353
.. code:: python
5454
55-
check_all_laws(IO, {'max_examples': 100})
55+
check_all_laws(IO, settings_kwargs={'max_examples': 100})
5656
5757
Note:
5858
Cannot be used inside doctests because of the magic we use inside.
@@ -113,37 +113,38 @@ def container_strategies(
113113
),
114114
)
115115

116-
with maybe_register_container(
117-
container_type,
118-
use_init=settings.use_init,
119-
):
116+
try:
120117
yield
121-
122-
for interface in our_interfaces:
123-
types._global_type_lookup.pop(interface) # noqa: WPS441
118+
finally:
119+
for interface in our_interfaces:
120+
types._global_type_lookup.pop(interface)
121+
_clean_caches()
124122

125123

126124
@contextmanager
127-
def maybe_register_container(
125+
def register_container(
128126
container_type: Type['Lawful'],
129127
*,
130128
use_init: bool,
131129
) -> Iterator[None]:
132130
"""Temporary registers a container if it is not registered yet."""
133-
unknown_container = container_type not in types._global_type_lookup
134-
if unknown_container:
135-
st.register_type_strategy(
131+
used = types._global_type_lookup.pop(container_type, None)
132+
st.register_type_strategy(
133+
container_type,
134+
strategy_from_container(
136135
container_type,
137-
strategy_from_container(
138-
container_type,
139-
use_init=use_init,
140-
),
141-
)
142-
143-
yield
136+
use_init=use_init,
137+
),
138+
)
144139

145-
if unknown_container:
146-
types._global_type_lookup.pop(container_type) # noqa: WPS441
140+
try:
141+
yield
142+
finally:
143+
types._global_type_lookup.pop(container_type)
144+
if used:
145+
st.register_type_strategy(container_type, used)
146+
else:
147+
_clean_caches()
147148

148149

149150
@contextmanager
@@ -154,10 +155,11 @@ def pure_functions() -> Iterator[None]:
154155
It is not a default in ``hypothesis``.
155156
"""
156157
def factory(thing) -> st.SearchStrategy:
157-
like = (lambda: None) if len(
158-
thing.__args__,
159-
) == 1 else (lambda *args, **kwargs: None)
160-
158+
like = (
159+
(lambda: None)
160+
if len(thing.__args__) == 1
161+
else (lambda *args, **kwargs: None)
162+
)
161163
return st.functions(
162164
like=like,
163165
returns=st.from_type(thing.__args__[-1]),
@@ -168,10 +170,11 @@ def factory(thing) -> st.SearchStrategy:
168170
used = types._global_type_lookup[callable_type]
169171
st.register_type_strategy(callable_type, factory)
170172

171-
yield
172-
173-
types._global_type_lookup.pop(callable_type)
174-
st.register_type_strategy(callable_type, used)
173+
try:
174+
yield
175+
finally:
176+
types._global_type_lookup.pop(callable_type)
177+
st.register_type_strategy(callable_type, used)
175178

176179

177180
def _get_callable_type() -> Any:
@@ -195,23 +198,47 @@ def type_vars() -> Iterator[None]:
195198
for example, ``nan`` does not work for us
196199
197200
"""
198-
used = types._global_type_lookup[TypeVar]
199-
200201
def factory(thing):
201-
type_strategies = [
202-
types.resolve_TypeVar(thing),
203-
# TODO: add mutable strategies
204-
]
205-
return st.one_of(type_strategies).filter(
202+
return types.resolve_TypeVar(thing).filter(
206203
lambda inner: inner == inner, # noqa: WPS312
207204
)
208205

206+
used = types._global_type_lookup.pop(TypeVar)
209207
st.register_type_strategy(TypeVar, factory)
210208

211-
yield
209+
try:
210+
yield
211+
finally:
212+
types._global_type_lookup.pop(TypeVar)
213+
st.register_type_strategy(TypeVar, used)
214+
215+
216+
@contextmanager
217+
def clean_plugin_context() -> Iterator[None]:
218+
"""
219+
We register a lot of types in `_entrypoint.py`, we need to clean them.
220+
221+
Otherwise, some types might be messed up.
222+
"""
223+
saved_stategies = {}
224+
for strategy_key, strategy in types._global_type_lookup.items():
225+
if isinstance(strategy_key, type):
226+
if strategy_key.__module__.startswith('returns.'):
227+
saved_stategies.update({strategy_key: strategy})
228+
229+
for key_to_remove in saved_stategies:
230+
types._global_type_lookup.pop(key_to_remove)
231+
_clean_caches()
232+
233+
try:
234+
yield
235+
finally:
236+
for saved_state in saved_stategies.items():
237+
st.register_type_strategy(*saved_state)
212238

213-
types._global_type_lookup.pop(TypeVar)
214-
st.register_type_strategy(TypeVar, used)
239+
240+
def _clean_caches() -> None:
241+
st.from_type.__clear_cache() # type: ignore[attr-defined]
215242

216243

217244
def _run_law(
@@ -221,10 +248,17 @@ def _run_law(
221248
settings: _Settings,
222249
) -> Callable[[st.DataObject], None]:
223250
def factory(source: st.DataObject) -> None:
224-
with type_vars():
225-
with pure_functions():
226-
with container_strategies(container_type, settings=settings):
227-
source.draw(st.builds(law.definition))
251+
with ExitStack() as stack:
252+
stack.enter_context(clean_plugin_context())
253+
stack.enter_context(type_vars())
254+
stack.enter_context(pure_functions())
255+
stack.enter_context(
256+
container_strategies(container_type, settings=settings),
257+
)
258+
stack.enter_context(
259+
register_container(container_type, use_init=settings.use_init),
260+
)
261+
source.draw(st.builds(law.definition))
228262
return factory
229263

230264

@@ -254,7 +288,12 @@ def _create_law_test_case(
254288
setattr(
255289
module,
256290
test_function.__name__,
257-
# We mark all tests with `returns_lawful` marker,
258-
# so users can easily skip them if needed.
259-
pytest.mark.returns_lawful(test_function),
291+
pytest.mark.filterwarnings(
292+
# We ignore multiple warnings about unused coroutines and stuff:
293+
'ignore::pytest.PytestUnraisableExceptionWarning',
294+
)(
295+
# We mark all tests with `returns_lawful` marker,
296+
# so users can easily skip them if needed.
297+
pytest.mark.returns_lawful(test_function),
298+
),
260299
)

returns/interfaces/failable.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
# Used in laws:
2828
_NewFirstType = TypeVar('_NewFirstType')
29-
_NewSecondType = TypeVar('_NewSecondType')
3029

3130

3231
@final

returns/primitives/hkt.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol, TypeVar
1+
from typing import TYPE_CHECKING, Any, Callable, Protocol, TypeVar
22

3-
from typing_extensions import Never
3+
from typing_extensions import Generic, Never, TypeVarTuple, Unpack
44

55
_InstanceType = TypeVar('_InstanceType', covariant=True)
66
_TypeArgType1 = TypeVar('_TypeArgType1', covariant=True)
@@ -22,10 +22,10 @@
2222
_FirstKind = TypeVar('_FirstKind')
2323
_SecondKind = TypeVar('_SecondKind')
2424

25+
_TypeVars = TypeVarTuple('_TypeVars')
2526

26-
class KindN(
27-
Generic[_InstanceType, _TypeArgType1, _TypeArgType2, _TypeArgType3],
28-
):
27+
28+
class KindN(Generic[_InstanceType, Unpack[_TypeVars]]):
2929
"""
3030
Emulation support for Higher Kinded Types.
3131
@@ -109,9 +109,7 @@ def __getattr__(self, attrname: str):
109109
Kind3 = KindN[_InstanceType, _TypeArgType1, _TypeArgType2, _TypeArgType3]
110110

111111

112-
class SupportsKindN(
113-
KindN[_InstanceType, _TypeArgType1, _TypeArgType2, _TypeArgType3],
114-
):
112+
class SupportsKindN(KindN[_InstanceType, Unpack[_TypeVars]]):
115113
"""
116114
Base class for your containers.
117115

setup.cfg

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
[flake8]
77
format = wemake
8-
show-source = True
9-
doctests = True
10-
statistics = False
8+
show-source = true
9+
doctests = true
10+
statistics = false
1111

1212
# darglint configuration:
1313
# https://github.com/terrencepreilly/darglint
@@ -23,7 +23,7 @@ staticmethod-decorators =
2323

2424
# wemake-python-styleguide
2525
max-annotation-complexity = 4
26-
i-control-code = False
26+
i-control-code = false
2727
allowed-domain-names = some, result, do
2828

2929
extend-exclude =
@@ -131,8 +131,6 @@ addopts =
131131
--cov-fail-under=100
132132
# pytest-mypy-plugin:
133133
--mypy-ini-file=setup.cfg
134-
# hypothesis, temporary solution:
135-
-p 'no:hypothesispytest'
136134

137135
# Ignores some warnings inside:
138136
filterwarnings =

0 commit comments

Comments
 (0)