Skip to content

Commit

Permalink
More tests (#613)
Browse files Browse the repository at this point in the history
* 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
Tinche authored Dec 26, 2024
1 parent a67ebd0 commit c4ab066
Showing 19 changed files with 157 additions and 88 deletions.
2 changes: 1 addition & 1 deletion src/cattrs/preconf/bson.py
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ def configure_converter(converter: BaseConverter):
* a deserialization hook is registered for bson.ObjectId by default
* string and int enums are passed through when unstructuring
.. versionchanged: 24.2.0
.. versionchanged:: 24.2.0
Enums are left to the library to unstructure, speeding them up.
"""

2 changes: 1 addition & 1 deletion src/cattrs/preconf/json.py
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ def configure_converter(converter: BaseConverter):
* union passthrough is configured for unions of strings, bools, ints,
floats and None
.. versionchanged: 24.2.0
.. versionchanged:: 24.2.0
Enums are left to the library to unstructure, speeding them up.
"""
converter.register_unstructure_hook(
2 changes: 1 addition & 1 deletion src/cattrs/preconf/msgpack.py
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ def configure_converter(converter: BaseConverter):
* sets are serialized as lists
* string and int enums are passed through when unstructuring
.. versionchanged: 24.2.0
.. versionchanged:: 24.2.0
Enums are left to the library to unstructure, speeding them up.
"""
converter.register_unstructure_hook(datetime, lambda v: v.timestamp())
2 changes: 1 addition & 1 deletion src/cattrs/preconf/msgspec.py
Original file line number Diff line number Diff line change
@@ -75,7 +75,7 @@ def configure_converter(converter: Converter) -> None:
* union passthrough configured for str, bool, int, float and None
* bare, string and int enums are passed through when unstructuring
.. versionchanged: 24.2.0
.. versionchanged:: 24.2.0
Enums are left to the library to unstructure, speeding them up.
"""
configure_passthroughs(converter)
12 changes: 6 additions & 6 deletions src/cattrs/preconf/orjson.py
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@

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


def configure_converter(converter: BaseConverter):
def configure_converter(converter: Converter):
"""
Configure the converter for use with the orjson library.
@@ -40,9 +40,9 @@ def configure_converter(converter: BaseConverter):
* mapping keys are coerced into strings when unstructuring
* bare, string and int enums are passed through when unstructuring
.. versionchanged: 24.1.0
.. versionchanged:: 24.1.0
Add support for typed namedtuples.
.. versionchanged: 24.2.0
.. versionchanged:: 24.2.0
Enums are left to the library to unstructure, speeding them up.
"""
converter.register_unstructure_hook(
@@ -53,7 +53,7 @@ def configure_converter(converter: BaseConverter):
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))

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

converter._unstructure_func.register_func_list(
[
(is_mapping, gen_unstructure_mapping, True),
(is_mapping, unstructure_mapping_factory, True),
(
is_namedtuple,
partial(namedtuple_unstructure_factory, unstructure_to=tuple),
2 changes: 1 addition & 1 deletion src/cattrs/preconf/pyyaml.py
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ def configure_converter(converter: BaseConverter):
* datetimes and dates are validated
* typed namedtuples are serialized as lists
.. versionchanged: 24.1.0
.. versionchanged:: 24.1.0
Add support for typed namedtuples.
"""
converter.register_unstructure_hook(
2 changes: 1 addition & 1 deletion src/cattrs/preconf/ujson.py
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ def configure_converter(converter: BaseConverter):
* sets are serialized as lists
* string and int enums are passed through when unstructuring
.. versionchanged: 24.2.0
.. versionchanged:: 24.2.0
Enums are left to the library to unstructure, speeding them up.
"""
converter.register_unstructure_hook(
6 changes: 5 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import os
from typing import Literal

from hypothesis import HealthCheck, settings
from hypothesis.strategies import just, one_of
from typing_extensions import TypeAlias

from cattrs import UnstructureStrategy

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

if "CI" in os.environ:
if "CI" in os.environ: # pragma: nocover
settings.load_profile("CI")

unstructure_strats = one_of(just(s) for s in UnstructureStrategy)

FeatureFlag: TypeAlias = Literal["always", "never", "sometimes"]
14 changes: 14 additions & 0 deletions tests/test_defaultdicts.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
from typing import DefaultDict

from cattrs import Converter
from cattrs.cols import defaultdict_structure_factory


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

assert genconverter.unstructure(res) == {"a": "1", "b": "0"}


def test_factory(genconverter: Converter):
"""Explicit factories work."""
genconverter.register_structure_hook_func(
lambda t: t == defaultdict[str, int],
defaultdict_structure_factory(defaultdict[str, int], genconverter, lambda: 2),
)
res = genconverter.structure({"a": 1}, defaultdict[str, int])

assert isinstance(res, defaultdict)
assert res["a"] == 1
assert res["b"] == 2
5 changes: 1 addition & 4 deletions tests/test_disambiguators.py
Original file line number Diff line number Diff line change
@@ -130,10 +130,7 @@ class A:
assert fn({}) is A
assert fn(asdict(cl(*vals, **kwargs))) is cl

attr_names = {a.name for a in fields(cl)}

if "xyz" not in attr_names:
assert fn({"xyz": 1}) is A # Uses the fallback.
assert fn({"xyz": 1}) is A # Uses the fallback.


@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow])
10 changes: 6 additions & 4 deletions tests/test_gen_dict.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
from .untyped import nested_classes, simple_classes


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


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


@given(one_of(just(BaseConverter), just(Converter)), nested_classes | simple_classes())
@given(
one_of(just(BaseConverter), just(Converter)), nested_classes() | simple_classes()
)
def test_nodefs_generated_unstructuring_cl(
converter_cls: Type[BaseConverter], cl_and_vals
):
@@ -105,7 +107,7 @@ def test_nodefs_generated_unstructuring_cl(

@given(
one_of(just(BaseConverter), just(Converter)),
nested_classes | simple_classes() | simple_typed_dataclasses(),
nested_classes() | simple_classes() | simple_typed_dataclasses(),
)
def test_individual_overrides(converter_cls, cl_and_vals):
"""
14 changes: 4 additions & 10 deletions tests/test_preconf.py
Original file line number Diff line number Diff line change
@@ -88,6 +88,7 @@ class ABareEnum(Enum):
an_int: int
a_float: float
a_dict: Dict[str, int]
a_bare_dict: dict
a_list: List[int]
a_homogenous_tuple: TupleSubscriptable[int, ...]
a_hetero_tuple: TupleSubscriptable[str, int, float]
@@ -160,6 +161,7 @@ def everythings(
draw(ints),
draw(fs),
draw(dictionaries(key_text, ints)),
draw(dictionaries(key_text, strings)),
draw(lists(ints)),
tuple(draw(lists(ints))),
(draw(strings), draw(ints), draw(fs)),
@@ -196,26 +198,18 @@ def everythings(
def native_unions(
draw: DrawFn,
include_strings=True,
include_bools=True,
include_ints=True,
include_floats=True,
include_nones=True,
include_bytes=True,
include_datetimes=True,
include_objectids=False,
include_literals=True,
) -> tuple[Any, Any]:
types = []
strats = {}
types = [bool, int]
strats = {bool: booleans(), int: integers()}
if include_strings:
types.append(str)
strats[str] = text()
if include_bools:
types.append(bool)
strats[bool] = booleans()
if include_ints:
types.append(int)
strats[int] = integers()
if include_floats:
types.append(float)
strats[float] = floats(allow_nan=False)
9 changes: 9 additions & 0 deletions tests/test_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .untyped import gen_attr_names


def test_gen_attr_names():
"""We can generate a lot of attribute names."""
assert len(list(gen_attr_names())) == 697

# No duplicates!
assert len(list(gen_attr_names())) == len(set(gen_attr_names()))
14 changes: 10 additions & 4 deletions tests/test_tuples.py
Original file line number Diff line number Diff line change
@@ -69,19 +69,25 @@ class Test(NamedTuple):
def test_simple_dict_nametuples(genconverter: Converter):
"""Namedtuples can be un/structured to/from dicts."""

class TestInner(NamedTuple):
a: int

class Test(NamedTuple):
a: int
b: str = "test"
c: TestInner = TestInner(1)

genconverter.register_unstructure_hook_factory(
lambda t: t is Test, namedtuple_dict_unstructure_factory
lambda t: t in (Test, TestInner), namedtuple_dict_unstructure_factory
)
genconverter.register_structure_hook_factory(
lambda t: t is Test, namedtuple_dict_structure_factory
lambda t: t in (Test, TestInner), namedtuple_dict_structure_factory
)

assert genconverter.unstructure(Test(1)) == {"a": 1, "b": "test"}
assert genconverter.structure({"a": 1, "b": "2"}, Test) == Test(1, "2")
assert genconverter.unstructure(Test(1)) == {"a": 1, "b": "test", "c": {"a": 1}}
assert genconverter.structure({"a": 1, "b": "2"}, Test) == Test(
1, "2", TestInner(1)
)

# Defaults work.
assert genconverter.structure({"a": 1}, Test) == Test(1, "test")
9 changes: 9 additions & 0 deletions tests/test_typeddicts.py
Original file line number Diff line number Diff line change
@@ -27,12 +27,21 @@

from ._compat import is_py311_plus
from .typeddicts import (
gen_typeddict_attr_names,
generic_typeddicts,
simple_typeddicts,
simple_typeddicts_with_extra_keys,
)


def test_gen_attr_names():
"""We can generate a lot of attribute names."""
assert len(list(gen_typeddict_attr_names())) == 697

# No duplicates!
assert len(list(gen_typeddict_attr_names())) == len(set(gen_typeddict_attr_names()))


def mk_converter(detailed_validation: bool = True) -> Converter:
"""We can't use function-scoped fixtures with Hypothesis strats."""
c = Converter(detailed_validation=detailed_validation)
6 changes: 3 additions & 3 deletions tests/test_unstructure.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tests for dumping."""

from attr import asdict, astuple
from attrs import asdict, astuple
from hypothesis import given
from hypothesis.strategies import data, just, lists, one_of, sampled_from

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


@given(nested_classes)
@given(nested_classes())
def test_attrs_asdict_unstructure(nested_class):
"""Our dumping should be identical to `attrs`."""
converter = BaseConverter()
instance = nested_class[0]()
assert converter.unstructure(instance) == asdict(instance)


@given(nested_classes)
@given(nested_classes())
def test_attrs_astuple_unstructure(nested_class):
"""Our dumping should be identical to `attrs`."""
converter = BaseConverter(unstruct_strat=UnstructureStrategy.AS_TUPLE)
18 changes: 11 additions & 7 deletions tests/typed.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Strategies for attributes with types and classes using them."""

from collections import OrderedDict
from collections.abc import MutableSequence as AbcMutableSequence
from collections.abc import MutableSet as AbcMutableSet
from collections.abc import Sequence as AbcSequence
@@ -27,7 +26,7 @@
)

from attr._make import _CountingAttr
from attrs import NOTHING, Factory, field, frozen
from attrs import NOTHING, AttrsInstance, Factory, field, frozen
from hypothesis import note
from hypothesis.strategies import (
DrawFn,
@@ -293,7 +292,7 @@ def key(t):
attr_name = attr_name[1:]
kwarg_strats[attr_name] = attr_and_strat[1]
return tuples(
just(make_class("HypClass", OrderedDict(zip(gen_attr_names(), attrs)))),
just(make_class("HypClass", dict(zip(gen_attr_names(), attrs)))),
just(tuples(*vals)),
just(fixed_dictionaries(kwarg_strats)),
)
@@ -401,8 +400,8 @@ def path_typed_attrs(

@composite
def dict_typed_attrs(
draw, defaults=None, allow_mutable_defaults=True, kw_only=None
) -> SearchStrategy[tuple[_CountingAttr, SearchStrategy]]:
draw: DrawFn, defaults=None, allow_mutable_defaults=True, kw_only=None
) -> tuple[_CountingAttr, SearchStrategy[dict[str, int]]]:
"""
Generate a tuple of an attribute and a strategy that yields dictionaries
for that attribute. The dictionaries map strings to integers.
@@ -820,7 +819,7 @@ def nested_classes(
tuple[type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]],
]
],
) -> SearchStrategy[tuple[Type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]:
) -> tuple[type[AttrsInstance], SearchStrategy[PosArgs], SearchStrategy[KwArgs]]:
attrs, class_and_strat = draw(attrs_and_classes)
cls, strat, kw_strat = class_and_strat
pos_defs = tuple(draw(strat))
@@ -860,7 +859,12 @@ def nested_typed_classes_and_strat(

@composite
def nested_typed_classes(
draw, defaults=None, min_attrs=0, kw_only=None, newtypes=True, allow_nan=True
draw: DrawFn,
defaults=None,
min_attrs=0,
kw_only=None,
newtypes=True,
allow_nan=True,
):
cl, strat, kwarg_strat = draw(
nested_typed_classes_and_strat(
3 changes: 1 addition & 2 deletions tests/typeddicts.py
Original file line number Diff line number Diff line change
@@ -280,8 +280,7 @@ def make_typeddict(
bases_snippet = ", ".join(f"_base{ix}" for ix in range(len(bases)))
for ix, base in enumerate(bases):
globs[f"_base{ix}"] = base
if bases_snippet:
bases_snippet = f", {bases_snippet}"
bases_snippet = f", {bases_snippet}"

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

0 comments on commit c4ab066

Please sign in to comment.