Skip to content

Commit 621592d

Browse files
committed
More factories with takes_self
1 parent 5d480f4 commit 621592d

File tree

5 files changed

+93
-51
lines changed

5 files changed

+93
-51
lines changed

tests/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
2+
from typing import Literal
23

34
from hypothesis import HealthCheck, settings
45
from hypothesis.strategies import just, one_of
6+
from typing_extensions import TypeAlias
57

68
from cattrs import UnstructureStrategy
79

@@ -13,3 +15,5 @@
1315
settings.load_profile("CI")
1416

1517
unstructure_strats = one_of(just(s) for s in UnstructureStrategy)
18+
19+
FeatureFlag: TypeAlias = Literal["always", "never", "sometimes"]

tests/test_gen_dict.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .untyped import nested_classes, simple_classes
1717

1818

19-
@given(nested_classes | simple_classes())
19+
@given(nested_classes() | simple_classes())
2020
def test_unmodified_generated_unstructuring(cl_and_vals):
2121
converter = BaseConverter()
2222
cl, vals, kwargs = cl_and_vals
@@ -33,7 +33,7 @@ def test_unmodified_generated_unstructuring(cl_and_vals):
3333
assert res_expected == res_actual
3434

3535

36-
@given(nested_classes | simple_classes())
36+
@given(nested_classes() | simple_classes())
3737
def test_nodefs_generated_unstructuring(cl_and_vals):
3838
"""Test omitting default values on a per-attribute basis."""
3939
converter = BaseConverter()
@@ -61,7 +61,9 @@ def test_nodefs_generated_unstructuring(cl_and_vals):
6161
assert attr.name not in res
6262

6363

