Skip to content

Commit 4e7b0ff

Browse files
committed
test: 🚨 Add lots of new field validator tests
1 parent 160e768 commit 4e7b0ff

File tree

7 files changed

+246
-93
lines changed

7 files changed

+246
-93
lines changed

‎pydantic_async_validation/metaclasses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
ASYNC_MODEL_VALIDATORS_KEY,
1010
)
1111

12-
if TYPE_CHECKING:
12+
if TYPE_CHECKING: # pragma: no cover
1313
from pydantic_async_validation.validators import ValidationInfo
1414

1515

‎pydantic_async_validation/mixins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async def model_async_validate(self) -> None:
5151
field_name,
5252
field_validator,
5353
)
54-
except (ValueError, TypeError, AssertionError) as o_O:
54+
except (ValueError, AssertionError) as o_O:
5555
validation_errors.append(
5656
InitErrorDetails(
5757
type=PydanticCustomError('value_error', str(o_O)), # type: ignore
@@ -71,7 +71,7 @@ async def model_async_validate(self) -> None:
7171
self,
7272
model_validator,
7373
)
74-
except (ValueError, TypeError, AssertionError) as o_O:
74+
except (ValueError, AssertionError) as o_O:
7575
validation_errors.append(
7676
InitErrorDetails(
7777
type=PydanticCustomError('value_error', str(o_O)), # type: ignore

‎pydantic_async_validation/utils.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Callable, List, Tuple, Union, cast
44

55
from pydantic import PydanticUserError
6-
from pydantic_core import InitErrorDetails
6+
from pydantic_core import ErrorDetails, InitErrorDetails, PydanticCustomError
77

88

99
def make_generic_field_validator(validator_func: Callable) -> Callable:
@@ -19,19 +19,19 @@ def make_generic_field_validator(validator_func: Callable) -> Callable:
1919
f'Invalid signature for validator {validator_func}: {sig},'
2020
f'"cls" not permitted as first argument, '
2121
f'should be: (self, value, field, config), '
22-
f'"field" and "config" are all optional.',
22+
f'"value", "field" and "config" are all optional.',
2323
code='validator-signature',
2424
)
2525
return wraps(validator_func)(
2626
generic_field_validator_wrapper(
2727
validator_func,
2828
sig,
29-
set(args[1:]),
29+
set(args),
3030
),
3131
)
3232

3333

34-
all_field_validator_kwargs = {'field', 'validator'}
34+
all_field_validator_kwargs = {'value', 'field', 'config'}
3535

3636

3737
def generic_field_validator_wrapper(
@@ -53,30 +53,46 @@ def generic_field_validator_wrapper(
5353
f'Invalid signature for validator {validator_func}: {sig}, '
5454
f'should be: '
5555
f'(self, value, field, config), '
56-
f'"field" and "config" are all optional.',
56+
f'"value", "field" and "config" are all optional.',
5757
code='validator-signature',
5858
)
5959

6060
if has_kwargs:
61-
return lambda self, v, field, config: validator_func(
62-
self, v, field=field, config=config,
61+
return lambda self, value, field, config: validator_func(
62+
self, value=value, field=field, config=config,
6363
)
6464
if args == set():
65-
return lambda self, v, field, config: validator_func(
66-
self, v,
65+
return lambda self, value, field, config: validator_func(
66+
self,
67+
)
68+
if args == {'value'}:
69+
return lambda self, value, field, config: validator_func(
70+
self, value=value,
6771
)
6872
if args == {'field'}:
69-
return lambda self, v, field, config: validator_func(
70-
self, v, field=field,
73+
return lambda self, value, field, config: validator_func(
74+
self, field=field,
75+
)
76+
if args == {'value', 'field'}:
77+
return lambda self, value, field, config: validator_func(
78+
self, value=value, field=field,
7179
)
7280
if args == {'config'}:
73-
return lambda self, v, field, config: validator_func(
74-
self, v, config=config,
81+
return lambda self, value, field, config: validator_func(
82+
self, config=config,
83+
)
84+
if args == {'value', 'config'}:
85+
return lambda self, value, field, config: validator_func(
86+
self, value=value, config=config,
87+
)
88+
if args == {'field', 'config'}:
89+
return lambda self, value, field, config: validator_func(
90+
self, field=field, config=config,
7591
)
7692

77-
# args == {'field', 'validator'}
78-
return lambda self, v, field, config: validator_func(
79-
self, v, field=field, config=config,
93+
# args == {'value', 'field', 'validator'}
94+
return lambda self, value, field, config: validator_func(
95+
self, value=value, field=field, config=config,
8096
)
8197

8298

@@ -100,7 +116,7 @@ def make_generic_model_validator(validator_func: Callable) -> Callable:
100116
generic_model_validator_wrapper(
101117
validator_func,
102118
sig,
103-
set(args[1:]),
119+
set(args),
104120
),
105121
)
106122

@@ -148,7 +164,7 @@ def generic_model_validator_wrapper(
148164

149165
def prefix_errors(
150166
prefix: Tuple[Union[int, str], ...],
151-
errors: List[InitErrorDetails],
167+
errors: List[Union[InitErrorDetails, ErrorDetails]],
152168
) -> List[InitErrorDetails]:
153169
"""
154170
Extend all errors passed as list to include an additional prefix.
@@ -161,6 +177,18 @@ def prefix_errors(
161177
cast(
162178
InitErrorDetails,
163179
{
180+
# Original data is ErrorDetails, we need to convert it back to
181+
# InitErrorDetails
182+
**error,
183+
'type': PydanticCustomError(error['type'], error['msg']),
184+
'loc': (*prefix, *error['loc']),
185+
},
186+
)
187+
if "msg" in error
188+
else cast(
189+
InitErrorDetails,
190+
{
191+
# Original data is InitErrorDetails, all fine
164192
**error,
165193
'loc': (*prefix, *error['loc']),
166194
},

‎pydantic_async_validation/validators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic_async_validation.constants import ASYNC_FIELD_VALIDATOR_CONFIG_KEY, ASYNC_MODEL_VALIDATOR_CONFIG_KEY
77
from pydantic_async_validation.utils import make_generic_field_validator, make_generic_model_validator
88

9-
if TYPE_CHECKING:
9+
if TYPE_CHECKING: # pragma: no cover
1010
from inspect import Signature # noqa
1111

1212
from pydantic.main import BaseConfig # noqa
@@ -58,7 +58,7 @@ def async_field_validator(
5858

5959
if isinstance(__field_name, FunctionType):
6060
raise PydanticUserError(
61-
"validators should be used with fields and keyword arguments, "
61+
"Validators should be used with fields and keyword arguments, "
6262
"not bare. "
6363
"E.g. usage should be `@async_field_validator('<field_name>', ...)`",
6464
code='validator-instance-method',

‎pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ packages = [{include = "pydantic_async_validation"}]
1111
python = "^3.8"
1212
pydantic = ">=2.0.0,<3.0.0"
1313
fastapi = {version = ">=0.100.0,<1.0.0", optional = true}
14+
pytest = "^7.4.0"
1415

1516
[tool.poetry.extras]
1617
fastapi = ["fastapi"]

‎tests/test_field_validators.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from typing import Any, Dict, List
2+
3+
import pydantic
4+
import pytest
5+
from pydantic.errors import PydanticUserError
6+
7+
from pydantic_async_validation import AsyncValidationModelMixin, async_field_validator
8+
from pydantic_async_validation.validators import ValidationInfo
9+
10+
11+
class SomethingModel(AsyncValidationModelMixin, pydantic.BaseModel):
12+
name: str
13+
age: int
14+
15+
@async_field_validator('name')
16+
async def validate_name(self, value: str) -> None:
17+
if value == "invalid":
18+
raise ValueError("Invalid name")
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_async_validation_raises_no_issues():
23+
instance = SomethingModel(name="valid", age=1)
24+
await instance.model_async_validate()
25+
26+
27+
@pytest.mark.asyncio
28+
async def test_async_validation_raises_when_validation_fails():
29+
instance = SomethingModel(name="invalid", age=1)
30+
with pytest.raises(pydantic.ValidationError):
31+
await instance.model_async_validate()
32+
33+
34+
@pytest.mark.asyncio
35+
async def test_all_field_validator_combinations_are_valid():
36+
class OtherModel(AsyncValidationModelMixin, pydantic.BaseModel):
37+
name: str
38+
39+
@async_field_validator('name')
40+
async def validate_name_1(self) -> None: pass
41+
42+
@async_field_validator('name')
43+
async def validate_name_2(self, value: str) -> None: pass
44+
45+
@async_field_validator('name')
46+
async def validate_name_3(self, field: str) -> None: pass
47+
48+
@async_field_validator('name')
49+
async def validate_name_4(self, value: str, field: str) -> None: pass
50+
51+
@async_field_validator('name')
52+
async def validate_name_5(self, config: ValidationInfo) -> None: pass
53+
54+
@async_field_validator('name')
55+
async def validate_name_6(self, value: str, config: ValidationInfo) -> None: pass
56+
57+
@async_field_validator('name')
58+
async def validate_name_7(self, field: str, config: ValidationInfo) -> None: pass
59+
60+
@async_field_validator('name')
61+
async def validate_name_8(self, value: str, field: str, config: ValidationInfo) -> None: pass
62+
63+
instance = OtherModel(name="valid")
64+
await instance.model_async_validate()
65+
66+
67+
@pytest.mark.asyncio
68+
async def test_invalid_validators_are_prohibited():
69+
with pytest.raises(PydanticUserError):
70+
class OtherModel1(AsyncValidationModelMixin, pydantic.BaseModel):
71+
name: str
72+
73+
@async_field_validator('name')
74+
async def validate_name(self, no_value: Any) -> None: pass
75+
76+
with pytest.raises(PydanticUserError):
77+
class OtherModel2(AsyncValidationModelMixin, pydantic.BaseModel):
78+
name: str
79+
80+
@async_field_validator('name')
81+
async def validate_name(self, value: str, something_else: Any) -> None: pass
82+
83+
with pytest.raises(PydanticUserError):
84+
class OtherModel3(AsyncValidationModelMixin, pydantic.BaseModel):
85+
name: str
86+
87+
@async_field_validator('name')
88+
async def validate_name(cls, value: str) -> None: pass
89+
90+
91+
@pytest.mark.asyncio
92+
async def test_async_validation_may_get_extra_details():
93+
class OtherModel(AsyncValidationModelMixin, pydantic.BaseModel):
94+
name: str
95+
96+
@async_field_validator('name', some="thing")
97+
async def validate_name(self, config: ValidationInfo) -> None:
98+
assert config.extra == {"some": "thing"}
99+
100+
instance = OtherModel(name="valid")
101+
await instance.model_async_validate()
102+
103+
104+
@pytest.mark.asyncio
105+
async def test_async_validation_will_call_sub_model_validation():
106+
class OtherModel(AsyncValidationModelMixin, pydantic.BaseModel):
107+
something: SomethingModel
108+
somethings: List[SomethingModel]
109+
somethings_by_name: Dict[str, SomethingModel]
110+
111+
instance = OtherModel(
112+
something=SomethingModel(name="invalid", age=1),
113+
somethings=[SomethingModel(name="invalid", age=1)],
114+
somethings_by_name={"some": SomethingModel(name="invalid", age=1)},
115+
)
116+
with pytest.raises(pydantic.ValidationError) as O_o:
117+
await instance.model_async_validate()
118+
119+
assert len(O_o.value.errors()) == 3
120+
assert {e['loc'] for e in O_o.value.errors()} == {
121+
('something', 'name'),
122+
('somethings', 0, 'name'),
123+
('somethings_by_name', 'some', 'name'),
124+
}

0 commit comments

Comments
 (0)