mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
Add support for Type[T] typehints when arbitrary_types_allowed==True. (#808)
* Add support for Type[T] typehints when arbitrary_types_allowe==True. * Add documentation. * Let black do its magic. * Ignore mypy warning - see here: https://github.com/python/mypy/issues/3060 * Prettify docs. * Change Changelog. * Refactor and simplify check for Type[T]. * Black again. ^^ - Really need pre-commit hooks. * Update pydantic/validators.py Co-Authored-By: Samuel Colvin <samcolvin@gmail.com> * Rename arbitrary_class to class. * Black. * Add type hints. * Make private function public. * Add support for bare Type. * Black again. * Update docs. * CO_ct not meant for export. * Fix get_class for Python3.6 * Update error message of ClassError. * Use relative import. * Incorporate typing feedback (both versions are fine with mypy). * Move from issubclass to lenient_issubclass. * correct docs
This commit is contained in:
committed by
Samuel Colvin
parent
ef894d20b3
commit
f08fd2fee7
@@ -0,0 +1 @@
|
||||
add support for ``Type[T]`` type hints
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import Type
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
class LenientSimpleModel(BaseModel):
|
||||
any_class_goes: Type
|
||||
|
||||
|
||||
LenientSimpleModel(any_class_goes=int)
|
||||
LenientSimpleModel(any_class_goes=Foo)
|
||||
try:
|
||||
LenientSimpleModel(any_class_goes=Foo())
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
"""
|
||||
1 validation error
|
||||
any_class_goes
|
||||
subclass of type expected (type=type_error.class)
|
||||
"""
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Type
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import ValidationError
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
class Bar(Foo):
|
||||
pass
|
||||
|
||||
class Other:
|
||||
pass
|
||||
|
||||
class SimpleModel(BaseModel):
|
||||
just_subclasses: Type[Foo]
|
||||
|
||||
|
||||
SimpleModel(just_subclasses=Foo)
|
||||
SimpleModel(just_subclasses=Bar)
|
||||
try:
|
||||
SimpleModel(just_subclasses=Other)
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
"""
|
||||
1 validation error
|
||||
just_subclasses
|
||||
subclass of Foo expected (type=type_error.class)
|
||||
"""
|
||||
+13
-1
@@ -818,6 +818,18 @@ With proper ordering in an annotated ``Union``, you can use this to parse types
|
||||
|
||||
(This script is complete, it should run "as is")
|
||||
|
||||
Type Type
|
||||
............
|
||||
|
||||
Pydantic supports the use of ``Type[T]`` to specify that a field may only accept classes (not instances)
|
||||
that are subclasses of ``T``.
|
||||
|
||||
.. literalinclude:: examples/type_type.py
|
||||
|
||||
You may also use ``Type`` to specify that any class is allowed.
|
||||
|
||||
.. literalinclude:: examples/bare_type_type.py
|
||||
|
||||
Custom Data Types
|
||||
.................
|
||||
|
||||
@@ -898,7 +910,7 @@ Options:
|
||||
:error_msg_templates: let's you to override default error message templates.
|
||||
Pass in a dictionary with keys matching the error messages you want to override (default: ``{}``)
|
||||
:arbitrary_types_allowed: whether to allow arbitrary user types for fields (they are validated simply by checking if the
|
||||
value is instance of that type). If False - RuntimeError will be raised on model declaration (default: ``False``)
|
||||
value is instance of that type). If ``False`` - ``RuntimeError`` will be raised on model declaration (default: ``False``)
|
||||
:json_encoders: customise the way types are encoded to json, see :ref:`JSON Serialisation <json_dump>` for more
|
||||
details.
|
||||
:orm_mode: allows usage of :ref:`ORM mode <orm_mode>`
|
||||
|
||||
@@ -324,6 +324,19 @@ class ArbitraryTypeError(PydanticTypeError):
|
||||
super().__init__(expected_arbitrary_type=display_as_type(expected_arbitrary_type))
|
||||
|
||||
|
||||
class ClassError(PydanticTypeError):
|
||||
code = 'class'
|
||||
msg_template = 'a class is expected'
|
||||
|
||||
|
||||
class SubclassError(PydanticTypeError):
|
||||
code = 'subclass'
|
||||
msg_template = 'subclass of {expected_class} expected'
|
||||
|
||||
def __init__(self, *, expected_class: AnyType) -> None:
|
||||
super().__init__(expected_class=display_as_type(expected_class))
|
||||
|
||||
|
||||
class JsonError(PydanticValueError):
|
||||
msg_template = 'Invalid JSON'
|
||||
|
||||
|
||||
@@ -248,6 +248,8 @@ class Field:
|
||||
)
|
||||
self.type_ = self.type_.__args__[1] # type: ignore
|
||||
self.shape = SHAPE_MAPPING
|
||||
elif issubclass(origin, Type): # type: ignore
|
||||
return
|
||||
else:
|
||||
raise TypeError(f'Fields of type "{origin}" are not supported.')
|
||||
|
||||
|
||||
@@ -193,3 +193,21 @@ def update_field_forward_refs(field: 'Field', globalns: Any, localns: Any) -> No
|
||||
if field.sub_fields:
|
||||
for sub_f in field.sub_fields:
|
||||
update_field_forward_refs(sub_f, globalns=globalns, localns=localns)
|
||||
|
||||
|
||||
def get_class(type_: AnyType) -> Union[None, bool, AnyType]:
|
||||
"""
|
||||
Tries to get the class of a Type[T] annotation. Returns True if Type is used
|
||||
without brackets. Otherwise returns None.
|
||||
"""
|
||||
try:
|
||||
origin = getattr(type_, '__origin__')
|
||||
if origin is None: # Python 3.6
|
||||
origin = type_
|
||||
if issubclass(origin, Type): # type: ignore
|
||||
if type_.__args__ is None or not isinstance(type_.__args__[0], type):
|
||||
return True
|
||||
return type_.__args__[0]
|
||||
except AttributeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
+25
-2
@@ -26,8 +26,8 @@ from uuid import UUID
|
||||
|
||||
from . import errors
|
||||
from .datetime_parse import parse_date, parse_datetime, parse_duration, parse_time
|
||||
from .typing import AnyCallable, AnyType, ForwardRef, display_as_type, is_callable_type, is_literal_type
|
||||
from .utils import almost_equal_floats, change_exception, sequence_like
|
||||
from .typing import AnyCallable, AnyType, ForwardRef, display_as_type, get_class, is_callable_type, is_literal_type
|
||||
from .utils import almost_equal_floats, change_exception, lenient_issubclass, sequence_like
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from .fields import Field
|
||||
@@ -404,6 +404,21 @@ def make_arbitrary_type_validator(type_: Type[T]) -> Callable[[T], T]:
|
||||
return arbitrary_type_validator
|
||||
|
||||
|
||||
def make_class_validator(type_: Type[T]) -> Callable[[Any], Type[T]]:
|
||||
def class_validator(v: Any) -> Type[T]:
|
||||
if lenient_issubclass(v, type_):
|
||||
return v
|
||||
raise errors.SubclassError(expected_class=type_)
|
||||
|
||||
return class_validator
|
||||
|
||||
|
||||
def any_class_validator(v: Any) -> Type[T]:
|
||||
if isinstance(v, type):
|
||||
return v
|
||||
raise errors.ClassError()
|
||||
|
||||
|
||||
def pattern_validator(v: Any) -> Pattern[str]:
|
||||
with change_exception(errors.PatternError, re.error):
|
||||
return re.compile(v)
|
||||
@@ -486,6 +501,14 @@ def find_validators( # noqa: C901 (ignore complexity)
|
||||
yield make_literal_validator(type_)
|
||||
return
|
||||
|
||||
class_ = get_class(type_)
|
||||
if class_ is not None:
|
||||
if isinstance(class_, type):
|
||||
yield make_class_validator(class_)
|
||||
else:
|
||||
yield any_class_validator
|
||||
return
|
||||
|
||||
supertype = _find_supertype(type_)
|
||||
if supertype is not None:
|
||||
type_ = supertype
|
||||
|
||||
+76
-1
@@ -1,5 +1,5 @@
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar, List, Mapping
|
||||
from typing import Any, ClassVar, List, Mapping, Type
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -530,6 +530,81 @@ def test_arbitrary_types_not_allowed():
|
||||
assert exc_info.value.args[0].startswith('no validator found for')
|
||||
|
||||
|
||||
def test_type_type_validation_success():
|
||||
class ArbitraryClassAllowedModel(BaseModel):
|
||||
t: Type[ArbitraryType]
|
||||
|
||||
arbitrary_type_class = ArbitraryType
|
||||
m = ArbitraryClassAllowedModel(t=arbitrary_type_class)
|
||||
assert m.t == arbitrary_type_class
|
||||
|
||||
|
||||
def test_type_type_subclass_validation_success():
|
||||
class ArbitraryClassAllowedModel(BaseModel):
|
||||
t: Type[ArbitraryType]
|
||||
|
||||
class ArbitrarySubType(ArbitraryType):
|
||||
pass
|
||||
|
||||
arbitrary_type_class = ArbitrarySubType
|
||||
m = ArbitraryClassAllowedModel(t=arbitrary_type_class)
|
||||
assert m.t == arbitrary_type_class
|
||||
|
||||
|
||||
def test_type_type_validation_fails_for_instance():
|
||||
class ArbitraryClassAllowedModel(BaseModel):
|
||||
t: Type[ArbitraryType]
|
||||
|
||||
class C:
|
||||
pass
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ArbitraryClassAllowedModel(t=C)
|
||||
assert exc_info.value.errors() == [
|
||||
{
|
||||
'loc': ('t',),
|
||||
'msg': 'subclass of ArbitraryType expected',
|
||||
'type': 'type_error.subclass',
|
||||
'ctx': {'expected_class': 'ArbitraryType'},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_type_type_validation_fails_for_basic_type():
|
||||
class ArbitraryClassAllowedModel(BaseModel):
|
||||
t: Type[ArbitraryType]
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ArbitraryClassAllowedModel(t=1)
|
||||
assert exc_info.value.errors() == [
|
||||
{
|
||||
'loc': ('t',),
|
||||
'msg': 'subclass of ArbitraryType expected',
|
||||
'type': 'type_error.subclass',
|
||||
'ctx': {'expected_class': 'ArbitraryType'},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_bare_type_type_validation_success():
|
||||
class ArbitraryClassAllowedModel(BaseModel):
|
||||
t: Type
|
||||
|
||||
arbitrary_type_class = ArbitraryType
|
||||
m = ArbitraryClassAllowedModel(t=arbitrary_type_class)
|
||||
assert m.t == arbitrary_type_class
|
||||
|
||||
|
||||
def test_bare_type_type_validation_fails():
|
||||
class ArbitraryClassAllowedModel(BaseModel):
|
||||
t: Type
|
||||
|
||||
arbitrary_type = ArbitraryType()
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ArbitraryClassAllowedModel(t=arbitrary_type)
|
||||
assert exc_info.value.errors() == [{'loc': ('t',), 'msg': 'a class is expected', 'type': 'type_error.class'}]
|
||||
|
||||
|
||||
def test_annotation_field_name_shadows_attribute():
|
||||
with pytest.raises(NameError):
|
||||
# When defining a model that has an attribute with the name of a built-in attribute, an exception is raised
|
||||
|
||||
Reference in New Issue
Block a user