Skip to content
Open
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
43 changes: 42 additions & 1 deletion tests/preconf/test_msgspec_cpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from attrs import define
from hypothesis import given
from msgspec import Struct, to_builtins
from pytest import fixture
from pytest import fixture, raises

from cattrs.fns import identity
from cattrs.preconf.json import make_converter as make_json_converter
Expand Down Expand Up @@ -109,6 +109,47 @@ def converter() -> Conv:
return make_converter()


@define
class RecursivePlain:
children: list[RecursivePlain]


@define
class ProbeFailureAttrs:
value: datetime


def test_unstructure_plain_recursive_attrs_still_uses_msgspec(converter: Conv):
"""Recursive attrs types without custom fields retain the passthrough path."""
value = RecursivePlain([RecursivePlain([])])

assert converter.get_unstructure_hook(RecursivePlain) is to_builtins
assert converter.unstructure(value) == {"children": [{"children": []}]}


def test_unstructure_probe_state_is_cleared_after_hook_error(
converter: Conv, monkeypatch
):
"""A failed hook lookup doesn't poison later converter use."""
converter.register_unstructure_hook(datetime, lambda value: int(value.timestamp()))
get_hook = converter.get_unstructure_hook

def fail_for_datetime(type, *args, **kwargs):
if type is datetime:
raise RuntimeError("probe failed")
return get_hook(type, *args, **kwargs)

monkeypatch.setattr(converter, "get_unstructure_hook", fail_for_datetime)
with raises(RuntimeError, match="probe failed"):
converter.get_unstructure_hook(ProbeFailureAttrs)

monkeypatch.setattr(converter, "get_unstructure_hook", get_hook)
assert converter.get_unstructure_hook(ProbeFailureAttrs) is not to_builtins
assert converter.unstructure(
ProbeFailureAttrs(datetime(2026, 1, 1, tzinfo=timezone.utc))
) == {"value": 1767225600}


def is_passthrough(fn: Callable) -> bool:
return fn in (identity, to_builtins)

Expand Down