Add Config.keep_untouched for custom descriptors support (#679)

* Add Config.keep_untouched for custom descriptors support

* Update HISTORY.rst

* Separate test

* Fix tests

* Update pydantic/validators.py

Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>

* Update HISTORY.rst

Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>

* Rename TYPE_BLACKLIST -> UNTOUCHED_TYPES, fix tests and formatting

* Update docs/index.rst

Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>
This commit is contained in:
Arseny Boykov
2019-07-24 18:33:49 +03:00
committed by Samuel Colvin
parent b702eb8738
commit fae3588f42
5 changed files with 73 additions and 3 deletions
+1
View File
@@ -12,6 +12,7 @@ v0.31 (unreleased)
* add advanced exclude support for ``dict``, ``json`` and ``copy``, #648 by @MrMrRobat
* fix bug in ``GenericModel`` for models with concrete parameterized fields, #672 by @dmontagu
* add documentation for Literal type, #651 by @dmontagu
* add ``Config.keep_untouched`` for custom descriptors support, #679 by @MrMrRobat
* use ``inspect.cleandoc`` internally to get model description, #657 by @tiangolo
* add Color to schema generation, by @euri10
+2
View File
@@ -762,6 +762,8 @@ Options:
details.
:orm_mode: allows usage of :ref:`ORM mode <orm_mode>`
:alias_generator: callable that takes field name and returns alias for it
:keep_untouched: tuple of types (e. g. descriptors) that won't change during model creation and won't be
included in the model schemas.
.. warning::
+4 -2
View File
@@ -97,6 +97,7 @@ class BaseConfig:
json_encoders: Dict[AnyType, AnyCallable] = {}
orm_mode: bool = False
alias_generator: Optional[Callable[[str], str]] = None
keep_untouched: Tuple[type, ...] = ()
@classmethod
def get_field_schema(cls, name: str) -> Dict[str, str]:
@@ -169,7 +170,7 @@ def validate_custom_root_type(fields: Dict[str, Field]) -> None:
raise TypeError('custom root type cannot allow mapping')
TYPE_BLACKLIST = FunctionType, property, type, classmethod, staticmethod
UNTOUCHED_TYPES = FunctionType, property, type, classmethod, staticmethod
class MetaModel(ABCMeta):
@@ -217,10 +218,11 @@ class MetaModel(ABCMeta):
config=config,
)
untouched_types = UNTOUCHED_TYPES + config.keep_untouched
for var_name, value in namespace.items():
if (
is_valid_field(var_name)
and (annotations.get(var_name) == PyObject or not isinstance(value, TYPE_BLACKLIST))
and (annotations.get(var_name) == PyObject or not isinstance(value, untouched_types))
and var_name not in class_vars
):
validate_field_name(bases, var_name)
+3 -1
View File
@@ -475,7 +475,9 @@ def find_validators( # noqa: C901 (ignore complexity)
if config.arbitrary_types_allowed:
yield make_arbitrary_type_validator(type_)
else:
raise RuntimeError(f'no validator found for {type_}')
raise RuntimeError(
f'no validator found for {type_} see `keep_untouched` or `arbitrary_types_allowed` in Config'
)
def _find_supertype(type_: AnyType) -> Optional[AnyType]:
+63
View File
@@ -743,3 +743,66 @@ def test_parse_root_as_mapping():
class MyModel(BaseModel):
__root__: Mapping[str, str]
def test_untouched_types():
from pydantic import BaseModel
class _ClassPropertyDescriptor:
def __init__(self, getter):
self.getter = getter
def __get__(self, instance, owner):
return self.getter(owner)
classproperty = _ClassPropertyDescriptor
class Model(BaseModel):
class Config:
keep_untouched = (classproperty,)
@classproperty
def class_name(cls) -> str:
return cls.__name__
assert Model.class_name == 'Model'
assert Model().class_name == 'Model'
def test_custom_types_fail_without_keep_untouched():
from pydantic import BaseModel
class _ClassPropertyDescriptor:
def __init__(self, getter):
self.getter = getter
def __get__(self, instance, owner):
return self.getter(owner)
classproperty = _ClassPropertyDescriptor
with pytest.raises(RuntimeError) as e:
class Model(BaseModel):
@classproperty
def class_name(cls) -> str:
return cls.__name__
Model.class_name
assert str(e.value) == (
"no validator found for <class 'tests.test_main.test_custom_types_fail_without_keep_untouched.<locals>."
"_ClassPropertyDescriptor'> see `keep_untouched` or `arbitrary_types_allowed` in Config"
)
class Model(BaseModel):
class Config:
arbitrary_types_allowed = True
@classproperty
def class_name(cls) -> str:
return cls.__name__
with pytest.raises(AttributeError) as e:
Model.class_name
assert str(e.value) == "type object 'Model' has no attribute 'class_name'"