Skip to content
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: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
([#723](https://github.com/python-attrs/cattrs/pull/723))
- _cattrs_ is now autoformatted using Ruff.
([#732](https://github.com/python-attrs/cattrs/pull/732))
- Support running the test suite without `cbor2` installed.
([#748](https://github.com/python-attrs/cattrs/pull/748))

## 26.1.0 (2026-02-18)

Expand Down
35 changes: 33 additions & 2 deletions tests/test_preconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
)
from cattrs.fns import identity
from cattrs.preconf.bson import make_converter as bson_make_converter
from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter
from cattrs.preconf.json import make_converter as json_make_converter
from cattrs.preconf.msgpack import make_converter as msgpack_make_converter
from cattrs.preconf.pyyaml import make_converter as pyyaml_make_converter
Expand All @@ -56,6 +55,13 @@
NO_MSGSPEC: Final = python_implementation() == "PyPy"
NO_ORJSON: Final = python_implementation() == "PyPy"

try:
__import__("cbor2")
except ImportError: # pragma: no cover
NO_CBOR2 = True
else:
NO_CBOR2 = False


@define
class A:
Expand Down Expand Up @@ -833,44 +839,59 @@ def test_tomllib_converter_unstruct_collection_overrides(everything: Everything)
assert raw["a_frozenset"] == sorted(raw["a_frozenset"])


@pytest.mark.skipif(NO_CBOR2, reason="cbor2 not available")
@given(everythings(min_int=-9223372036854775808, max_int=18446744073709551615))
def test_cbor2(everything: Everything):
from cbor2 import dumps as cbor2_dumps
from cbor2 import loads as cbor2_loads

from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter

converter = cbor2_make_converter()
raw = cbor2_dumps(converter.unstructure(everything))
assert converter.structure(cbor2_loads(raw), Everything) == everything


@pytest.mark.skipif(NO_CBOR2, reason="cbor2 not available")
@given(everythings(min_int=-9223372036854775808, max_int=18446744073709551615))
def test_cbor2_converter(everything: Everything):
from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter

converter = cbor2_make_converter()
raw = converter.dumps(everything)
assert converter.loads(raw, Everything) == everything


@pytest.mark.skipif(NO_CBOR2, reason="cbor2 not available")
@given(everythings(min_int=-9223372036854775808, max_int=18446744073709551615))
def test_cbor2_converter_unstruct_collection_overrides(everything: Everything):
from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter

converter = cbor2_make_converter(unstruct_collection_overrides={Set: sorted})
raw = converter.unstructure(everything)
assert raw["a_set"] == sorted(raw["a_set"])
assert raw["a_mutable_set"] == sorted(raw["a_mutable_set"])
assert raw["a_frozenset"] == sorted(raw["a_frozenset"])


@pytest.mark.skipif(NO_CBOR2, reason="cbor2 not available")
@given(union_and_val=native_unions(include_datetimes=False), detailed_validation=...)
def test_cbor2_unions(union_and_val: tuple, detailed_validation: bool):
"""Native union passthrough works."""
from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter

converter = cbor2_make_converter(detailed_validation=detailed_validation)
type, val = union_and_val

assert converter.structure(val, type) == val


@pytest.mark.skipif(NO_CBOR2, reason="cbor2 not available")
def test_cbor2_native_enums():
"""Bare, string and int enums are handled correctly."""

from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter

converter = cbor2_make_converter()

assert converter.dumps(Everything.AnIntEnum.A) == converter.dumps(
Expand All @@ -884,8 +905,11 @@ def test_cbor2_native_enums():
)


@pytest.mark.skipif(NO_CBOR2, reason="cbor2 not available")
def test_cbor2_efficient_enum():
"""`str` and `int` enums are handled efficiently."""
from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter

converter = cbor2_make_converter()

assert converter.get_unstructure_hook(Everything.AnIntEnum) == identity
Expand Down Expand Up @@ -970,7 +994,6 @@ def test_msgspec_efficient_enum():
"converter_factory",
[
bson_make_converter,
cbor2_make_converter,
json_make_converter,
msgpack_make_converter,
tomlkit_make_converter,
Expand All @@ -986,6 +1009,14 @@ def test_literal_dicts(converter_factory: Callable[[], Converter]):
assert converter.unstructure({"a": 1}, Dict[Literal["a"], int]) == {"a": 1}


@pytest.mark.skipif(NO_CBOR2, reason="cbor2 not available")
def test_literal_dicts_cbor2():
"""Dicts with keys that aren't subclasses of `type` work."""
from cattrs.preconf.cbor2 import make_converter as cbor2_make_converter

test_literal_dicts(cbor2_make_converter)


@pytest.mark.skipif(NO_ORJSON, reason="orjson not available")
def test_literal_dicts_orjson():
"""Dicts with keys that aren't subclasses of `type` work."""
Expand Down
Loading