Skip to content

Commit 490893d

Browse files
raabfTinche
authored andcommitted
The exception ForbiddenExtraKeysError gets cl and extra_keys attributes.
1 parent 3dcd6fe commit 490893d

File tree

4 files changed

+41
-14
lines changed

4 files changed

+41
-14
lines changed

src/cattrs/errors.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Type
1+
from typing import Optional, Set, Type
22

33
from cattr._compat import ExceptionGroup
44

@@ -35,5 +35,23 @@ class ClassValidationError(BaseValidationError):
3535
pass
3636

3737

38-
class ForbiddenExtraKeyError(Exception):
39-
"""Raised when `forbid_extra_keys` is activated and such an extra key is detected during structuring."""
38+
class ForbiddenExtraKeysError(Exception):
39+
"""Raised when `forbid_extra_keys` is activated and such extra keys are detected during structuring.
40+
41+
The attribute `extra_fields` is a sequence of those extra keys, which were the cause of this error,
42+
and `cl` is the class which was structured with those extra keys.
43+
"""
44+
45+
def __init__(
46+
self, message: Optional[str], cl: Type, extra_fields: Set[str]
47+
) -> None:
48+
self.cl = cl
49+
self.extra_fields = extra_fields
50+
51+
msg = (
52+
message
53+
if message
54+
else f"Extra fields in constructor for {cl.__name__}: {', '.join(extra_fields)}"
55+
)
56+
57+
super().__init__(msg)

src/cattrs/gen.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
is_generic,
1818
)
1919
from cattr._generics import deep_copy_with
20-
from cattrs.errors import ClassValidationError, IterableValidationError
20+
from cattrs.errors import (
21+
ClassValidationError,
22+
ForbiddenExtraKeysError,
23+
IterableValidationError,
24+
)
2125

2226
if TYPE_CHECKING: # pragma: no cover
2327
from cattr.converters import Converter
@@ -447,13 +451,11 @@ def make_dict_structure_fn(
447451

448452
if _cattrs_forbid_extra_keys:
449453
globs["__c_a"] = allowed_fields
450-
globs["ForbiddenExtraKeyError"] = ForbiddenExtraKeyError
454+
globs["__c_feke"] = ForbiddenExtraKeysError
451455
post_lines += [
452456
" unknown_fields = set(o.keys()) - __c_a",
453457
" if unknown_fields:",
454-
" raise ForbiddenExtraKeyError(",
455-
f" 'Extra fields in constructor for {cl_name}: ' + ', '.join(unknown_fields)"
456-
" )",
458+
" raise __c_feke('', __cl, unknown_fields)",
457459
]
458460

459461
# At the end, we create the function header.

tests/metadata/test_genconverter.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
from cattr import GenConverter as Converter
2020
from cattr import UnstructureStrategy
2121
from cattr._compat import is_py39_plus, is_py310_plus
22-
from cattrs.errors import ForbiddenExtraKeyError
2322
from cattr.gen import make_dict_structure_fn, override
23+
from cattrs.errors import ForbiddenExtraKeysError
2424

2525
from . import (
2626
nested_typed_classes,
@@ -88,9 +88,12 @@ 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(ForbiddenExtraKeyError):
91+
with pytest.raises(ForbiddenExtraKeysError) as feke:
9292
converter.structure(unstructured, cl)
9393

94+
assert feke.value.cl is cl
95+
assert feke.value.extra_fields == {bad_key}
96+
9497

9598
@given(simple_typed_attrs(defaults=True))
9699
def test_forbid_extra_keys_defaults(attr_and_vals):
@@ -103,9 +106,12 @@ def test_forbid_extra_keys_defaults(attr_and_vals):
103106
inst = cl()
104107
unstructured = converter.unstructure(inst)
105108
unstructured["aa"] = unstructured.pop("a")
106-
with pytest.raises(ForbiddenExtraKeyError):
109+
with pytest.raises(ForbiddenExtraKeysError) as feke:
107110
converter.structure(unstructured, cl)
108111

112+
assert feke.value.cl is cl
113+
assert feke.value.extra_fields == {"aa"}
114+
109115

110116
def test_forbid_extra_keys_nested_override():
111117
@attr.s
@@ -123,15 +129,15 @@ class A:
123129
converter.structure(unstructured, A)
124130
# if we break it in the subclass, we need it to raise
125131
unstructured["c"]["aa"] = 5
126-
with pytest.raises(ForbiddenExtraKeyError):
132+
with pytest.raises(ForbiddenExtraKeysError):
127133
converter.structure(unstructured, A)
128134
# we can "fix" that by disabling forbid_extra_keys on the subclass
129135
hook = make_dict_structure_fn(C, converter, _cattrs_forbid_extra_keys=False)
130136
converter.register_structure_hook(C, hook)
131137
converter.structure(unstructured, A)
132138
# but we should still raise at the top level
133139
unstructured["b"] = 6
134-
with pytest.raises(ForbiddenExtraKeyError):
140+
with pytest.raises(ForbiddenExtraKeysError):
135141
converter.structure(unstructured, A)
136142

137143

tests/test_gen_dict.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from cattr._compat import adapted_fields, fields
99
from cattrs import Converter
10+
from cattrs.errors import ForbiddenExtraKeysError
1011
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
1112

1213
from . import nested_classes, simple_classes
@@ -226,7 +227,7 @@ class A:
226227

227228
assert new_inst == A(1, "str")
228229

229-
with pytest.raises(Exception):
230+
with pytest.raises(ForbiddenExtraKeysError):
230231
converter.structure({"b": 1, "c": "str"}, A)
231232

232233

0 commit comments

Comments
 (0)