mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
Create model method (#125)
* adding create_model method * adding method * docs and tweaks * prevent config and base together * tweak docs
This commit is contained in:
+2
-1
@@ -3,9 +3,10 @@
|
||||
History
|
||||
-------
|
||||
|
||||
v0.6.5 (2018-02-XX)
|
||||
v0.7.0 (2018-02-XX)
|
||||
...................
|
||||
* added compatibility with abstract base classes (ABCs) #123
|
||||
* add ``create_model`` method #113 #125
|
||||
|
||||
v0.6.4 (2018-02-01)
|
||||
...................
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
|
||||
class FooModel(BaseModel):
|
||||
foo: str
|
||||
bar: int = 123
|
||||
|
||||
|
||||
BarModel = create_model('BarModel', apple='russet', banana='yellow', __base__=FooModel)
|
||||
print(BarModel)
|
||||
# > <class 'pydantic.main.BarModel'>
|
||||
print(', '.join(BarModel.__fields__.keys()))
|
||||
# > foo, bar, apple, banana
|
||||
@@ -0,0 +1,8 @@
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
DynamicFoobarModel = create_model('DynamicFoobarModel', foo=(str, ...), bar=123)
|
||||
|
||||
|
||||
class StaticFoobarModel(BaseModel):
|
||||
foo: str
|
||||
bar: int = 123
|
||||
@@ -252,6 +252,22 @@ Here ``redis_port`` could be modified via ``export MY_PREFIX_REDIS_PORT=6380`` o
|
||||
|
||||
Complex types like ``list``, ``set``, ``dict`` and submodels can be set by using JSON environment variables.
|
||||
|
||||
Dynamic model creation
|
||||
......................
|
||||
|
||||
There are some occasions where the shape of a model is not known until runtime, for this *pydantic* provides
|
||||
the ``create_model`` method to allow models to be created on the fly.
|
||||
|
||||
.. literalinclude:: examples/dynamic_model_creation.py
|
||||
|
||||
Here ``StaticFoobarModel`` and ``DynamicFoobarModel`` are identical.
|
||||
|
||||
Fields are defined by either a a tuple of the form ``(<type>, <default value>)`` or just a default value. The
|
||||
special key word arguments ``__config__`` and ``__base__`` can be used to customise the new model. This includes
|
||||
extending a base model with extra fields.
|
||||
|
||||
.. literalinclude:: examples/dynamic_inheritance.py
|
||||
|
||||
.. _usage_mypy:
|
||||
|
||||
Usage with mypy
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from .env_settings import BaseSettings
|
||||
from .exceptions import *
|
||||
from .fields import Required
|
||||
from .main import BaseModel, validator
|
||||
from .main import BaseModel, create_model, validator
|
||||
from .parse import Protocol
|
||||
from .types import *
|
||||
from .version import VERSION
|
||||
|
||||
+56
-1
@@ -3,7 +3,7 @@ from abc import ABCMeta
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from types import FunctionType
|
||||
from typing import Any, Dict, Set, Union
|
||||
from typing import Any, Dict, Set, Type, Union
|
||||
|
||||
from .exceptions import ConfigError, Error, Extra, Missing, ValidationError
|
||||
from .fields import Field, Validator
|
||||
@@ -96,6 +96,7 @@ class MetaModel(ABCMeta):
|
||||
new_namespace = {
|
||||
'config': config,
|
||||
'__fields__': fields,
|
||||
'__validators__': validators,
|
||||
**{n: v for n, v in namespace.items() if n not in fields}
|
||||
}
|
||||
return super().__new__(mcs, name, bases, new_namespace)
|
||||
@@ -109,6 +110,8 @@ EXTRA_ERROR = Error(Extra('extra fields not permitted'), None, None)
|
||||
class BaseModel(metaclass=MetaModel):
|
||||
# populated by the metaclass, defined here to help IDEs only
|
||||
__fields__ = {}
|
||||
__validators__ = {}
|
||||
|
||||
Config = BaseConfig
|
||||
__slots__ = '__values__',
|
||||
|
||||
@@ -308,6 +311,58 @@ class BaseModel(metaclass=MetaModel):
|
||||
return self.to_string()
|
||||
|
||||
|
||||
def create_model(
|
||||
model_name: str, *,
|
||||
__config__: Type[BaseConfig]=None,
|
||||
__base__: Type[BaseModel]=None,
|
||||
**field_definitions):
|
||||
"""
|
||||
Dynamically create a model.
|
||||
:param model_name: name of the created model
|
||||
:param __config__: config class to use for the new model
|
||||
:param __base__: base class for the new model to inherit from
|
||||
:param **field_definitions: fields of the model (or extra fields if a base is supplied) in the format
|
||||
`<name>=(<type>, <default default>)` or `<name>=<default value> eg. `foobar=(str, ...)` or `foobar=123`
|
||||
"""
|
||||
if __base__:
|
||||
fields = __base__.__fields__
|
||||
validators = __base__.__validators__
|
||||
if __config__ is not None:
|
||||
raise ConfigError('to avoid confusion __config__ and __base__ cannot be used together')
|
||||
else:
|
||||
__base__ = BaseModel
|
||||
fields = OrderedDict()
|
||||
validators = {}
|
||||
|
||||
config = __config__ or BaseConfig
|
||||
|
||||
for f_name, f_def in field_definitions.items():
|
||||
if isinstance(f_def, tuple):
|
||||
try:
|
||||
f_annotation, f_value = f_def
|
||||
except ValueError as e:
|
||||
raise ConfigError(f'field definitions should either be a tuple of (<type>, <default>) or just a '
|
||||
f'default value, unfortunately this means tuples as '
|
||||
f'default values are not allowed') from e
|
||||
else:
|
||||
f_annotation, f_value = None, f_def
|
||||
if f_name.startswith('_'):
|
||||
warnings.warn(f'fields may not start with an underscore, ignoring "{f_name}"', RuntimeWarning)
|
||||
else:
|
||||
fields[f_name] = Field.infer(
|
||||
name=f_name,
|
||||
value=f_value,
|
||||
annotation=f_annotation,
|
||||
class_validators=validators.get(f_name),
|
||||
config=config,
|
||||
)
|
||||
namespace = {
|
||||
'config': config,
|
||||
'__fields__': fields,
|
||||
}
|
||||
return type(model_name, (__base__,), namespace)
|
||||
|
||||
|
||||
_FUNCS = set()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
|
||||
from pydantic import BaseModel, ConfigError, ValidationError, create_model, validator
|
||||
|
||||
|
||||
def test_create_model():
|
||||
model = create_model('FooModel', foo=(str, ...), bar=123)
|
||||
assert issubclass(model, BaseModel)
|
||||
assert issubclass(model.config, BaseModel.Config)
|
||||
assert model.__name__ == 'FooModel'
|
||||
assert model.__fields__.keys() == {'foo', 'bar'}
|
||||
assert model.__validators__ == {}
|
||||
assert model.config.__name__ == 'BaseConfig'
|
||||
|
||||
|
||||
def test_create_model_usage():
|
||||
model = create_model('FooModel', foo=(str, ...), bar=123)
|
||||
m = model(foo='hello')
|
||||
assert m.foo == 'hello'
|
||||
assert m.bar == 123
|
||||
with pytest.raises(ValidationError):
|
||||
model()
|
||||
with pytest.raises(ValidationError):
|
||||
model(foo='hello', bar='xxx')
|
||||
|
||||
|
||||
def test_invalid_name():
|
||||
with pytest.warns(RuntimeWarning):
|
||||
model = create_model('FooModel', _foo=(str, ...))
|
||||
assert len(model.__fields__) == 0
|
||||
|
||||
|
||||
def test_field_wrong_tuple():
|
||||
with pytest.raises(ConfigError):
|
||||
create_model('FooModel', foo=(1, 2, 3))
|
||||
|
||||
|
||||
def test_config_and_base():
|
||||
with pytest.raises(ConfigError):
|
||||
create_model('FooModel', __config__=BaseModel.Config, __base__=BaseModel)
|
||||
|
||||
|
||||
def test_inheritance():
|
||||
class BarModel(BaseModel):
|
||||
x = 1
|
||||
y = 2
|
||||
model = create_model('FooModel', foo=(str, ...), bar=(int, 123), __base__=BarModel)
|
||||
assert model.__fields__.keys() == {'foo', 'bar', 'x', 'y'}
|
||||
m = model(foo='a', x=4)
|
||||
assert m.dict() == {'bar': 123, 'foo': 'a', 'x': 4, 'y': 2}
|
||||
|
||||
|
||||
def test_custom_config():
|
||||
class Config(BaseModel.Config):
|
||||
fields = {
|
||||
'foo': 'api-foo-field'
|
||||
}
|
||||
model = create_model('FooModel', foo=(int, ...), __config__=Config)
|
||||
assert model(**{'api-foo-field': '987'}).foo == 987
|
||||
with pytest.raises(ValidationError):
|
||||
model(foo=654)
|
||||
|
||||
|
||||
def test_inheritance_validators():
|
||||
class BarModel(BaseModel):
|
||||
@validator('a')
|
||||
def check_a(cls, v):
|
||||
if 'foobar' not in v:
|
||||
raise ValueError('"foobar" not found in a')
|
||||
return v
|
||||
|
||||
model = create_model('FooModel', a='cake', __base__=BarModel)
|
||||
assert model().a == 'cake'
|
||||
with pytest.raises(ValidationError):
|
||||
model(a='something else')
|
||||
Reference in New Issue
Block a user