Skip to content

Commit 88c6cf2

Browse files
committed
Improve Hypothesis test robustness
1 parent 727aa89 commit 88c6cf2

File tree

5 files changed

+36
-6
lines changed

5 files changed

+36
-6
lines changed

HISTORY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ 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 BaseConverter.
17+
- Make some Hypothesis tests more robust.
18+
1419
## 25.2.0 (2025-08-31)
1520

1621
- **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/asserts.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Helpers for assertions."""
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 .asserts 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 .asserts 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

0 commit comments

Comments
 (0)