Skip to content

Commit c4ab066

Browse files
authored
More tests (#613)
* Improve cols coverage * Test the tests * Improve typeddict coverage * Improve disambiguators coverage * preconf: test bare dicts * test_preconf: always include bools and ints * More factories with `takes_self` * Fix return type annotations
1 parent a67ebd0 commit c4ab066

19 files changed

+157
-88
lines changed

Diff for: src/cattrs/preconf/bson.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def configure_converter(converter: BaseConverter):
6161
* a deserialization hook is registered for bson.ObjectId by default
6262
* string and int enums are passed through when unstructuring
6363
64-
.. versionchanged: 24.2.0
64+
.. versionchanged:: 24.2.0
6565
Enums are left to the library to unstructure, speeding them up.
6666
"""
6767

Diff for: src/cattrs/preconf/json.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def configure_converter(converter: BaseConverter):
3636
* union passthrough is configured for unions of strings, bools, ints,
3737
floats and None
3838
39-
.. versionchanged: 24.2.0
39+
.. versionchanged:: 24.2.0
4040
Enums are left to the library to unstructure, speeding them up.
4141
"""
4242
converter.register_unstructure_hook(

Diff for: src/cattrs/preconf/msgpack.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def configure_converter(converter: BaseConverter):
3131
* sets are serialized as lists
3232
* string and int enums are passed through when unstructuring
3333
34-
.. versionchanged: 24.2.0
34+
.. versionchanged:: 24.2.0
3535
Enums are left to the library to unstructure, speeding them up.
3636
"""
3737
converter.register_unstructure_hook(datetime, lambda v: v.timestamp())

Diff for: src/cattrs/preconf/msgspec.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def configure_converter(converter: Converter) -> None:
7575
* union passthrough configured for str, bool, int, float and None
7676
* bare, string and int enums are passed through when unstructuring
7777
78-
.. versionchanged: 24.2.0
78+
.. versionchanged:: 24.2.0
7979
Enums are left to the library to unstructure, speeding them up.
8080
"""
8181
configure_passthroughs(converter)

Diff for: src/cattrs/preconf/orjson.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from .._compat import is_subclass
1313
from ..cols import is_mapping, is_namedtuple, namedtuple_unstructure_factory
14-
from ..converters import BaseConverter, Converter
14+
from ..converters import Converter
1515
from ..fns import identity
1616
from ..literals import is_literal_containing_enums
1717
from ..strategies import configure_union_passthrough
@@ -28,7 +28,7 @@ def loads(self, data: Union[bytes, bytearray, memoryview, str], cl: type[T]) ->
2828
return self.structure(loads(data), cl)
2929

3030

31-
def configure_converter(converter: BaseConverter):
31+
def configure_converter(converter: Converter):
3232
"""
3333
Configure the converter for use with the orjson library.
3434
@@ -40,9 +40,9 @@ def configure_converter(converter: BaseConverter):
4040
* mapping keys are coerced into strings when unstructuring
4141
* bare, string and int enums are passed through when unstructuring
4242
43-
.. versionchanged: 24.1.0
43+
.. versionchanged:: 24.1.0
4444
Add support for typed namedtuples.
45-
.. versionchanged: 24.2.0
45+
.. versionchanged:: 24.2.0
4646
Enums are left to the library to unstructure, speeding them up.
4747
"""
4848
converter.register_unstructure_hook(
@@ -53,7 +53,7 @@ def configure_converter(converter: BaseConverter):
5353
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
5454
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
5555

56-
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
56+
def unstructure_mapping_factory(cl: Any, unstructure_to=None):
5757
key_handler = str
5858
args = getattr(cl, "__args__", None)
5959
if args:
@@ -77,7 +77,7 @@ def key_handler(v):
7777

7878
converter._unstructure_func.register_func_list(
7979
[
80-
(is_mapping, gen_unstructure_mapping, True),
80+
(is_mapping, unstructure_mapping_factory, True),
8181
(
8282
is_namedtuple,
8383
partial(namedtuple_unstructure_factory, unstructure_to=tuple),

Diff for: src/cattrs/preconf/pyyaml.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def configure_converter(converter: BaseConverter):
3838
* datetimes and dates are validated
3939
* typed namedtuples are serialized as lists
4040
41-
.. versionchanged: 24.1.0
41+
.. versionchanged:: 24.1.0
4242
Add support for typed namedtuples.
4343
"""
4444
converter.register_unstructure_hook(

Diff for: src/cattrs/preconf/ujson.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def configure_converter(converter: BaseConverter):
3333
* sets are serialized as lists
3434
* string and int enums are passed through when unstructuring
3535
36-
.. versionchanged: 24.2.0
36+
.. versionchanged:: 24.2.0
3737
Enums are left to the library to unstructure, speeding them up.
3838
"""
3939
converter.register_unstructure_hook(

Diff for: tests/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
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

810
settings.register_profile(
911
"CI", settings(suppress_health_check=[HealthCheck.too_slow]), deadline=None
1012
)
1113

12-
if "CI" in os.environ:
14+
if "CI" in os.environ: # pragma: nocover
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"]

Diff for: tests/test_defaultdicts.py

+14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import DefaultDict
55

66
from cattrs import Converter
7+
from cattrs.cols import defaultdict_structure_factory
78

89

910
def test_typing_defaultdicts(genconverter: Converter):
@@ -30,3 +31,16 @@ def test_collection_defaultdicts(genconverter: Converter):
3031
genconverter.register_unstructure_hook(int, str)
3132

3233
assert genconverter.unstructure(res) == {"a": "1", "b": "0"}
34+
35+
36+
def test_factory(genconverter: Converter):
37+
"""Explicit factories work."""
38+
genconverter.register_structure_hook_func(
39+
lambda t: t == defaultdict[str, int],
40+
defaultdict_structure_factory(defaultdict[str, int], genconverter, lambda: 2),
41+
)
42+
res = genconverter.structure({"a": 1}, defaultdict[str, int])
43+
44+
assert isinstance(res, defaultdict)
45+
assert res["a"] == 1
46+
assert res["b"] == 2

Diff for: tests/test_disambiguators.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,7 @@ class A:
130130
assert fn({}) is A
131131
assert fn(asdict(cl(*vals, **kwargs))) is cl
132132

133-
attr_names = {a.name for a in fields(cl)}
134-
135-
if "xyz" not in attr_names:
136-
assert fn({"xyz": 1}) is A # Uses the fallback.
133+
assert fn({"xyz": 1}) is A # Uses the fallback.
137134

138135

139136
@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow])

Diff for: 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
"""

Diff for: tests/test_preconf.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class ABareEnum(Enum):
8888
an_int: int
8989
a_float: float
9090
a_dict: Dict[str, int]
91+
a_bare_dict: dict
9192
a_list: List[int]
9293
a_homogenous_tuple: TupleSubscriptable[int, ...]
9394
a_hetero_tuple: TupleSubscriptable[str, int, float]
@@ -160,6 +161,7 @@ def everythings(
160161
draw(ints),
161162
draw(fs),
162163
draw(dictionaries(key_text, ints)),
164+
draw(dictionaries(key_text, strings)),
163165
draw(lists(ints)),
164166
tuple(draw(lists(ints))),
165167
(draw(strings), draw(ints), draw(fs)),
@@ -196,26 +198,18 @@ def everythings(
196198
def native_unions(
197199
draw: DrawFn,
198200
include_strings=True,
199-
include_bools=True,
200-
include_ints=True,
201201
include_floats=True,
202202
include_nones=True,
203203
include_bytes=True,
204204
include_datetimes=True,
205205
include_objectids=False,
206206
include_literals=True,
207207
) -> tuple[Any, Any]:
208-
types = []
209-
strats = {}
208+
types = [bool, int]
209+
strats = {bool: booleans(), int: integers()}
210210
if include_strings:
211211
types.append(str)
212212
strats[str] = text()
213-
if include_bools:
214-
types.append(bool)
215-
strats[bool] = booleans()
216-
if include_ints:
217-
types.append(int)
218-
strats[int] = integers()
219213
if include_floats:
220214
types.append(float)
221215
strats[float] = floats(allow_nan=False)

Diff for: tests/test_tests.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .untyped import gen_attr_names
2+
3+
4+
def test_gen_attr_names():
5+
"""We can generate a lot of attribute names."""
6+
assert len(list(gen_attr_names())) == 697
7+
8+
# No duplicates!
9+
assert len(list(gen_attr_names())) == len(set(gen_attr_names()))

Diff for: tests/test_tuples.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,25 @@ class Test(NamedTuple):
6969
def test_simple_dict_nametuples(genconverter: Converter):
7070
"""Namedtuples can be un/structured to/from dicts."""
7171

72+
class TestInner(NamedTuple):
73+
a: int
74+
7275
class Test(NamedTuple):
7376
a: int
7477
b: str = "test"
78+
c: TestInner = TestInner(1)
7579

7680
genconverter.register_unstructure_hook_factory(
77-
lambda t: t is Test, namedtuple_dict_unstructure_factory
81+
lambda t: t in (Test, TestInner), namedtuple_dict_unstructure_factory
7882
)
7983
genconverter.register_structure_hook_factory(
80-
lambda t: t is Test, namedtuple_dict_structure_factory
84+
lambda t: t in (Test, TestInner), namedtuple_dict_structure_factory
8185
)
8286

83-
assert genconverter.unstructure(Test(1)) == {"a": 1, "b": "test"}
84-
assert genconverter.structure({"a": 1, "b": "2"}, Test) == Test(1, "2")
87+
assert genconverter.unstructure(Test(1)) == {"a": 1, "b": "test", "c": {"a": 1}}
88+
assert genconverter.structure({"a": 1, "b": "2"}, Test) == Test(
89+
1, "2", TestInner(1)
90+
)
8591

8692
# Defaults work.
8793
assert genconverter.structure({"a": 1}, Test) == Test(1, "test")

Diff for: tests/test_typeddicts.py

+9
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,21 @@
2727

2828
from ._compat import is_py311_plus
2929
from .typeddicts import (
30+
gen_typeddict_attr_names,
3031
generic_typeddicts,
3132
simple_typeddicts,
3233
simple_typeddicts_with_extra_keys,
3334
)
3435

3536

37+
def test_gen_attr_names():
38+
"""We can generate a lot of attribute names."""
39+
assert len(list(gen_typeddict_attr_names())) == 697
40+
41+
# No duplicates!
42+
assert len(list(gen_typeddict_attr_names())) == len(set(gen_typeddict_attr_names()))
43+
44+
3645
def mk_converter(detailed_validation: bool = True) -> Converter:
3746
"""We can't use function-scoped fixtures with Hypothesis strats."""
3847
c = Converter(detailed_validation=detailed_validation)

Diff for: 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)

Diff for: tests/typed.py

+11-7
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
@@ -27,7 +26,7 @@
2726
)
2827

2928
from attr._make import _CountingAttr
30-
from attrs import NOTHING, Factory, field, frozen
29+
from attrs import NOTHING, AttrsInstance, Factory, field, frozen
3130
from hypothesis import note
3231
from hypothesis.strategies import (
3332
DrawFn,
@@ -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
)
@@ -401,8 +400,8 @@ def path_typed_attrs(
401400

402401
@composite
403402
def dict_typed_attrs(
404-
draw, defaults=None, allow_mutable_defaults=True, kw_only=None
405-
) -> SearchStrategy[tuple[_CountingAttr, SearchStrategy]]:
403+
draw: DrawFn, defaults=None, allow_mutable_defaults=True, kw_only=None
404+
) -> tuple[_CountingAttr, SearchStrategy[dict[str, int]]]:
406405
"""
407406
Generate a tuple of an attribute and a strategy that yields dictionaries
408407
for that attribute. The dictionaries map strings to integers.
@@ -820,7 +819,7 @@ def nested_classes(
820819
tuple[type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]],
821820
]
822821
],
823-
) -> SearchStrategy[tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]:
822+
) -> tuple[type[AttrsInstance], SearchStrategy[PosArgs], SearchStrategy[KwArgs]]:
824823
attrs, class_and_strat = draw(attrs_and_classes)
825824
cls, strat, kw_strat = class_and_strat
826825
pos_defs = tuple(draw(strat))
@@ -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(

Diff for: tests/typeddicts.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,7 @@ def make_typeddict(
280280
bases_snippet = ", ".join(f"_base{ix}" for ix in range(len(bases)))
281281
for ix, base in enumerate(bases):
282282
globs[f"_base{ix}"] = base
283-
if bases_snippet:
284-
bases_snippet = f", {bases_snippet}"
283+
bases_snippet = f", {bases_snippet}"
285284

286285
lines.append(f"class {cls_name}(TypedDict{bases_snippet}, total={total}):")
287286
for n, t in attrs.items():

0 commit comments

Comments
 (0)