Skip to content

Commit

Permalink
typeddicts: improve error message on invalid input (#617)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche authored Jan 7, 2025
1 parent 527291f commit f4a7385
Show file tree
Hide file tree
Showing 4 changed files with 23 additions and 15 deletions.
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

0 comments on commit f4a7385

Please sign in to comment.