Skip to content

Commit 4355120

Browse files
committed
Work on dicts
1 parent e9e2c77 commit 4355120

File tree

2 files changed

+102
-10
lines changed

2 files changed

+102
-10
lines changed

src/cattrs/v/__init__.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Cattrs validation."""
2-
from typing import Any, Callable, List, Tuple, Type, TypeVar, Union, overload
2+
from typing import Any, Callable, Dict, List, Tuple, Type, TypeVar, Union, overload
33

44
from attrs import NOTHING, frozen
55

@@ -152,6 +152,7 @@ def transform_error(
152152

153153
T = TypeVar("T")
154154
E = TypeVar("E")
155+
TV = TypeVar("TV")
155156

156157

157158
@overload
@@ -161,15 +162,30 @@ def ensure(
161162
...
162163

163164

165+
@overload
166+
def ensure(
167+
type: Type[Dict],
168+
*validators: Callable[[Dict], Any],
169+
keys: Type[E],
170+
values: Type[TV],
171+
) -> Type[Dict[E, TV]]:
172+
...
173+
174+
164175
@overload
165176
def ensure(type: Type[T], *validators: Callable[[T], Any]) -> Type[T]:
166177
...
167178

168179

169-
def ensure(type: Any, *validators: Any, elems: Any = NOTHING) -> Any:
180+
def ensure(type, *validators, elems=NOTHING, keys=NOTHING, values=NOTHING):
181+
"""Ensure validators run when structuring the given type."""
170182
if elems is not NOTHING:
171183
# These are lists.
172184
if not validators:
173185
return type[elems]
174-
return Annotated[type, VAnnotation(*validators)]
186+
return Annotated[type[elems], VAnnotation(*validators)]
187+
if keys is not NOTHING or values is not NOTHING:
188+
if not validators:
189+
return type[keys, values]
190+
return Annotated[type[keys, values], VAnnotation(*validators)]
175191
return Annotated[type, VAnnotation(*validators)]

tests/v/test_ensure.py

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
"""Tests for `cattrs.v.ensure`."""
22
import sys
3-
from typing import List, MutableSequence, Sequence
3+
from typing import Dict, List, MutableSequence, Sequence
44

55
from pytest import fixture, mark, raises
66

77
from cattrs import BaseConverter
88
from cattrs._compat import ExceptionGroup
9+
from cattrs.errors import IterableValidationError
910
from cattrs.v import ensure
1011
from cattrs.v._hooks import is_validated, validator_factory
1112

1213

1314
@fixture
1415
def valconv(converter) -> BaseConverter:
15-
converter.register_structure_hook_factory(is_validated)(validator_factory)
16+
converter.register_structure_hook_factory(is_validated, validator_factory)
1617
return converter
1718

1819

@@ -38,7 +39,7 @@ def test_ensured_lists(valconv: BaseConverter):
3839
valconv.structure([], ensure(List[int], lambda lst: len(lst) > 0))
3940

4041
if valconv.detailed_validation:
41-
assert isinstance(exc.value, ExceptionGroup)
42+
assert isinstance(exc.value, IterableValidationError)
4243
assert isinstance(exc.value.exceptions[0], ValueError)
4344
else:
4445
assert isinstance(exc.value, ValueError)
@@ -53,7 +54,53 @@ def test_ensured_list_elements(valconv: BaseConverter, type):
5354
valconv.structure([1, -2], ensure(type, elems=ensure(int, lambda i: i > 0)))
5455

5556
if valconv.detailed_validation:
56-
assert isinstance(exc.value, ExceptionGroup)
57+
assert isinstance(exc.value, IterableValidationError)
58+
assert isinstance(exc.value.exceptions[0], ExceptionGroup)
59+
assert isinstance(exc.value.exceptions[0].exceptions[0], ValueError)
60+
else:
61+
assert isinstance(exc.value, ValueError)
62+
63+
# Now both elements and the list itself.
64+
assert valconv.structure(
65+
[1, 2],
66+
ensure(type, lambda lst: len(lst) < 3, elems=ensure(int, lambda i: i > 0)),
67+
)
68+
69+
with raises(Exception) as exc:
70+
valconv.structure(
71+
[1, 2, 3],
72+
ensure(type, lambda lst: len(lst) < 3, elems=ensure(int, lambda i: i > 0)),
73+
)
74+
75+
if valconv.detailed_validation:
76+
assert isinstance(exc.value, IterableValidationError)
77+
assert isinstance(exc.value.exceptions[0], ValueError)
78+
else:
79+
assert isinstance(exc.value, ValueError)
80+
81+
with raises(Exception) as exc:
82+
valconv.structure(
83+
[1, -2],
84+
ensure(type, lambda lst: len(lst) < 3, elems=ensure(int, lambda i: i > 0)),
85+
)
86+
87+
if valconv.detailed_validation:
88+
assert isinstance(exc.value, IterableValidationError)
89+
assert isinstance(exc.value.exceptions[0], ExceptionGroup)
90+
assert isinstance(exc.value.exceptions[0].exceptions[0], ValueError)
91+
else:
92+
assert isinstance(exc.value, ValueError)
93+
94+
95+
def test_ensured_typing_list(valconv: BaseConverter):
96+
"""Ensure works for typing lists."""
97+
assert valconv.structure([1, 2], ensure(List, elems=ensure(int, lambda i: i > 0)))
98+
99+
with raises(Exception) as exc:
100+
valconv.structure([1, -2], ensure(List, elems=ensure(int, lambda i: i > 0)))
101+
102+
if valconv.detailed_validation:
103+
assert isinstance(exc.value, IterableValidationError)
57104
assert isinstance(exc.value.exceptions[0], ExceptionGroup)
58105
assert isinstance(exc.value.exceptions[0].exceptions[0], ValueError)
59106
else:
@@ -63,13 +110,42 @@ def test_ensured_list_elements(valconv: BaseConverter, type):
63110
@mark.skipif(sys.version_info[:2] < (3, 10), reason="Not supported on older Pythons")
64111
def test_ensured_list(valconv: BaseConverter):
65112
"""Ensure works for builtin lists."""
66-
assert valconv.structure([1, 2], ensure(List, elems=ensure(int, lambda i: i > 0)))
113+
assert valconv.structure([1, 2], ensure(list, elems=ensure(int, lambda i: i > 0)))
67114

68115
with raises(Exception) as exc:
69-
valconv.structure([1, -2], ensure(List, elems=ensure(int, lambda i: i > 0)))
116+
valconv.structure([1, -2], ensure(list, elems=ensure(int, lambda i: i > 0)))
70117

71118
if valconv.detailed_validation:
72-
assert isinstance(exc.value, ExceptionGroup)
119+
assert isinstance(exc.value, IterableValidationError)
120+
assert isinstance(exc.value.exceptions[0], ExceptionGroup)
121+
assert isinstance(exc.value.exceptions[0].exceptions[0], ValueError)
122+
else:
123+
assert isinstance(exc.value, ValueError)
124+
125+
126+
def test_ensured_typing_dict(valconv: BaseConverter):
127+
"""Ensure works for typing.Dicts."""
128+
assert valconv.structure(
129+
{"a": 1}, ensure(Dict, lambda d: len(d) > 0, keys=str, values=int)
130+
)
131+
132+
with raises(Exception) as exc:
133+
valconv.structure({}, ensure(Dict, lambda d: len(d) > 0, keys=str, values=int))
134+
135+
if valconv.detailed_validation:
136+
assert isinstance(exc.value, IterableValidationError)
137+
assert isinstance(exc.value.exceptions[0], ValueError)
138+
else:
139+
assert isinstance(exc.value, ValueError)
140+
141+
with raises(Exception) as exc:
142+
valconv.structure(
143+
{"b": 1, "c": "a"},
144+
ensure(Dict, keys=ensure(str, lambda s: s.startswith("a")), values=int),
145+
)
146+
147+
if valconv.detailed_validation:
148+
assert isinstance(exc.value, IterableValidationError)
73149
assert isinstance(exc.value.exceptions[0], ExceptionGroup)
74150
assert isinstance(exc.value.exceptions[0].exceptions[0], ValueError)
75151
else:

0 commit comments

Comments
 (0)