Files
pydantic/pydantic/_internal/_validation_functions.py
T
Samuel Colvin 594effa279 Switching to pydantic_core (#4516)
* working on core schema generation

* adapting main.py

* getting tests to run

* fix tests

* disable pyright, fix mypy

* moving to class-based model generation

* working on validators

* change how models are created

* start fixing test_main.py

* fixing mypy

* SelfType

* recursive models working, more tests fixed

* fix tests on <3.10

* get docs build to pass

* starting to cleanup types.py

* starting works on custom types

* working on using annotated-types

* using annoated types for constraints

* lots of cleanup, fixing network tests

* network tests passing 🎉

* working on types

* working on types and cleanup

* fixing UUID type, restructing again

* more types and newer pydantic-core

* working on Iterable

* more test_types tests

* support newer pydantic-core, fixing more test_types.py

* working through more test_types.py

* test_types.py at last passing locally 🎉

* fixing more tests in test_types.py

* fix datetime_parse tests and linting

* get tests running again, rename to test_datetime.py

* renaming internal modules

* working through mypy errors

* fixing mypy

* refactoring _generate_schema.py

* test_main.py passing

* uprev deps

* fix conftest and linting?

* importing Annotated

* ltining

* import Annotated from typing_extensions

* fixing 3.7 compatibility

* fixing tests on 3.9

* fix linting

* fixing SecretField and 3.9 tests

* customising get_type_hints

* ignore warnings on 3.11

* spliting repr out of utils

* removing unused bits of _repr, fix tests for 3.7

* more cleanup, removing many type aliases

* clean up repr

* support namedtuples and typeddicts

* test is_union

* removing errors, uprev pydantic-core

* fix tests on 3.8

* fixing private attributes and model_post_init

* renaming and cleanup

* remove unnecessary PydanticMetadata inheritance

* fixing forward refs and mypy tests

* fix signatures, change how xfail works

* revert mypy tests to 3.7 syntax

* correct model title

* try to fix tests

* fixing ClassVar forward refs

* uprev pydantic-core, new error format

* add "force" argument to model_rebuild

* Apply suggestions from code review

Suggestions from @tiangolo and @hramezani 🙏

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>

* more suggestions from @tiangolo

* extra -> json_schema_extra on Field

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2022-11-02 12:01:17 +00:00

160 lines
6.0 KiB
Python

"""
Logic related to validators applied to models etc. via the `@validator` and `@root_validator` decorators.
"""
from __future__ import annotations as _annotations
import warnings
from typing import TYPE_CHECKING, Any, Callable
from ..errors import PydanticUserError
if TYPE_CHECKING:
from typing_extensions import Literal
from ..main import BaseModel
__all__ = 'FIELD_VALIDATOR_TAG', 'ROOT_VALIDATOR_TAG', 'Validator', 'ValidationFunctions', 'prepare_validator'
FIELD_VALIDATOR_TAG = '_field_validator'
ROOT_VALIDATOR_TAG = '_root_validator'
class Validator:
"""
Store information about field and root validators.
"""
__slots__ = 'function', 'mode', 'sub_path', 'check_fields'
def __init__(
self,
*,
mode: Literal['before', 'after', 'wrap', 'plain'],
sub_path: tuple[str | int, ...] | None = None,
check_fields: bool | None = None,
):
# function is set later after the class is created and functions are bound
self.function: Callable[..., Any] | None = None
self.mode = mode
self.sub_path = sub_path
self.check_fields = check_fields
class ValidationFunctions:
__slots__ = (
'_validators',
'_field_validators',
'_direct_field_validators',
'_all_fields_validators',
'_root_validators',
'_used_validators',
)
def __init__(self, bases: tuple[type[Any], ...]) -> None:
self._validators: dict[str, Validator] = {}
self._field_validators: dict[str, list[str]] = {}
self._direct_field_validators: set[str] = set()
self._all_fields_validators: list[str] = []
self._root_validators: list[str] = []
self._used_validators: set[str] = set()
self._inherit(bases)
def extract_validator(self, name: str, value: Any) -> None:
"""
If the value is a field or root validator, add it to the appropriate group of validators.
Note at this point the function is not bound to the class,
we have to set functions later in `set_bound_functions`.
"""
f_validator: tuple[tuple[str, ...], Validator] | None = getattr(value, FIELD_VALIDATOR_TAG, None)
if f_validator:
fields, validator = f_validator
self._validators[name] = validator
for field_name in fields:
this_field_validators = self._field_validators.get(field_name)
if this_field_validators:
this_field_validators.append(name)
else:
self._field_validators[field_name] = [name]
else:
r_validator: Validator | None = getattr(value, ROOT_VALIDATOR_TAG, None)
if r_validator:
self._validators[name] = r_validator
self._root_validators.append(name)
def set_bound_functions(self, cls: type[BaseModel]) -> None:
"""
Set functions in self._validators, now that the class is created and functions are bound.
"""
for name, validator in self._validators.items():
validator.function = getattr(cls, name)
def get_root_validators(self) -> list[Validator]:
return [self._validators[name] for name in self._root_validators]
def get_field_validators(self, name: str) -> list[Validator]:
"""
Get all validators for a given field name.
"""
self._used_validators.add(name)
validators_names = self._field_validators.get(name, [])
validators_names += self._all_fields_validators
return [self._validators[name] for name in validators_names]
def check_for_unused(self) -> None:
unused_validator_keys = self._validators.keys() - self._used_validators - set(self._root_validators)
unused_validators = [name for name in unused_validator_keys if self._validators[name].check_fields]
if unused_validators:
fn = ', '.join(unused_validators)
raise PydanticUserError(
f"Validators defined with incorrect fields: {fn} " # noqa: Q000
f"(use check_fields=False if you're inheriting from the model and intended this)"
)
def _inherit(self, bases: tuple[type[Any], ...]) -> None:
"""
Inherit validators from `ValidationFunctions` instances on base classes.
Validators from the closest base should be called last, and the greatest-(grand)parent first - to roughly
match their definition order in code.
"""
for base in reversed(bases):
parent_vf: ValidationFunctions | None = getattr(base, '__pydantic_validator_functions__', None)
if parent_vf:
self._validators.update(parent_vf._validators)
for k, v in parent_vf._field_validators.items():
existing = self._field_validators.get(k)
if existing:
existing.extend(v)
self._field_validators[k] = v[:]
self._all_fields_validators.extend(parent_vf._all_fields_validators)
self._root_validators.extend(parent_vf._root_validators)
_FUNCS: set[str] = set()
def prepare_validator(function: Callable[..., Any], allow_reuse: bool) -> classmethod[Any]:
"""
Warn about validators with duplicated names since without this, validators can be overwritten silently
which generally isn't the intended behaviour, don't run in ipython (see #312) or if `allow_reuse` is True.
"""
f_cls = function if isinstance(function, classmethod) else classmethod(function)
if not allow_reuse and not in_ipython():
ref = f'{f_cls.__func__.__module__}::{f_cls.__func__.__qualname__}'
if ref in _FUNCS:
warnings.warn(f'duplicate validator function "{ref}"; if this is intended, set `allow_reuse=True`')
_FUNCS.add(ref)
return f_cls
def in_ipython() -> bool:
"""
Check whether we're in an ipython environment, including jupyter notebooks.
"""
try:
eval('__IPYTHON__')
except NameError:
return False
else: # pragma: no cover
return True