Skip to content

Commit 5d23a08

Browse files
authored
Bump test dependencies (#673)
* Bump some test dependencies * Tweak tests for coverage * Small test refactor * Rebump coverage, pin Hypothesis * Fix tests * Docs and test * Bump Hypothesis, rework `FAST` * Move tests around * Remove unused imports * Refactor `kw_only` to a FeatureFlag * Remove unused import
1 parent 5b18ffd commit 5d23a08

File tree

11 files changed

+361
-308
lines changed

11 files changed

+361
-308
lines changed

HISTORY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
3232
- For {class}`cattrs.errors.StructureHandlerNotFoundError` and {class}`cattrs.errors.ForbiddenExtraKeysError`
3333
correctly set {attr}`BaseException.args` in `super()` and hence make them pickable.
3434
([#666](https://github.com/python-attrs/cattrs/pull/666))
35+
- The default disambiguation hook factory is now only enabled for converters with `unstructure_strat=AS_DICT` (the default).
36+
Since the strategy doesn't support tuples, it is skipped for `unstructure_strat=AS_TUPLE` converters.
37+
([#673](https://github.com/python-attrs/cattrs/pull/673))
3538

3639
## 25.1.1 (2025-06-04)
3740

pdm.lock

Lines changed: 92 additions & 96 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ lint = [
77
"ruff>=0.12.2",
88
]
99
test = [
10-
"hypothesis>=6.111.2",
11-
"pytest>=8.3.2",
12-
"pytest-benchmark>=4.0.0",
13-
"immutables>=0.20",
14-
"coverage>=7.6.1",
15-
"pytest-xdist>=3.6.1",
10+
"hypothesis>=6.135.26",
11+
"pytest>=8.4.1",
12+
"pytest-benchmark>=5.1.0",
13+
"immutables>=0.21",
14+
"coverage>=7.9.2",
15+
"pytest-xdist>=3.8.0",
1616
]
1717
docs = [
1818
"sphinx>=5.3.0",

src/cattrs/converters.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,11 @@ def __init__(
281281
(is_tuple, self._structure_tuple),
282282
(is_namedtuple, namedtuple_structure_factory, "extended"),
283283
(is_mapping, self._structure_dict),
284-
(is_supported_union, self._gen_attrs_union_structure, True),
284+
*(
285+
[(is_supported_union, self._gen_attrs_union_structure, True)]
286+
if unstruct_strat is UnstructureStrategy.AS_DICT
287+
else []
288+
),
285289
(is_optional, self._structure_optional),
286290
(
287291
lambda t: is_union_type(t) and t in self._union_struct_registry,

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def converter_cls(request):
2828
)
2929
settings.register_profile("fast", settings.get_profile("tests"), max_examples=10)
3030

31-
settings.load_profile("fast" if "FAST" in environ else "tests")
31+
settings.load_profile("fast" if environ.get("FAST") == "1" else "tests")
3232

3333
collect_ignore_glob = []
3434
if sys.version_info < (3, 10):

tests/test_baseconverter.py

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from hypothesis.strategies import just, one_of
99

1010
from cattrs import BaseConverter, UnstructureStrategy
11+
from cattrs.errors import StructureHandlerNotFoundError
1112

1213
from ._compat import is_py310_plus
1314
from .typed import nested_typed_classes, simple_typed_attrs, simple_typed_classes
@@ -28,7 +29,7 @@ def test_simple_roundtrip(cls_and_vals, strat):
2829

2930

3031
@given(
31-
simple_typed_attrs(defaults=True, newtypes=False, allow_nan=False),
32+
simple_typed_attrs(defaults="always", newtypes=False, allow_nan=False),
3233
unstructure_strats,
3334
)
3435
def test_simple_roundtrip_defaults(attr_and_strat, strat):
@@ -58,7 +59,7 @@ def test_nested_roundtrip(cls_and_vals):
5859
assert inst == converter.structure(converter.unstructure(inst), cl)
5960

6061

61-
@given(nested_typed_classes(kw_only=False, newtypes=False, allow_nan=False))
62+
@given(nested_typed_classes(kw_only="never", newtypes=False, allow_nan=False))
6263
def test_nested_roundtrip_tuple(cls_and_vals):
6364
"""
6465
Nested classes with metadata can be unstructured and restructured.
@@ -71,24 +72,24 @@ def test_nested_roundtrip_tuple(cls_and_vals):
7172
assert inst == converter.structure(converter.unstructure(inst), cl)
7273

7374

74-
@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow])
75+
@settings(suppress_health_check=[HealthCheck.too_slow])
7576
@given(
76-
simple_typed_classes(defaults=False, newtypes=False, allow_nan=False),
77-
simple_typed_classes(defaults=False, newtypes=False, allow_nan=False),
78-
unstructure_strats,
77+
simple_typed_classes(
78+
defaults="never", newtypes=False, allow_nan=False, min_attrs=2
79+
),
80+
simple_typed_classes(
81+
defaults="never", newtypes=False, allow_nan=False, min_attrs=1
82+
),
7983
)
80-
def test_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat):
84+
def test_union_field_roundtrip_dict(cl_and_vals_a, cl_and_vals_b):
8185
"""
8286
Classes with union fields can be unstructured and structured.
8387
"""
84-
converter = BaseConverter(unstruct_strat=strat)
88+
converter = BaseConverter()
8589
cl_a, vals_a, kwargs_a = cl_and_vals_a
86-
assume(strat is UnstructureStrategy.AS_DICT or not kwargs_a)
87-
cl_b, vals_b, _ = cl_and_vals_b
90+
cl_b, _, _ = cl_and_vals_b
8891
a_field_names = {a.name for a in fields(cl_a)}
8992
b_field_names = {a.name for a in fields(cl_b)}
90-
assume(a_field_names)
91-
assume(b_field_names)
9293

9394
common_names = a_field_names & b_field_names
9495
assume(len(a_field_names) > len(common_names))
@@ -99,25 +100,55 @@ class C:
99100

100101
inst = C(a=cl_a(*vals_a, **kwargs_a))
101102

102-
if strat is UnstructureStrategy.AS_DICT:
103-
assert inst == converter.structure(converter.unstructure(inst), C)
104-
else:
105-
# Our disambiguation functions only support dictionaries for now.
106-
with pytest.raises(ValueError):
107-
converter.structure(converter.unstructure(inst), C)
103+
unstructured = converter.unstructure(inst)
104+
assert inst == converter.structure(converter.unstructure(unstructured), C)
108105

109-
def handler(obj, _):
110-
return converter.structure(obj, cl_a)
111106

112-
converter.register_structure_hook(Union[cl_a, cl_b], handler)
113-
assert inst == converter.structure(converter.unstructure(inst), C)
107+
@settings(suppress_health_check=[HealthCheck.too_slow])
108+
@given(
109+
simple_typed_classes(
110+
defaults="never", newtypes=False, allow_nan=False, kw_only="never", min_attrs=2
111+
),
112+
simple_typed_classes(
113+
defaults="never", newtypes=False, allow_nan=False, kw_only="never", min_attrs=1
114+
),
115+
)
116+
def test_union_field_roundtrip_tuple(cl_and_vals_a, cl_and_vals_b):
117+
"""
118+
Classes with union fields can be unstructured and structured.
119+
"""
120+
converter = BaseConverter(unstruct_strat=UnstructureStrategy.AS_TUPLE)
121+
cl_a, vals_a, _ = cl_and_vals_a
122+
cl_b, _, _ = cl_and_vals_b
123+
124+
@define
125+
class C:
126+
a: Union[cl_a, cl_b]
127+
128+
inst = C(a=cl_a(*vals_a))
129+
130+
# Our disambiguation functions only support dictionaries for now.
131+
raw = converter.unstructure(inst)
132+
with pytest.raises(StructureHandlerNotFoundError):
133+
converter.structure(raw, C)
134+
135+
def handler(obj, _):
136+
return converter.structure(obj, cl_a)
137+
138+
converter.register_structure_hook(Union[cl_a, cl_b], handler)
139+
unstructured = converter.unstructure(inst)
140+
assert inst == converter.structure(unstructured, C)
114141

115142

116143
@pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax")
117-
@settings(suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow])
144+
@settings(suppress_health_check=[HealthCheck.too_slow])
118145
@given(
119-
simple_typed_classes(defaults=False, newtypes=False, allow_nan=False),
120-
simple_typed_classes(defaults=False, newtypes=False, allow_nan=False),
146+
simple_typed_classes(
147+
defaults="never", newtypes=False, allow_nan=False, min_attrs=1
148+
),
149+
simple_typed_classes(
150+
defaults="never", newtypes=False, allow_nan=False, min_attrs=1
151+
),
121152
unstructure_strats,
122153
)
123154
def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat):
@@ -126,12 +157,10 @@ def test_310_union_field_roundtrip(cl_and_vals_a, cl_and_vals_b, strat):
126157
"""
127158
converter = BaseConverter(unstruct_strat=strat)
128159
cl_a, vals_a, kwargs_a = cl_and_vals_a
129-
cl_b, vals_b, _ = cl_and_vals_b
160+
cl_b, _, _ = cl_and_vals_b
130161
assume(strat is UnstructureStrategy.AS_DICT or not kwargs_a)
131162
a_field_names = {a.name for a in fields(cl_a)}
132163
b_field_names = {a.name for a in fields(cl_b)}
133-
assume(a_field_names)
134-
assume(b_field_names)
135164

136165
common_names = a_field_names & b_field_names
137166
assume(len(a_field_names) > len(common_names))
@@ -146,7 +175,7 @@ class C:
146175
assert inst == converter.structure(converter.unstructure(inst), C)
147176
else:
148177
# Our disambiguation functions only support dictionaries for now.
149-
with pytest.raises(ValueError):
178+
with pytest.raises(StructureHandlerNotFoundError):
150179
converter.structure(converter.unstructure(inst), C)
151180

152181
def handler(obj, _):
@@ -156,7 +185,7 @@ def handler(obj, _):
156185
assert inst == converter.structure(converter.unstructure(inst), C)
157186

158187

159-
@given(simple_typed_classes(defaults=False, newtypes=False, allow_nan=False))
188+
@given(simple_typed_classes(defaults="never", newtypes=False, allow_nan=False))
160189
def test_optional_field_roundtrip(cl_and_vals):
161190
"""
162191
Classes with optional fields can be unstructured and structured.
@@ -178,7 +207,7 @@ class C:
178207

179208

180209
@pytest.mark.skipif(not is_py310_plus, reason="3.10+ union syntax")
181-
@given(simple_typed_classes(defaults=False, newtypes=False, allow_nan=False))
210+
@given(simple_typed_classes(defaults="never", newtypes=False, allow_nan=False))
182211
def test_310_optional_field_roundtrip(cl_and_vals):
183212
"""
184213
Classes with optional fields can be unstructured and structured.

0 commit comments

Comments
 (0)