Skip to content

Commit 75bfffa

Browse files
authored
Improve Hypothesis test robustness (#684)
* Improve Hypothesis test robustness * Add tests for `test_assert_only_unstructured_passes_for_primitives`
1 parent 727aa89 commit 75bfffa

File tree

6 files changed

+90
-6
lines changed

6 files changed

+90
-6
lines changed

HISTORY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ The third number is for emergencies when we need to start branches for older rel
1111

1212
Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).
1313

14+
## NEXT (UNRELEASED)
15+
16+
- Fix unstructuring NewTypes with the {class}`BaseConverter`.
17+
([#684](https://github.com/python-attrs/cattrs/pull/684))
18+
- Make some Hypothesis tests more robust.
19+
([#684](https://github.com/python-attrs/cattrs/pull/684))
20+
1421
## 25.2.0 (2025-08-31)
1522

1623
- **Potentially breaking**: Sequences are now structured into tuples.

src/cattrs/converters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ def __init__(
227227
)
228228
self._unstructure_func.register_func_list(
229229
[
230+
(
231+
lambda t: get_newtype_base(t) is not None,
232+
lambda o: self.unstructure(o, unstructure_as=o.__class__),
233+
),
230234
(
231235
is_protocol,
232236
lambda o: self.unstructure(o, unstructure_as=o.__class__),

tests/helpers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Helpers for tests."""
2+
3+
from typing import Any
4+
5+
6+
def assert_only_unstructured(obj: Any):
7+
"""Assert the object is comprised of only unstructured data:
8+
9+
* dicts, lists, tuples
10+
* strings, ints, floats, bools, None
11+
"""
12+
if isinstance(obj, dict):
13+
for k, v in obj.items():
14+
assert_only_unstructured(k)
15+
assert_only_unstructured(v)
16+
elif isinstance(obj, (list, tuple, frozenset, set)):
17+
for e in obj:
18+
assert_only_unstructured(e)
19+
else:
20+
assert isinstance(obj, (int, float, str, bool, type(None)))

tests/test_converter.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from cattrs.gen import make_dict_structure_fn, override
3333

3434
from ._compat import is_py310_plus
35+
from .helpers import assert_only_unstructured
3536
from .typed import (
3637
nested_typed_classes,
3738
simple_typed_attrs,
@@ -54,7 +55,7 @@ def test_simple_roundtrip(cls_and_vals, detailed_validation):
5455
cl, vals, kwargs = cls_and_vals
5556
inst = cl(*vals, **kwargs)
5657
unstructured = converter.unstructure(inst)
57-
assert "Hyp" not in repr(unstructured)
58+
assert_only_unstructured(unstructured)
5859
assert inst == converter.structure(unstructured, cl)
5960

6061

@@ -73,7 +74,7 @@ def test_simple_roundtrip_tuple(cls_and_vals, dv: bool):
7374
cl, vals, _ = cls_and_vals
7475
inst = cl(*vals)
7576
unstructured = converter.unstructure(inst)
76-
assert "Hyp" not in repr(unstructured)
77+
assert_only_unstructured(unstructured)
7778
assert inst == converter.structure(unstructured, cl)
7879

7980

@@ -125,7 +126,7 @@ def test_simple_roundtrip_with_extra_keys_forbidden(cls_and_vals, strat):
125126
assume(strat is UnstructureStrategy.AS_DICT or not kwargs)
126127
inst = cl(*vals, **kwargs)
127128
unstructured = converter.unstructure(inst)
128-
assert "Hyp" not in repr(unstructured)
129+
assert_only_unstructured(unstructured)
129130
assert inst == converter.structure(unstructured, cl)
130131

131132

tests/test_gen_dict.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError
1414
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
1515

16+
from .helpers import assert_only_unstructured
1617
from .typed import nested_typed_classes, simple_typed_classes, simple_typed_dataclasses
1718
from .untyped import nested_classes, simple_classes
1819

@@ -143,8 +144,7 @@ def test_individual_overrides(converter_cls, cl_and_vals):
143144
inst = cl(*vals, **kwargs)
144145

145146
res = converter.unstructure(inst)
146-
assert "Hyp" not in repr(res)
147-
assert "Factory" not in repr(res)
147+
assert_only_unstructured(res)
148148

149149
for attr, val in zip(fields, vals):
150150
if attr.name == chosen_name:
@@ -181,7 +181,7 @@ def test_unmodified_generated_structuring(cl_and_vals, dv: bool):
181181

182182
unstructured = converter.unstructure(inst)
183183

184-
assert "Hyp" not in repr(unstructured)
184+
assert_only_unstructured(unstructured)
185185

186186
converter.register_structure_hook(cl, fn)
187187

tests/test_helpers.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Tests for test helpers."""
2+
3+
import pytest
4+
from attrs import define
5+
6+
from .helpers import assert_only_unstructured
7+
8+
9+
def test_assert_only_unstructured_passes_for_primitives():
10+
"""assert_only_unstructured should pass for basic Python data types."""
11+
# Test primitives
12+
assert_only_unstructured(42)
13+
assert_only_unstructured("hello")
14+
assert_only_unstructured(3.14)
15+
assert_only_unstructured(True)
16+
assert_only_unstructured(None)
17+
18+
# Test collections of primitives
19+
assert_only_unstructured([1, 2, 3])
20+
assert_only_unstructured({"key": "value", "number": 42})
21+
assert_only_unstructured((1, "two", 3.0))
22+
assert_only_unstructured({1, 2, 3})
23+
assert_only_unstructured(frozenset([1, 2, 3]))
24+
25+
# Test nested structures
26+
assert_only_unstructured(
27+
{"list": [1, 2, {"nested": "dict"}], "tuple": (True, None), "number": 42}
28+
)
29+
30+
31+
def test_assert_only_unstructured_fails_for_attrs_classes():
32+
"""assert_only_unstructured should fail for attrs classes."""
33+
34+
@define
35+
class SimpleAttrsClass:
36+
value: int
37+
38+
instance = SimpleAttrsClass(42)
39+
40+
# Should raise AssertionError for attrs class instance
41+
with pytest.raises(AssertionError):
42+
assert_only_unstructured(instance)
43+
44+
# Should also fail when attrs instance is nested in collections
45+
with pytest.raises(AssertionError):
46+
assert_only_unstructured([instance])
47+
48+
with pytest.raises(AssertionError):
49+
assert_only_unstructured({"key": instance})
50+
51+
with pytest.raises(AssertionError):
52+
assert_only_unstructured((1, instance, 3))

0 commit comments

Comments
 (0)