Skip to content

Commit 5c5876e

Browse files
authored
typeddicts: raise proper error on invalid input (#616)
1 parent e2bdc84 commit 5c5876e

File tree

4 files changed

+25
-6
lines changed

4 files changed

+25
-6
lines changed

HISTORY.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
2020
([#577](https://github.com/python-attrs/cattrs/pull/577))
2121
- Expose {func}`cattrs.cols.mapping_unstructure_factory` through {mod}`cattrs.cols`.
2222
- Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and
23-
{func}`cattrs.cols.is_defaultdict`{func} and `cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.
23+
{func}`cattrs.cols.is_defaultdict` and {func}`cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.
2424
([#519](https://github.com/python-attrs/cattrs/issues/519) [#588](https://github.com/python-attrs/cattrs/pull/588))
2525
- Many preconf converters (_bson_, stdlib JSON, _cbor2_, _msgpack_, _msgspec_, _orjson_, _ujson_) skip unstructuring `int` and `str` enums,
2626
leaving them to the underlying libraries to handle with greater efficiency.
@@ -29,7 +29,9 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
2929
([#598](https://github.com/python-attrs/cattrs/pull/598))
3030
- Preconf converters now handle dictionaries with literal keys properly.
3131
([#599](https://github.com/python-attrs/cattrs/pull/599))
32-
- Replace `cattrs.gen.MappingStructureFn` with `cattrs.SimpleStructureHook[In, T]`.
32+
- Structuring TypedDicts from invalid inputs now properly raises a {class}`ClassValidationError`.
33+
([#615](https://github.com/python-attrs/cattrs/issues/615) [#616](https://github.com/python-attrs/cattrs/pull/616))
34+
- Replace `cattrs.gen.MappingStructureFn` with {class}`cattrs.SimpleStructureHook`.
3335
- Python 3.13 is now supported.
3436
([#543](https://github.com/python-attrs/cattrs/pull/543) [#547](https://github.com/python-attrs/cattrs/issues/547))
3537
- Python 3.8 is no longer supported, as it is end-of-life. Use previous versions on this Python version.

docs/defaulthooks.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ Generic TypedDicts work on Python 3.11 and later, since that is the first Python
333333

334334
[`typing.Required` and `typing.NotRequired`](https://peps.python.org/pep-0655/) are supported.
335335

336-
[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), un/structuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_structure_fn` and {meth}`cattrs.gen.typeddicts.make_dict_unstructure_fn`.
336+
[Similar to _attrs_ classes](customizing.md#using-cattrsgen-hook-factories), un/structuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_structure_fn` and {meth}`cattrs.gen.typeddicts.make_dict_unstructure_fn`.
337337

338338
```{doctest}
339339
>>> from typing import TypedDict

src/cattrs/gen/typeddicts.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,16 @@ def make_dict_structure_fn(
307307
globs["__c_a"] = allowed_fields
308308
globs["__c_feke"] = ForbiddenExtraKeysError
309309

310-
lines.append(" res = o.copy()")
311-
312310
if _cattrs_detailed_validation:
311+
# When running under detailed validation, be extra careful about copying
312+
# so that the correct error is raised if the input isn't a dict.
313+
lines.append(" try:")
314+
lines.append(" res = o.copy()")
315+
lines.append(" except Exception as exc:")
316+
lines.append(
317+
f" raise __c_cve('While structuring ' + {cl.__name__!r}, [exc], __cl)"
318+
)
319+
313320
lines.append(" errors = []")
314321
internal_arg_parts["__c_cve"] = ClassValidationError
315322
internal_arg_parts["__c_avn"] = AttributeValidationNote
@@ -383,6 +390,7 @@ def make_dict_structure_fn(
383390
f" if errors: raise __c_cve('While structuring ' + {cl.__name__!r}, errors, __cl)"
384391
)
385392
else:
393+
lines.append(" res = o.copy()")
386394
non_required = []
387395

388396
# The first loop deals with required args.

tests/test_typeddicts.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pytest import raises
1111
from typing_extensions import NotRequired, Required
1212

13-
from cattrs import BaseConverter, Converter
13+
from cattrs import BaseConverter, Converter, transform_error
1414
from cattrs._compat import ExtensionsTypedDict, get_notrequired_base, is_generic
1515
from cattrs.errors import (
1616
ClassValidationError,
@@ -509,3 +509,12 @@ class A(ExtensionsTypedDict):
509509

510510
assert converter.unstructure({"a": 10, "b": 10}, A) == {"a": 1, "b": 2}
511511
assert converter.structure({"a": 10, "b": 10}, A) == {"a": 1, "b": 2}
512+
513+
514+
def test_nondict_input():
515+
"""Trying to structure typeddict from a non-dict raises the proper exception."""
516+
converter = Converter(detailed_validation=True)
517+
with raises(ClassValidationError) as exc:
518+
converter.structure(1, TypedDictA)
519+
520+
assert transform_error(exc.value) == ["expected a mapping @ $"]

0 commit comments

Comments
 (0)