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:
Samuel Colvin
2018-02-06 14:29:56 +00:00
committed by GitHub
parent 06008146fe
commit f9cf6b42f4
7 changed files with 171 additions and 3 deletions
+2 -1
View File
@@ -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)
...................
+13
View File
@@ -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
+8
View File
@@ -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
+16
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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()
+75
View File
@@ -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')