Skip to content

Commit 82c4942

Browse files
committed
feat: ✨ Refactor validation to be a more straight forward implementation
1 parent d34d4db commit 82c4942

File tree

8 files changed

+153
-152
lines changed

8 files changed

+153
-152
lines changed

pydantic_async_validation/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
from pydantic_async_validation.exceptions import AsyncValidationError
21
from pydantic_async_validation.mixins import AsyncValidationModelMixin
32
from pydantic_async_validation.validators import async_field_validator, async_model_validator

pydantic_async_validation/exceptions.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

pydantic_async_validation/fastapi.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
from fastapi.exceptions import RequestValidationError
55
from pydantic import ValidationError
66

7-
from pydantic_async_validation.exceptions import AsyncValidationError
8-
97

108
class ensure_request_validation_errors():
119
"""
12-
Converter for ValidationErrors.
10+
Converter for `ValidationError` to `RequestValidationError`.
1311
14-
This will convert any ValidationError's inside the called code
15-
into RequestValidationErrors which will trigger HTTP 422 responses.
12+
This will convert any ValidationError's inside the called code into
13+
RequestValidationErrors which will trigger HTTP 422 responses in
14+
FastAPI. This is useful for when you want to do extra validation in
15+
your code that is not covered by FastAPI's normal request parameter
16+
handling.
1617
1718
Usage examples:
1819
@@ -56,5 +57,3 @@ def __exit__(
5657

5758
if isinstance(exc_value, ValidationError):
5859
raise RequestValidationError(errors=exc_value.errors())
59-
if isinstance(exc_value, AsyncValidationError):
60-
raise RequestValidationError(errors=exc_value.errors())

pydantic_async_validation/metaclasses.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __new__(
4141
)
4242

4343
for _attr_name, attr_value in namespace.items():
44-
# Register all validators
44+
# Register all field validators
4545
async_field_validator_fields, async_field_validator_config = getattr(
4646
attr_value,
4747
ASYNC_FIELD_VALIDATOR_CONFIG_KEY,
@@ -54,7 +54,7 @@ def __new__(
5454
):
5555
async_field_validators.append(attr_value)
5656

57-
# Register all root validators
57+
# Register all model validators
5858
async_model_validator_config = getattr(
5959
attr_value,
6060
ASYNC_MODEL_VALIDATOR_CONFIG_KEY,

pydantic_async_validation/mixins.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from typing import ClassVar, List, Tuple
22

33
import pydantic
4-
from pydantic_core import ErrorDetails
4+
from pydantic_core import ErrorDetails, PydanticCustomError, ValidationError
55

66
from pydantic_async_validation.constants import (
77
ASYNC_FIELD_VALIDATOR_CONFIG_KEY,
88
ASYNC_FIELD_VALIDATORS_KEY,
99
ASYNC_MODEL_VALIDATOR_CONFIG_KEY,
1010
ASYNC_MODEL_VALIDATORS_KEY,
1111
)
12-
from pydantic_async_validation.exceptions import AsyncValidationError
1312
from pydantic_async_validation.metaclasses import AsyncValidationModelMetaclass
1413
from pydantic_async_validation.validators import Validator
1514

@@ -23,6 +22,12 @@ class AsyncValidationModelMixin(
2322
pydantic_model_async_model_validators: ClassVar[List[Validator]]
2423

2524
async def model_async_validate(self) -> None:
25+
"""
26+
Run async validation for the model instance.
27+
28+
Will call all async field and async model validators. All errors will be
29+
collected and raised as a `ValidationError` exception.
30+
"""
2631
field_names: list[str]
2732
validator: Validator
2833

@@ -38,16 +43,15 @@ async def model_async_validate(self) -> None:
3843
for field_name in field_names:
3944
try:
4045
await validator.func(
41-
self.__class__,
42-
getattr(self, field_name, None),
4346
self,
47+
getattr(self, field_name, None),
4448
field_name,
4549
validator,
4650
)
4751
except (ValueError, TypeError, AssertionError) as o_O:
4852
validation_errors.append(
4953
ErrorDetails(
50-
type='value_error',
54+
type=PydanticCustomError('value_error', str(o_O)),
5155
msg=str(o_O),
5256
loc=(field_name,),
5357
input=getattr(self, field_name, None),
@@ -59,22 +63,28 @@ async def model_async_validate(self) -> None:
5963
validator_attr,
6064
ASYNC_MODEL_VALIDATOR_CONFIG_KEY,
6165
)
62-
if validator.skip_on_failure and validation_errors:
63-
continue
6466
try:
65-
await validator.func(self.__class__, self)
67+
await validator.func(
68+
self,
69+
validator,
70+
)
6671
except (ValueError, TypeError, AssertionError) as o_O:
6772
validation_errors.append(
6873
ErrorDetails(
69-
type='value_error',
74+
type=PydanticCustomError('value_error', str(o_O)),
7075
msg=str(o_O),
71-
loc=(),
76+
loc=('__root__',),
7277
input=self.__dict__,
7378
),
7479
)
7580

81+
# TODO:
82+
# for attribute_name, attribute_value in self.__dict__.items():
83+
# if isinstance(attribute_value, AsyncValidationModelMixin):
84+
# await attribute_value.model_async_validate()
85+
7686
if len(validation_errors) > 0:
77-
raise AsyncValidationError(
78-
errors=validation_errors,
79-
model=self.__class__,
87+
raise ValidationError.from_exception_data(
88+
self.__class__.__name__,
89+
validation_errors,
8090
)

pydantic_async_validation/utils.py

Lines changed: 96 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,40 @@
11
from functools import wraps
2-
from inspect import Signature
2+
from inspect import Signature, signature
33
from typing import Callable
44

5-
import pydantic
65
from pydantic import PydanticUserError
76

8-
_ASYNC_VALIDATOR_FUNCS: set[str] = set()
97

10-
11-
def prepare_validator(function: Callable, allow_reuse: bool = False) -> classmethod:
12-
"""
13-
Return the function as classmethod and check for duplicate names.
14-
15-
Avoid validators with duplicated names since without this,
16-
validators can be overwritten silently
17-
which generally isn't the intended behaviour,
18-
don't run in ipython (see #312) or if allow_reuse is False.
8+
def make_generic_field_validator(validator_func: Callable) -> Callable:
199
"""
20-
f_cls: classmethod = (
21-
function
22-
if isinstance(function, classmethod)
23-
else classmethod(function)
24-
)
25-
if not allow_reuse:
26-
ref = f_cls.__func__.__module__ + '.' + f_cls.__func__.__qualname__
27-
if ref in _ASYNC_VALIDATOR_FUNCS:
28-
# TODO: Does this still make sense? pydantic v2 seems to now need this?
29-
raise pydantic.PydanticUserError(
30-
f'Duplicate validator function "{ref}"; if this is intended, '
31-
f'set `allow_reuse=True`',
32-
# TODO: Find correct code for this - if we keep this exception
33-
code='validator-reuse', # type: ignore
34-
)
35-
_ASYNC_VALIDATOR_FUNCS.add(ref)
36-
return f_cls
37-
38-
39-
def make_generic_validator(validator: Callable) -> Callable:
10+
Make a generic function which calls a field validator with the right arguments.
4011
"""
41-
Make a generic function which calls a validator with the right arguments.
42-
43-
Unfortunately other approaches
44-
(eg. return a partial of a function that builds the arguments) is slow,
45-
hence this laborious way of doing things.
4612

