mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
Allow arbitrary types in model (#209)
* Allow arbitrary types in model * Replaced ConfigError with RuntimeError * Corrections of the ArbitraryTypeError exception class
This commit is contained in:
committed by
Samuel Colvin
parent
8f42e515d5
commit
73015d2a72
@@ -345,6 +345,8 @@ Options:
|
||||
``False``)
|
||||
: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``)
|
||||
|
||||
.. warning::
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from .utils import display_as_type
|
||||
|
||||
|
||||
class PydanticErrorMixin:
|
||||
code: str
|
||||
@@ -225,3 +227,11 @@ class UUIDVersionError(PydanticValueError):
|
||||
|
||||
def __init__(self, *, required_version: int) -> None:
|
||||
super().__init__(required_version=required_version)
|
||||
|
||||
|
||||
class ArbitraryTypeError(PydanticTypeError):
|
||||
code = 'arbitrary_type'
|
||||
msg_template = 'instance of {expected_arbitrary_type} expected'
|
||||
|
||||
def __init__(self, *, expected_arbitrary_type) -> None:
|
||||
super().__init__(expected_arbitrary_type=display_as_type(expected_arbitrary_type))
|
||||
|
||||
+2
-1
@@ -213,7 +213,8 @@ class Field:
|
||||
get_validators = getattr(self.type_, 'get_validators', None)
|
||||
v_funcs = (
|
||||
*tuple(v.func for v in self.class_validators if not v.whole and v.pre),
|
||||
*(get_validators() if get_validators else find_validators(self.type_)),
|
||||
*(get_validators() if get_validators else find_validators(self.type_,
|
||||
self.model_config.arbitrary_types_allowed)),
|
||||
*tuple(v.func for v in self.class_validators if not v.whole and not v.pre),
|
||||
)
|
||||
self.validators = self._prep_vals(v_funcs)
|
||||
|
||||
@@ -30,6 +30,7 @@ class BaseConfig:
|
||||
fields = {}
|
||||
validate_assignment = False
|
||||
error_msg_templates: Dict[str, str] = {}
|
||||
arbitrary_types_allowed = False
|
||||
|
||||
@classmethod
|
||||
def get_field_schema(cls, name):
|
||||
|
||||
+13
-2
@@ -214,6 +214,14 @@ def path_exists_validator(v) -> Path:
|
||||
return v
|
||||
|
||||
|
||||
def make_arbitrary_type_validator(type_):
|
||||
def arbitrary_type_validator(v) -> type_:
|
||||
if isinstance(v, type_):
|
||||
return v
|
||||
raise errors.ArbitraryTypeError(expected_arbitrary_type=type_)
|
||||
return arbitrary_type_validator
|
||||
|
||||
|
||||
# order is important here, for example: bool is a subclass of int so has to come first, datetime before date same
|
||||
_VALIDATORS = [
|
||||
(Enum, [enum_validator]),
|
||||
@@ -242,7 +250,7 @@ _VALIDATORS = [
|
||||
]
|
||||
|
||||
|
||||
def find_validators(type_):
|
||||
def find_validators(type_, arbitrary_types_allowed=False):
|
||||
if type_ is Any:
|
||||
return []
|
||||
for val_type, validators in _VALIDATORS:
|
||||
@@ -251,4 +259,7 @@ def find_validators(type_):
|
||||
return validators
|
||||
except TypeError as e:
|
||||
raise RuntimeError(f'error checking inheritance of {type_!r} (type: {display_as_type(type_)})') from e
|
||||
raise errors.ConfigError(f'no validator found for {type_}')
|
||||
if arbitrary_types_allowed:
|
||||
return [make_arbitrary_type_validator(type_)]
|
||||
else:
|
||||
raise RuntimeError(f'no validator found for {type_}')
|
||||
|
||||
+45
-7
@@ -205,13 +205,6 @@ def test_invalid_validator():
|
||||
assert exc_info.value.args[0].startswith('Invalid signature for validator')
|
||||
|
||||
|
||||
def test_no_validator():
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class NoValidatorModel(BaseModel):
|
||||
x: object = ...
|
||||
assert exc_info.value.args[0] == "no validator found for <class 'object'>"
|
||||
|
||||
|
||||
def test_unable_to_infer():
|
||||
with pytest.raises(errors.ConfigError) as exc_info:
|
||||
class InvalidDefinitionModel(BaseModel):
|
||||
@@ -462,3 +455,48 @@ def test_default_copy():
|
||||
u1 = User()
|
||||
u2 = User()
|
||||
assert u1.friends is not u2.friends
|
||||
|
||||
|
||||
class ArbitraryType:
|
||||
pass
|
||||
|
||||
|
||||
def test_arbitrary_type_allowed_validation_success():
|
||||
class ArbitraryTypeAllowedModel(BaseModel):
|
||||
t: ArbitraryType
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
arbitrary_type_instance = ArbitraryType()
|
||||
m = ArbitraryTypeAllowedModel(t=arbitrary_type_instance)
|
||||
assert m.t == arbitrary_type_instance
|
||||
|
||||
|
||||
def test_arbitrary_type_allowed_validation_fails():
|
||||
class ArbitraryTypeAllowedModel(BaseModel):
|
||||
t: ArbitraryType
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
class C:
|
||||
pass
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ArbitraryTypeAllowedModel(t=C())
|
||||
assert exc_info.value.errors() == [
|
||||
{
|
||||
'loc': ('t',),
|
||||
'msg': "instance of ArbitraryType expected",
|
||||
'type': 'type_error.arbitrary_type',
|
||||
'ctx': {'expected_arbitrary_type': 'ArbitraryType'}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_arbitrary_types_not_allowed():
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
class ArbitraryTypeNotAllowedModel(BaseModel):
|
||||
t: ArbitraryType
|
||||
assert exc_info.value.args[0].startswith('no validator found for')
|
||||
|
||||
Reference in New Issue
Block a user