Skip to content

Commit 2b7a42a

Browse files
committed
Tests for all_elements_must
1 parent 23dfdc2 commit 2b7a42a

File tree

4 files changed

+85
-33
lines changed

4 files changed

+85
-33
lines changed

src/cattrs/v/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@
88
IterableValidationError,
99
)
1010
from ._fluent import V, customize
11-
from ._validators import between, greater_than, ignoring_none, is_unique, len_between
11+
from ._validators import (
12+
all_elements_must,
13+
between,
14+
greater_than,
15+
ignoring_none,
16+
is_unique,
17+
len_between,
18+
)
1219

1320
__all__ = [
21+
"all_elements_must",
1422
"between",
1523
"customize",
1624
"format_exception",

src/cattrs/v/_fluent.py

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""The fluent validation API."""
22
from __future__ import annotations
33

4-
from typing import Any, Callable, Generic, Iterable, Literal, Sequence, TypeVar
4+
from typing import Any, Callable, Generic, Literal, Sequence, TypeVar
55

66
try:
77
from typing import assert_never
@@ -102,35 +102,6 @@ def omit(self) -> VOmitted:
102102
"""Omit the attribute."""
103103
return VOmitted(self.attr)
104104

105-
def replace_on_structure(self, value: T) -> VOmitted:
106-
"""This attribute should be replaced with a value when structuring."""
107-
return VOmitted(self.attr)
108-
109-
110-
def all_elements_must(
111-
validator: Callable[[T], None | bool], *validators: Callable[[T], None | bool]
112-
) -> Callable[[Iterable[T]], None | bool]:
113-
"""A helper validator included with cattrs.
114-
115-
Run all the given validators against all members of the
116-
iterable.
117-
"""
118-
119-
validators = (validator, *validators)
120-
121-
def assert_all_elements(val: Iterable[T]) -> None:
122-
errors = []
123-
for e in val:
124-
for v in validators:
125-
try:
126-
v(e)
127-
except Exception as exc:
128-
errors.append(exc)
129-
if errors:
130-
raise ExceptionGroup("", errors)
131-
132-
return assert_all_elements
133-
134105

135106
def _is_validator_factory(
136107
validator: Callable[[Any], None | bool] | ValidatorFactory[T]

src/cattrs/v/_validators.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

3-
from collections.abc import Hashable
3+
from collections.abc import Hashable, Iterable
44
from typing import Callable, Collection, Protocol, Sized, TypeVar
55

66
from .._compat import ExceptionGroup
7+
from ..errors import IterableValidationError, IterableValidationNote
78
from ._fluent import ValidatorFactory
89

910
T = TypeVar("T")
@@ -94,3 +95,50 @@ def skip_none(val: T | None, _validators=validators) -> None:
9495
return skip_none
9596

9697
return factory
98+
99+
100+
def all_elements_must(
101+
validator: Callable[[T], None | bool], *validators: Callable[[T], None | bool]
102+
) -> ValidatorFactory[T]:
103+
"""A helper validator included with cattrs.
104+
105+
Run all the given validators against all members of the
106+
iterable.
107+
"""
108+
109+
validators = (validator, *validators)
110+
111+
def factory(detailed_validation: bool) -> Callable[[T], None]:
112+
if detailed_validation:
113+
114+
def assert_all_elements(val: Iterable[T], _validators=validators) -> None:
115+
errors = []
116+
ix = 0
117+
for e in val:
118+
try:
119+
for v in _validators:
120+
try:
121+
v(e)
122+
except Exception as exc:
123+
exc.__notes__ = [
124+
*getattr(exc, "__notes__", []),
125+
IterableValidationNote(
126+
f"Validating @ index {ix}", ix, None
127+
),
128+
]
129+
errors.append(exc)
130+
finally:
131+
ix += 1
132+
if errors:
133+
raise IterableValidationError("", errors, val.__class__)
134+
135+
else:
136+
137+
def assert_all_elements(val: Iterable[T], _validators=validators) -> None:
138+
for e in val:
139+
for v in _validators:
140+
v(e)
141+
142+
return assert_all_elements
143+
144+
return factory

tests/v/test_validators.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from cattrs.errors import ClassValidationError
99
from cattrs.v import (
1010
V,
11+
all_elements_must,
1112
between,
1213
customize,
1314
greater_than,
@@ -167,4 +168,28 @@ def test_ignoring_none(converter: BaseConverter):
167168
with raises(ValueError) as exc_info:
168169
converter.structure({"a": 10}, WithOptional)
169170

170-
# assert repr(exc_info.value) == "invalid value (10 not between 0 and 5) @ $.a"
171+
assert repr(exc_info.value) == "ValueError('10 not between 0 and 5')"
172+
173+
174+
def test_all_elements_must(converter: BaseConverter):
175+
"""`all_elements_must` works."""
176+
177+
hook = customize(
178+
converter,
179+
WithList,
180+
V(f(WithList).a).ensure(all_elements_must(greater_than(5), between(5, 10))),
181+
)
182+
183+
assert hook({"a": []}, None) == WithList([])
184+
assert hook({"a": [6, 7, 8]}, None) == WithList([6, 7, 8])
185+
186+
if converter.detailed_validation:
187+
with raises(ClassValidationError) as exc_info:
188+
hook({"a": [1, 2]}, None)
189+
190+
assert transform_error(exc_info.value) == [
191+
"invalid value (1 not greater than 5) @ $.a[0]",
192+
"invalid value (1 not between 5 and 10) @ $.a[0]",
193+
"invalid value (2 not greater than 5) @ $.a[1]",
194+
"invalid value (2 not between 5 and 10) @ $.a[1]",
195+
]

0 commit comments

Comments
 (0)