diff --git a/src/cattrs/gen/typeddicts.py b/src/cattrs/gen/typeddicts.py index 5dd5d749..827f5086 100644 --- a/src/cattrs/gen/typeddicts.py +++ b/src/cattrs/gen/typeddicts.py @@ -2,6 +2,7 @@ import re import sys +from collections.abc import Mapping from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar from attrs import NOTHING, Attribute @@ -307,15 +308,18 @@ def make_dict_structure_fn( globs["__c_feke"] = ForbiddenExtraKeysError if _cattrs_detailed_validation: - # When running under detailed validation, be extra careful about copying - # so that the correct error is raised if the input isn't a dict. - lines.append(" try:") - lines.append(" res = o.copy()") - lines.append(" except Exception as exc:") + # When running under detailed validation, be extra careful about the + # input type so that the correct error is raised if the input isn't a dict. + internal_arg_parts["__c_mapping"] = Mapping + lines.append(" if not isinstance(o, __c_mapping):") + te = "TypeError(f'expected a mapping, not {o.__class__.__name__}')" lines.append( - f" raise __c_cve('While structuring ' + {cl.__name__!r}, [exc], __cl)" + f" raise __c_cve('While structuring ' + {cl.__name__!r}, [{te}], __cl)" ) + lines.append(" res = o.copy()") + + if _cattrs_detailed_validation: lines.append(" errors = []") internal_arg_parts["__c_cve"] = ClassValidationError internal_arg_parts["__c_avn"] = AttributeValidationNote @@ -389,7 +393,6 @@ def make_dict_structure_fn( f" if errors: raise __c_cve('While structuring ' + {cl.__name__!r}, errors, __cl)" ) else: - lines.append(" res = o.copy()") non_required = [] # The first loop deals with required args. diff --git a/src/cattrs/v.py b/src/cattrs/v.py index 5c40310d..134c990f 100644 --- a/src/cattrs/v.py +++ b/src/cattrs/v.py @@ -47,12 +47,6 @@ def format_exception(exc: BaseException, type: Union[type, None]) -> str: ): # This was supposed to be a mapping (and have .items()) but it something else. res = "expected a mapping" - elif isinstance(exc, AttributeError) and exc.args[0].endswith( - "object has no attribute 'copy'" - ): - # This was supposed to be a mapping (and have .copy()) but it something else. - # Used for TypedDicts. - res = "expected a mapping" else: res = f"unknown error ({exc})" diff --git a/tests/test_typeddicts.py b/tests/test_typeddicts.py index 7583d6aa..3456cd67 100644 --- a/tests/test_typeddicts.py +++ b/tests/test_typeddicts.py @@ -517,4 +517,13 @@ def test_nondict_input(): with raises(ClassValidationError) as exc: converter.structure(1, TypedDictA) - assert transform_error(exc.value) == ["expected a mapping @ $"] + assert transform_error(exc.value) == [ + "invalid type (expected a mapping, not int) @ $" + ] + + with raises(ClassValidationError) as exc: + converter.structure([1], TypedDictA) + + assert transform_error(exc.value) == [ + "invalid type (expected a mapping, not list) @ $" + ] diff --git a/tests/test_v.py b/tests/test_v.py index ac361be4..513027c6 100644 --- a/tests/test_v.py +++ b/tests/test_v.py @@ -323,7 +323,9 @@ class D(TypedDict): try: c.structure({"c": 1}, D) except Exception as exc: - assert transform_error(exc) == ["expected a mapping @ $.c"] + assert transform_error(exc) == [ + "invalid type (expected a mapping, not int) @ $.c" + ] try: c.structure({"c": {"a": "str"}}, D)