Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions src/cattrs/gen/typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 0 additions & 6 deletions src/cattrs/v.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})"

Expand Down
11 changes: 10 additions & 1 deletion tests/test_typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) @ $"
]
4 changes: 3 additions & 1 deletion tests/test_v.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down