64-
@given(one_of(just(BaseConverter), just(Converter)), nested_classes | simple_classes())
64+
@given(
65+
one_of(just(BaseConverter), just(Converter)), nested_classes() | simple_classes()
66+
)
6567
def test_nodefs_generated_unstructuring_cl(
6668
converter_cls: Type[BaseConverter], cl_and_vals
6769
):
@@ -105,7 +107,7 @@ def test_nodefs_generated_unstructuring_cl(
105107

106108
@given(
107109
one_of(just(BaseConverter), just(Converter)),
108-
nested_classes | simple_classes() | simple_typed_dataclasses(),
110+
nested_classes() | simple_classes() | simple_typed_dataclasses(),
109111
)
110112
def test_individual_overrides(converter_cls, cl_and_vals):
111113
"""

tests/test_unstructure.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests for dumping."""
22

3-
from attr import asdict, astuple
3+
from attrs import asdict, astuple
44
from hypothesis import given
55
from hypothesis.strategies import data, just, lists, one_of, sampled_from
66

@@ -69,15 +69,15 @@ def test_enum_unstructure(enum, dump_strat, data):
6969
assert converter.unstructure(member) == member.value
7070

7171

72-
@given(nested_classes)
72+
@given(nested_classes())
7373
def test_attrs_asdict_unstructure(nested_class):
7474
"""Our dumping should be identical to `attrs`."""
7575
converter = BaseConverter()
7676
instance = nested_class[0]()
7777
assert converter.unstructure(instance) == asdict(instance)
7878

7979

80-
@given(nested_classes)
80+
@given(nested_classes())
8181
def test_attrs_astuple_unstructure(nested_class):
8282
"""Our dumping should be identical to `attrs`."""
8383
converter = BaseConverter(unstruct_strat=UnstructureStrategy.AS_TUPLE)

tests/typed.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Strategies for attributes with types and classes using them."""
22

3-
from collections import OrderedDict
43
from collections.abc import MutableSequence as AbcMutableSequence
54
from collections.abc import MutableSet as AbcMutableSet
65
from collections.abc import Sequence as AbcSequence
@@ -293,7 +292,7 @@ def key(t):
293292
attr_name = attr_name[1:]
294293
kwarg_strats[attr_name] = attr_and_strat[1]
295294
return tuples(
296-
just(make_class("HypClass", OrderedDict(zip(gen_attr_names(), attrs)))),
295+
just(make_class("HypClass", dict(zip(gen_attr_names(), attrs)))),
297296
just(tuples(*vals)),
298297
just(fixed_dictionaries(kwarg_strats)),
299298
)
@@ -860,7 +859,12 @@ def nested_typed_classes_and_strat(
860859

861860
@composite
862861
def nested_typed_classes(
863-
draw, defaults=None, min_attrs=0, kw_only=None, newtypes=True, allow_nan=True
862+
draw: DrawFn,
863+
defaults=None,
864+
min_attrs=0,
865+
kw_only=None,
866+
newtypes=True,
867+
allow_nan=True,
864868
):
865869
cl, strat, kwarg_strat = draw(
866870
nested_typed_classes_and_strat(

tests/untyped.py

+73-41
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import keyword
44
import string
5-
from collections import OrderedDict
5+
from collections.abc import Callable
66
from enum import Enum
77
from typing import (
88
Any,
@@ -23,11 +23,15 @@
2323
from attr._make import _CountingAttr
2424
from attrs import NOTHING, AttrsInstance, Factory, make_class
2525
from hypothesis import strategies as st
26-
from hypothesis.strategies import SearchStrategy
26+
from hypothesis.strategies import SearchStrategy, booleans
27+
from typing_extensions import TypeAlias
28+
29+
from . import FeatureFlag
2730

2831
PosArg = Any
2932
PosArgs = tuple[PosArg]
3033
KwArgs = dict[str, Any]
34+
AttrsAndArgs: TypeAlias = tuple[type[AttrsInstance], PosArgs, KwArgs]
3135

3236
primitive_strategies = st.sampled_from(
3337
[
@@ -167,7 +171,7 @@ def gen_attr_names() -> Iterable[str]:
167171
def _create_hyp_class(
168172
attrs_and_strategy: list[tuple[_CountingAttr, st.SearchStrategy[PosArgs]]],
169173
frozen=None,
170-
) -> SearchStrategy[tuple]:
174+
) -> SearchStrategy[AttrsAndArgs]:
171175
"""
172176
A helper function for Hypothesis to generate attrs classes.
173177
@@ -192,7 +196,7 @@ def key(t):
192196
return st.tuples(
193197
st.builds(
194198
lambda f: make_class(
195-
"HypClass", OrderedDict(zip(gen_attr_names(), attrs)), frozen=f
199+
"HypClass", dict(zip(gen_attr_names(), attrs)), frozen=f
196200
),
197201
st.booleans() if frozen is None else st.just(frozen),
198202
),
@@ -209,26 +213,28 @@ def just_class(tup):
209213
return _create_hyp_class(combined_attrs)
210214

211215

212-
def just_class_with_type(tup):
216+
def just_class_with_type(tup: tuple) -> SearchStrategy[AttrsAndArgs]:
213217
nested_cl = tup[1][0]
214-
default = attr.Factory(nested_cl)
215-
combined_attrs = list(tup[0])
216-
combined_attrs.append(
217-
(attr.ib(default=default, type=nested_cl), st.just(nested_cl()))
218-
)
219-
return _create_hyp_class(combined_attrs)
220218

219+
def make_with_default(takes_self: bool) -> SearchStrategy[AttrsAndArgs]:
220+
combined_attrs = list(tup[0])
221+
combined_attrs.append(
222+
(
223+
attr.ib(
224+
default=(
225+
Factory(
226+
nested_cl if not takes_self else lambda _: nested_cl(),
227+
takes_self=takes_self,
228+
)
229+
),
230+
type=nested_cl,
231+
),
232+
st.just(nested_cl()),
233+
)
234+
)
235+
return _create_hyp_class(combined_attrs)
221236

222-
def just_class_with_type_takes_self(
223-
tup: tuple[list[tuple[_CountingAttr, SearchStrategy]], tuple[type[AttrsInstance]]]
224-
) -> SearchStrategy[tuple[type[AttrsInstance]]]:
225-
nested_cl = tup[1][0]
226-
default = Factory(lambda _: nested_cl(), takes_self=True)
227-
combined_attrs = list(tup[0])
228-
combined_attrs.append(
229-
(attr.ib(default=default, type=nested_cl), st.just(nested_cl()))
230-
)
231-
return _create_hyp_class(combined_attrs)
237+
return booleans().flatmap(make_with_default)
232238

233239

234240
def just_frozen_class_with_type(tup):
@@ -240,22 +246,45 @@ def just_frozen_class_with_type(tup):
240246
return _create_hyp_class(combined_attrs)
241247

242248

243-
def list_of_class(tup):
249+
def list_of_class(tup: tuple) -> SearchStrategy[AttrsAndArgs]:
244250
nested_cl = tup[1][0]
245-
default = attr.Factory(lambda: [nested_cl()])
246-
combined_attrs = list(tup[0])
247-
combined_attrs.append((attr.ib(default=default), st.just([nested_cl()])))
248-
return _create_hyp_class(combined_attrs)
249251

252+
def make_with_default(takes_self: bool) -> SearchStrategy[AttrsAndArgs]:
253+
combined_attrs = list(tup[0])
254+
combined_attrs.append(
255+
(
256+
attr.ib(
257+
default=(
258+
Factory(lambda: [nested_cl()])
259+
if not takes_self
260+
else Factory(lambda _: [nested_cl()], takes_self=True)
261+
),
262+
type=list[nested_cl],
263+
),
264+
st.just([nested_cl()]),
265+
)
266+
)
267+
return _create_hyp_class(combined_attrs)
268+
269+
return booleans().flatmap(make_with_default)
250270

251-
def list_of_class_with_type(tup):
271+
272+
def list_of_class_with_type(tup: tuple) -> SearchStrategy[AttrsAndArgs]:
252273
nested_cl = tup[1][0]
253-
default = attr.Factory(lambda: [nested_cl()])
254-
combined_attrs = list(tup[0])
255-
combined_attrs.append(
256-
(attr.ib(default=default, type=List[nested_cl]), st.just([nested_cl()]))
257-
)
258-
return _create_hyp_class(combined_attrs)
274+
275+
def make_with_default(takes_self: bool) -> SearchStrategy[AttrsAndArgs]:
276+
default = (
277+
Factory(lambda: [nested_cl()])
278+
if not takes_self
279+
else Factory(lambda _: [nested_cl()], takes_self=True)
280+
)
281+
combined_attrs = list(tup[0])
282+
combined_attrs.append(
283+
(attr.ib(default=default, type=List[nested_cl]), st.just([nested_cl()]))
284+
)
285+
return _create_hyp_class(combined_attrs)
286+
287+
return booleans().flatmap(make_with_default)
259288

260289

261290
def dict_of_class(tup):
@@ -266,7 +295,9 @@ def dict_of_class(tup):
266295
return _create_hyp_class(combined_attrs)
267296

268297

269-
def _create_hyp_nested_strategy(simple_class_strategy):
298+
def _create_hyp_nested_strategy(
299+
simple_class_strategy: SearchStrategy,
300+
) -> SearchStrategy:
270301
"""
271302
Create a recursive attrs class.
272303
Given a strategy for building (simpler) classes, create and return
@@ -275,6 +306,7 @@ def _create_hyp_nested_strategy(simple_class_strategy):
275306
* a list of simpler classes
276307
* a dict mapping the string "cls" to a simpler class.
277308
"""
309+
278310
# A strategy producing tuples of the form ([list of attributes], <given
279311
# class strategy>).
280312
attrs_and_classes = st.tuples(lists_of_attrs(defaults=True), simple_class_strategy)
@@ -286,7 +318,6 @@ def _create_hyp_nested_strategy(simple_class_strategy):
286318
| attrs_and_classes.flatmap(list_of_class_with_type)
287319
| attrs_and_classes.flatmap(dict_of_class)
288320
| attrs_and_classes.flatmap(just_frozen_class_with_type)
289-
| attrs_and_classes.flatmap(just_class_with_type_takes_self)
290321
)
291322

292323

@@ -430,9 +461,10 @@ def simple_classes(defaults=None, min_attrs=0, frozen=None, kw_only=None):
430461
)
431462

432463

433-
# Ok, so st.recursive works by taking a base strategy (in this case,
434-
# simple_classes) and a special function. This function receives a strategy,
435-
# and returns another strategy (building on top of the base strategy).
436-
nested_classes = st.recursive(
437-
simple_classes(defaults=True), _create_hyp_nested_strategy
438-
)
464+
def nested_classes(
465+
takes_self: FeatureFlag = "sometimes",
466+
) -> SearchStrategy[AttrsAndArgs]:
467+
# Ok, so st.recursive works by taking a base strategy (in this case,
468+
# simple_classes) and a special function. This function receives a strategy,
469+
# and returns another strategy (building on top of the base strategy).
470+
return st.recursive(simple_classes(defaults=True), _create_hyp_nested_strategy)

0 commit comments

Comments
 (0)