From 9ad1a0ad24e2af787fcfb450d165fcf3e8f1797b Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 27 Dec 2018 20:30:41 +0000 Subject: [PATCH] reame get_validators > __get_validators__ (#338) * reame get_validators > __get_validators__ * update docs --- HISTORY.rst | 1 + docs/examples/custom_data_types.py | 2 +- docs/index.rst | 9 +++- pydantic/fields.py | 10 +++- pydantic/main.py | 2 +- pydantic/types.py | 26 +++++----- tests/{test_complex.py => test_edge_cases.py} | 47 ++++++++++++++++++- tests/test_main.py | 30 +----------- 8 files changed, 78 insertions(+), 49 deletions(-) rename tests/{test_complex.py => test_edge_cases.py} (93%) diff --git a/HISTORY.rst b/HISTORY.rst index 7da9457..15e29f6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,7 @@ v0.17.0 (unreleased) (**breaking change**: this supersedes the ``validate_assignment`` argument with ``config``) * support for nested dataclasses, #334 by @samuelcolvin * better errors when getting an ``ImportError`` with ``PyObject``, #309 by @samuelcolvin +* rename ``get_validators`` to ``__get_validators__``, deprecation warning on use of old name, #338 by @samuelcolvin v0.16.1 (2018-12-10) .................... diff --git a/docs/examples/custom_data_types.py b/docs/examples/custom_data_types.py index b5a29fc..bee5298 100644 --- a/docs/examples/custom_data_types.py +++ b/docs/examples/custom_data_types.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, ValidationError class StrictStr(str): @classmethod - def get_validators(cls): + def __get_validators__(cls): yield cls.validate @classmethod diff --git a/docs/index.rst b/docs/index.rst index 255149b..fd33559 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -391,8 +391,13 @@ against defined Json structure if it's provided. Custom Data Types ................. -You can also define your own data types. Class method ``get_validators`` will be called to get validators to parse and -validate the input data. +You can also define your own data types. The class method ``__get_validators__`` will be called +to get validators to parse and validate the input data. + +.. note:: + + The name of ``__get_validators__`` was changed from ``get_validators`` in ``v0.17``, + the old name is currently still supported but deprecated and will be removed in future. .. literalinclude:: examples/custom_data_types.py diff --git a/pydantic/fields.py b/pydantic/fields.py index 939a579..a13103a 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1,4 +1,5 @@ import inspect +import warnings from enum import IntEnum from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Pattern, Set, Tuple, Type, Union @@ -201,8 +202,13 @@ class Field: def _populate_validators(self): class_validators_ = self.class_validators.values() if not self.sub_fields: - get_validators = getattr(self.type_, 'get_validators', None) - get_validators = get_validators or getattr(self.type_, '__get_validators__', None) + get_validators = getattr(self.type_, '__get_validators__', None) + if not get_validators: + get_validators = getattr(self.type_, 'get_validators', None) + if get_validators: + warnings.warn( + f'get_validators has been replaced by __get_validators__ (on {self.name})', DeprecationWarning + ) v_funcs = ( *tuple(v.func for v in class_validators_ if not v.whole and v.pre), *( diff --git a/pydantic/main.py b/pydantic/main.py index 2f700d9..e1e33e2 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -345,7 +345,7 @@ class BaseModel(metaclass=MetaModel): return json.dumps(cls.schema(by_alias=by_alias), default=pydantic_encoder, **dumps_kwargs) @classmethod - def get_validators(cls): + def __get_validators__(cls): yield dict_validator yield cls.validate diff --git a/pydantic/types.py b/pydantic/types.py index b80c2e0..ab3d6b5 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -67,7 +67,7 @@ NoneStrBytes = Optional[StrBytes] class StrictStr(str): @classmethod - def get_validators(cls): + def __get_validators__(cls): yield cls.validate @classmethod @@ -85,7 +85,7 @@ class ConstrainedStr(str): regex: Optional[Pattern] = None @classmethod - def get_validators(cls): + def __get_validators__(cls): yield not_none_validator yield str_validator yield anystr_strip_whitespace @@ -118,7 +118,7 @@ def constr(*, strip_whitespace=False, min_length=None, max_length=None, curtail_ class EmailStr(str): @classmethod - def get_validators(cls): + def __get_validators__(cls): # included here and below so the error happens straight away if email_validator is None: raise ImportError('email-validator is not installed, run `pip install pydantic[email]`') @@ -140,7 +140,7 @@ class UrlStr(str): require_tld = True # whether to reject non-FQDN hostnames @classmethod - def get_validators(cls): + def __get_validators__(cls): yield not_none_validator yield str_validator yield anystr_strip_whitespace @@ -192,7 +192,7 @@ class NameEmail: self.email = email @classmethod - def get_validators(cls): + def __get_validators__(cls): if email_validator is None: raise ImportError('email-validator is not installed, run `pip install pydantic[email]`') @@ -214,7 +214,7 @@ class PyObject: validate_always = True @classmethod - def get_validators(cls): + def __get_validators__(cls): yield str_validator yield cls.validate @@ -233,7 +233,7 @@ class DSN(str): validate_always = True @classmethod - def get_validators(cls): + def __get_validators__(cls): yield str_validator yield cls.validate @@ -268,7 +268,7 @@ class ConstrainedInt(int, metaclass=ConstrainedNumberMeta): le: Optional[int] = None @classmethod - def get_validators(cls): + def __get_validators__(cls): yield int_validator yield number_size_validator @@ -294,7 +294,7 @@ class ConstrainedFloat(float, metaclass=ConstrainedNumberMeta): le: Union[None, int, float] = None @classmethod - def get_validators(cls): + def __get_validators__(cls): yield float_validator yield number_size_validator @@ -322,7 +322,7 @@ class ConstrainedDecimal(Decimal, metaclass=ConstrainedNumberMeta): decimal_places: Optional[int] = None @classmethod - def get_validators(cls): + def __get_validators__(cls): yield not_none_validator yield decimal_validator yield number_size_validator @@ -389,7 +389,7 @@ class UUID5(UUID): class FilePath(Path): @classmethod - def get_validators(cls): + def __get_validators__(cls): yield path_validator yield path_exists_validator yield cls.validate @@ -404,7 +404,7 @@ class FilePath(Path): class DirectoryPath(Path): @classmethod - def get_validators(cls): + def __get_validators__(cls): yield path_validator yield path_exists_validator yield cls.validate @@ -428,7 +428,7 @@ class JsonMeta(type): class Json(metaclass=JsonMeta): @classmethod - def get_validators(cls): + def __get_validators__(cls): yield str_validator yield cls.validate diff --git a/tests/test_complex.py b/tests/test_edge_cases.py similarity index 93% rename from tests/test_complex.py rename to tests/test_edge_cases.py index 1f09d4d..056aac6 100644 --- a/tests/test_complex.py +++ b/tests/test_edge_cases.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union import pytest -from pydantic import BaseConfig, BaseModel, NoneStrBytes, StrBytes, ValidationError, constr, validate_model +from pydantic import BaseConfig, BaseModel, NoneStrBytes, StrBytes, ValidationError, constr, errors, validate_model def test_str_bytes(): @@ -546,3 +546,48 @@ def test_optional_required(): assert Model(bar=123).dict() == {'bar': 123} assert Model().dict() == {'bar': None} assert Model(bar=None).dict() == {'bar': None} + + +def test_invalid_validator(): + class InvalidValidator: + @classmethod + def __get_validators__(cls): + yield cls.has_wrong_arguments + + @classmethod + def has_wrong_arguments(cls, value, bar): + pass + + with pytest.raises(errors.ConfigError) as exc_info: + + class InvalidValidatorModel(BaseModel): + x: InvalidValidator = ... + + assert exc_info.value.args[0].startswith('Invalid signature for validator') + + +def test_unable_to_infer(): + with pytest.raises(errors.ConfigError) as exc_info: + + class InvalidDefinitionModel(BaseModel): + x = None + + assert exc_info.value.args[0] == 'unable to infer type for attribute "x"' + + +def test_get_validator(): + class CustomClass: + @classmethod + def get_validators(cls): + yield cls.validate + + @classmethod + def validate(cls, v): + return v * 2 + + with pytest.warns(DeprecationWarning): + + class Model(BaseModel): + x: CustomClass + + assert Model(x=42).x == 84 diff --git a/tests/test_main.py b/tests/test_main.py index 48c3482..33b5c56 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,7 +3,7 @@ from typing import Any, List import pytest -from pydantic import BaseModel, NoneBytes, NoneStr, Required, ValidationError, constr, errors +from pydantic import BaseModel, NoneBytes, NoneStr, Required, ValidationError, constr def test_success(): @@ -159,34 +159,6 @@ def test_prevent_extra_fails(): ] -class InvalidValidator: - @classmethod - def get_validators(cls): - yield cls.has_wrong_arguments - - @classmethod - def has_wrong_arguments(cls, value, bar): - pass - - -def test_invalid_validator(): - with pytest.raises(errors.ConfigError) as exc_info: - - class InvalidValidatorModel(BaseModel): - x: InvalidValidator = ... - - assert exc_info.value.args[0].startswith('Invalid signature for validator') - - -def test_unable_to_infer(): - with pytest.raises(errors.ConfigError) as exc_info: - - class InvalidDefinitionModel(BaseModel): - x = None - - assert exc_info.value.args[0] == 'unable to infer type for attribute "x"' - - def test_not_required(): class Model(BaseModel): a: float = None