Skip to content

Commit fd5a9b8

Browse files
committed
Add ForbiddenExtraKeysError to ClassValidationError if used.
1 parent b2e47b5 commit fd5a9b8

File tree

3 files changed

+52
-19
lines changed

3 files changed

+52
-19
lines changed

src/cattrs/gen.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ def make_dict_structure_fn(
262262
resolve_types(cl)
263263

264264
allowed_fields = set()
265+
if _cattrs_forbid_extra_keys:
266+
globs["__c_a"] = allowed_fields
267+
globs["__c_feke"] = ForbiddenExtraKeysError
268+
265269
if _cattrs_detailed_validation:
266270
lines.append(" res = {}")
267271
lines.append(" errors = []")
@@ -327,6 +331,14 @@ def make_dict_structure_fn(
327331
f"{i}e.__note__ = 'Structuring class {cl.__qualname__} @ attribute {an}'"
328332
)
329333
lines.append(f"{i}errors.append(e)")
334+
335+
if _cattrs_forbid_extra_keys:
336+
post_lines += [
337+
" unknown_fields = set(o.keys()) - __c_a",
338+
" if unknown_fields:",
339+
" errors.append(__c_feke('', __cl, unknown_fields))",
340+
]
341+
330342
post_lines.append(
331343
f" if errors: raise __c_cve('While structuring {cl.__name__}', errors, __cl)"
332344
)
@@ -449,14 +461,12 @@ def make_dict_structure_fn(
449461
[" return __cl("] + [f" {line}" for line in invocation_lines] + [" )"]
450462
)
451463

452-
if _cattrs_forbid_extra_keys:
453-
globs["__c_a"] = allowed_fields
454-
globs["__c_feke"] = ForbiddenExtraKeysError
455-
lines += [
456-
" unknown_fields = set(o.keys()) - __c_a",
457-
" if unknown_fields:",
458-
" raise __c_feke('', __cl, unknown_fields)",
459-
]
464+
if _cattrs_forbid_extra_keys:
465+
post_lines += [
466+
" unknown_fields = set(o.keys()) - __c_a",
467+
" if unknown_fields:",
468+
" raise __c_feke('', __cl, unknown_fields)",
469+
]
460470

461471
# At the end, we create the function header.
462472
internal_arg_line = ", ".join([f"{i}={i}" for i in internal_arg_parts])

tests/metadata/test_genconverter.py

+26-9
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from cattr import UnstructureStrategy
2121
from cattr._compat import is_py39_plus, is_py310_plus
2222
from cattr.gen import make_dict_structure_fn, override
23-
from cattrs.errors import ForbiddenExtraKeysError
23+
from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError
2424

2525
from . import (
2626
nested_typed_classes,
@@ -88,11 +88,13 @@ def test_forbid_extra_keys(cls_and_vals):
8888
while bad_key in unstructured:
8989
bad_key += "A"
9090
unstructured[bad_key] = 1
91-
with pytest.raises(ForbiddenExtraKeysError) as feke:
91+
with pytest.raises(ClassValidationError) as cve:
9292
converter.structure(unstructured, cl)
9393

94-
assert feke.value.cl is cl
95-
assert feke.value.extra_fields == {bad_key}
94+
assert len(cve.value.exceptions) == 1
95+
assert isinstance(cve.value.exceptions[0], ForbiddenExtraKeysError)
96+
assert cve.value.exceptions[0].cl is cl
97+
assert cve.value.exceptions[0].extra_fields == {bad_key}
9698

9799

98100
@given(simple_typed_attrs(defaults=True))
@@ -106,11 +108,13 @@ def test_forbid_extra_keys_defaults(attr_and_vals):
106108
inst = cl()
107109
unstructured = converter.unstructure(inst)
108110
unstructured["aa"] = unstructured.pop("a")
109-
with pytest.raises(ForbiddenExtraKeysError) as feke:
111+
with pytest.raises(ClassValidationError) as cve:
110112
converter.structure(unstructured, cl)
111113

112-
assert feke.value.cl is cl
113-
assert feke.value.extra_fields == {"aa"}
114+
assert len(cve.value.exceptions) == 1
115+
assert isinstance(cve.value.exceptions[0], ForbiddenExtraKeysError)
116+
assert cve.value.exceptions[0].cl is cl
117+
assert cve.value.exceptions[0].extra_fields == {"aa"}
114118

115119

116120
def test_forbid_extra_keys_nested_override():
@@ -129,17 +133,30 @@ class A:
129133
converter.structure(unstructured, A)
130134
# if we break it in the subclass, we need it to raise
131135
unstructured["c"]["aa"] = 5
132-
with pytest.raises(ForbiddenExtraKeysError):
136+
with pytest.raises(ClassValidationError) as cve:
133137
converter.structure(unstructured, A)
138+
139+
assert len(cve.value.exceptions) == 1
140+
assert isinstance(cve.value.exceptions[0], ClassValidationError)
141+
assert len(cve.value.exceptions[0].exceptions) == 1
142+
assert isinstance(cve.value.exceptions[0].exceptions[0], ForbiddenExtraKeysError)
143+
assert cve.value.exceptions[0].exceptions[0].cl is C
144+
assert cve.value.exceptions[0].exceptions[0].extra_fields == {"aa"}
145+
134146
# we can "fix" that by disabling forbid_extra_keys on the subclass
135147
hook = make_dict_structure_fn(C, converter, _cattrs_forbid_extra_keys=False)
136148
converter.register_structure_hook(C, hook)
137149
converter.structure(unstructured, A)
138150
# but we should still raise at the top level
139151
unstructured["b"] = 6
140-
with pytest.raises(ForbiddenExtraKeysError):
152+
with pytest.raises(ClassValidationError) as cve:
141153
converter.structure(unstructured, A)
142154

155+
assert len(cve.value.exceptions) == 1
156+
assert isinstance(cve.value.exceptions[0], ForbiddenExtraKeysError)
157+
assert cve.value.exceptions[0].cl is A
158+
assert cve.value.exceptions[0].extra_fields == {"b"}
159+
143160

144161
@given(nested_typed_classes(defaults=True, min_attrs=1), unstructure_strats, booleans())
145162
def test_nested_roundtrip(cls_and_vals, strat, omit_if_default):

tests/test_gen_dict.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from cattr._compat import adapted_fields, fields
99
from cattrs import Converter
10-
from cattrs.errors import ForbiddenExtraKeysError
10+
from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError
1111
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
1212

1313
from . import nested_classes, simple_classes
@@ -227,9 +227,15 @@ class A:
227227

228228
assert new_inst == A(1, "str")
229229

230-
with pytest.raises(ForbiddenExtraKeysError):
230+
with pytest.raises(ClassValidationError) as cve:
231231
converter.structure({"b": 1, "c": "str"}, A)
232232

233+
assert len(cve.value.exceptions) == 2
234+
assert isinstance(cve.value.exceptions[0], KeyError)
235+
assert isinstance(cve.value.exceptions[1], ForbiddenExtraKeysError)
236+
assert cve.value.exceptions[1].cl is A
237+
assert cve.value.exceptions[1].extra_fields == {"c"}
238+
233239

234240
def test_omitting():
235241
converter = Converter()

0 commit comments

Comments
 (0)