Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More tests #613

Merged
merged 8 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cattrs/preconf/bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/preconf/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/preconf/msgpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/preconf/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions src/cattrs/preconf/orjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/preconf/pyyaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/preconf/ujson.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
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
Expand Up @@ -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):
Expand All @@ -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
Expand Up @@ -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])
Expand Down
10 changes: 6 additions & 4 deletions tests/test_gen_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
):
Expand Down Expand Up @@ -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):
"""
Expand Down
14 changes: 4 additions & 10 deletions tests/test_preconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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)
Expand Down
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
Expand Up @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions tests/test_typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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

Expand Down Expand Up @@ -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)
Expand Down
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
Expand All @@ -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,
Expand Down Expand Up @@ -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)),
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions tests/typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading
Loading