Skip to content

Commit

Permalink
Add test
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche committed Dec 29, 2024
1 parent 31b6412 commit 2f141aa
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 16 deletions.
92 changes: 84 additions & 8 deletions src/cattrs/strategies/_listfromdict.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@

from .. import BaseConverter, SimpleStructureHook
from ..dispatch import UnstructureHook
from ..errors import (
AttributeValidationNote,
ClassValidationError,
IterableValidationError,
IterableValidationNote,
)
from ..fns import identity
from ..gen.typeddicts import is_typeddict

Expand Down Expand Up @@ -48,14 +54,84 @@ def configure_list_from_dict(
if isinstance(field, Attribute):
field = field.name

def structure_hook(
value: Mapping,
_: Any = seq_type,
_arg_type=arg_type,
_arg_hook=arg_structure_hook,
_field=field,
) -> list[T]:
return [_arg_hook(v | {_field: k}, _arg_type) for k, v in value.items()]
if converter.detailed_validation:

def structure_hook(
value: Mapping,
_: Any = seq_type,
_arg_type=arg_type,
_arg_hook=arg_structure_hook,
_field=field,
) -> list[T]:
res = []
errors = []
for k, v in value.items():
try:
res.append(_arg_hook(v | {_field: k}, _arg_type))
except ClassValidationError as exc:
# Rewrite the notes of any errors relating to `_field`
non_key_exceptions = []
key_exceptions = []
for inner_exc in exc.exceptions:
if not (existing := getattr(inner_exc, "__notes__", [])):
non_key_exceptions.append(inner_exc)
continue
for note in existing:
if not isinstance(note, AttributeValidationNote):
continue
if note.name == _field:
inner_exc.__notes__.remove(note)
inner_exc.__notes__.append(
IterableValidationNote(
f"Structuring mapping key @ key {k!r}",
note.name,
note.type,
)
)
key_exceptions.append(inner_exc)
break
else:
non_key_exceptions.append(inner_exc)

if non_key_exceptions != exc.exceptions:
if non_key_exceptions:
errors.append(
new_exc := ClassValidationError(
exc.message, non_key_exceptions, exc.cl
)
)
new_exc.__notes__ = [
*getattr(exc, "__notes__", []),
IterableValidationNote(
"Structuring mapping value @ key {k!r}",
k,
_arg_type,
),
]
else:
exc.__notes__ = [
*getattr(exc, "__notes__", []),
IterableValidationNote(
"Structuring mapping value @ key {k!r}", k, _arg_type
),
]
errors.append(exc)
if key_exceptions:
errors.extend(key_exceptions)
if errors:
raise IterableValidationError("While structuring", errors, dict)
return res

else:

def structure_hook(
value: Mapping,
_: Any = seq_type,
_arg_type=arg_type,
_arg_hook=arg_structure_hook,
_field=field,
) -> list[T]:
return [_arg_hook(v | {_field: k}, _arg_type) for k, v in value.items()]

arg_unstructure_hook = converter.get_unstructure_hook(arg_type, cache_result=False)

Expand Down
55 changes: 47 additions & 8 deletions tests/strategies/test_list_from_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,28 @@
import pytest
from attrs import define, fields

from cattrs import BaseConverter
from cattrs import BaseConverter, transform_error
from cattrs.converters import Converter
from cattrs.errors import IterableValidationError
from cattrs.gen import make_dict_structure_fn, override
from cattrs.strategies import configure_list_from_dict


@define
class AttrsA:
a: int
b: str
b: int


@dataclass
class DataclassA:
a: int
b: str
b: int


class TypedDictA(TypedDict):
a: int
b: str
b: int


@pytest.mark.parametrize("cls", [AttrsA, DataclassA, TypedDictA])
Expand All @@ -33,18 +36,54 @@ def test_simple_roundtrip(
):
hook, hook2 = configure_list_from_dict(list[cls], "a", converter)

structured = [cls(a=1, b="2"), cls(a=3, b="4")]
structured = [cls(a=1, b=2), cls(a=3, b=4)]
unstructured = hook2(structured)
assert unstructured == {1: {"b": "2"}, 3: {"b": "4"}}
assert unstructured == {1: {"b": 2}, 3: {"b": 4}}

assert hook(unstructured) == structured


def test_simple_roundtrip_attrs(converter: BaseConverter):
hook, hook2 = configure_list_from_dict(list[AttrsA], fields(AttrsA).a, converter)

structured = [AttrsA(a=1, b="2"), AttrsA(a=3, b="4")]
structured = [AttrsA(a=1, b=2), AttrsA(a=3, b=4)]
unstructured = hook2(structured)
assert unstructured == {1: {"b": "2"}, 3: {"b": "4"}}
assert unstructured == {1: {"b": 2}, 3: {"b": 4}}

assert hook(unstructured) == structured


def test_validation_errors():
"""
With detailed validation, validation errors should be adjusted for the
extracted keys.
"""
conv = Converter(detailed_validation=True)
hook, _ = configure_list_from_dict(list[AttrsA], "a", conv)

# Key failure
with pytest.raises(IterableValidationError) as exc:
hook({"a": {"b": "1"}})

assert transform_error(exc.value) == [
"invalid value for type, expected int @ $['a']"
]

# Value failure
with pytest.raises(IterableValidationError) as exc:
hook({1: {"b": "a"}})

assert transform_error(exc.value) == [
"invalid value for type, expected int @ $[1].b"
]

conv.register_structure_hook(
AttrsA, make_dict_structure_fn(AttrsA, conv, _cattrs_forbid_extra_keys=True)
)
hook, _ = configure_list_from_dict(list[AttrsA], "a", conv)

# Value failure, not attribute related
with pytest.raises(IterableValidationError) as exc:
hook({1: {"b": 1, "c": 2}})

assert transform_error(exc.value) == ["extra fields found (c) @ $[1]"]

0 comments on commit 2f141aa

Please sign in to comment.