47-
It's done like this so validators don't all need **kwargs in
48-
their signature, eg. any combination of
49-
the arguments "values", "fields" and/or "config" are permitted.
50-
"""
51-
from inspect import signature # noqa
52-
53-
sig = signature(validator)
13+
sig = signature(validator_func)
5414
args = list(sig.parameters.keys())
5515
first_arg = args.pop(0)
56-
if first_arg == 'self':
16+
if first_arg == 'cls':
5717
raise PydanticUserError(
58-
f'Invalid signature for validator {validator}: {sig},'
59-
f'"self" not permitted as first argument, '
60-
f'should be: (cls, value, instance, config, field), '
61-
f'"instance", "config" and "field" are all optional.',
18+
f'Invalid signature for validator {validator_func}: {sig},'
19+
f'"cls" not permitted as first argument, '
20+
f'should be: (self, value, field, config), '
21+
f'"field" and "config" are all optional.',
6222
code='validator-signature',
6323
)
64-
return wraps(validator)(
65-
generic_validator_cls(validator, sig, set(args[1:])),
24+
return wraps(validator_func)(
25+
generic_field_validator_wrapper(
26+
validator_func,
27+
sig,
28+
set(args[1:]),
29+
),
6630
)
6731

6832

69-
all_kwargs = {'instance', 'field', 'config'}
33+
all_field_validator_kwargs = {'field', 'validator'}
7034

7135

72-
def generic_validator_cls(
73-
validator: Callable,
36+
def generic_field_validator_wrapper(
37+
validator_func: Callable,
7438
sig: 'Signature',
7539
args: set[str],
7640
) -> Callable:
@@ -83,47 +47,99 @@ def generic_validator_cls(
8347
has_kwargs = True
8448
args -= {'kwargs'}
8549

86-
if not args.issubset(all_kwargs):
50+
if not args.issubset(all_field_validator_kwargs):
8751
raise PydanticUserError( # noqa
88-
f'Invalid signature for validator {validator}: {sig}, '
52+
f'Invalid signature for validator {validator_func}: {sig}, '
8953
f'should be: '
90-
f'(cls, value, instance, config, field), '
91-
f'"instance", "config" and "field" are all optional.',
54+
f'(self, value, field, config), '
55+
f'"field" and "config" are all optional.',
9256
code='validator-signature',
9357
)
9458

9559
if has_kwargs:
96-
return lambda cls, v, instance, field, config: validator(
97-
cls, v, instance=instance, field=field, config=config,
60+
return lambda self, v, field, config: validator_func(
61+
self, v, field=field, config=config,
9862
)
9963
if args == set():
100-
return lambda cls, v, instance, field, config: validator(cls, v)
101-
if args == {'instance'}:
102-
return lambda cls, v, instance, field, config: validator(
103-
cls, v, instance=instance,
64+
return lambda self, v, field, config: validator_func(
65+
self, v,
10466
)
10567
if args == {'field'}:
106-
return lambda cls, v, instance, field, config: validator(
107-
cls, v, field=field,
68+
return lambda self, v, field, config: validator_func(
69+
self, v, field=field,
10870
)
10971
if args == {'config'}:
110-
return lambda cls, v, instance, field, config: validator(
111-
cls, v, config=config,
72+
return lambda self, v, field, config: validator_func(
73+
self, v, config=config,
11274
)
113-
if args == {'instance', 'field'}:
114-
return lambda cls, v, instance, field, config: validator(
115-
cls, v, instance=instance, field=field,
75+
76+
# args == {'field', 'validator'}
77+
return lambda self, v, field, config: validator_func(
78+
self, v, field=field, config=config,
79+
)
80+
81+
82+
def make_generic_model_validator(validator_func: Callable) -> Callable:
83+
"""
84+
Make a generic function which calls a model validator with the right arguments.
85+
"""
86+
87+
sig = signature(validator_func)
88+
args = list(sig.parameters.keys())
89+
first_arg = args.pop(0)
90+
if first_arg == 'cls':
91+
raise PydanticUserError(
92+
f'Invalid signature for validator {validator_func}: {sig},'
93+
f'"cls" not permitted as first argument, '
94+
f'should be: (self, config), '
95+
f'"config" is optional.',
96+
code='validator-signature',
97+
)
98+
return wraps(validator_func)(
99+
generic_model_validator_wrapper(
100+
validator_func,
101+
sig,
102+
set(args[1:]),
103+
),
104+
)
105+
106+
107+
all_model_validator_kwargs = {'validator'}
108+
109+
110+
def generic_model_validator_wrapper(
111+
validator_func: Callable,
112+
sig: 'Signature',
113+
args: set[str],
114+
) -> Callable:
115+
"""
116+
Return a helper function to wrap a method to be called with its defined parameters.
117+
"""
118+
# assume the first argument is value
119+
has_kwargs = False
120+
if 'kwargs' in args:
121+
has_kwargs = True
122+
args -= {'kwargs'}
123+
124+
if not args.issubset(all_model_validator_kwargs):
125+
raise PydanticUserError( # noqa
126+
f'Invalid signature for validator {validator_func}: {sig}, '
127+
f'should be: '
128+
f'(self, config), '
129+
f'"config" is optional.',
130+
code='validator-signature',
116131
)
117-
if args == {'instance', 'config'}:
118-
return lambda cls, v, instance, field, config: validator(
119-
cls, v, instance=instance, config=config,
132+
133+
if has_kwargs:
134+
return lambda self, config: validator_func(
135+
self, config=config,
120136
)
121-
if args == {'field', 'config'}:
122-
return lambda cls, v, instance, field, config: validator(
123-
cls, v, field=field, config=config,
137+
if args == set():
138+
return lambda self, config: validator_func(
139+
self,
124140
)
125141

126-
# args == {'instance', 'field', 'config'}
127-
return lambda cls, v, instance, field, config: validator(
128-
cls, v, instance=instance, field=field, config=config,
142+
# args == {'validator'}
143+
return lambda self, config: validator_func(
144+
self, config=config,
129145
)

0 commit comments

Comments
 (0)