support ForwardRef on dataclasses (#399)

* support ForwardRef on dataclasses, fix #397

* explicit docs about when postponed annotations don't work
This commit is contained in:
Samuel Colvin
2019-02-18 08:41:35 +00:00
committed by GitHub
parent 5438aa5c91
commit 1161df3ca0
7 changed files with 61 additions and 6 deletions
+1
View File
@@ -7,6 +7,7 @@ v0.20.0 (unreleased)
....................
* fix tests for python 3.8, #396 by @samuelcolvin
* Adds fields to the ``dir`` method for autocompletion in interactive sessions, #398 by @dgasmith
* support ``ForwardRef`` (and therefore ``from __future__ import annotations``) with dataclasses, #397 by @samuelcolvin
v0.20.0a1 (2019-02-13)
......................
+8
View File
@@ -0,0 +1,8 @@
from __future__ import annotations
from pydantic import BaseModel
def this_is_broken():
from typing import List # <-- List is defined inside the function so is not in the module's global scope
class Model(BaseModel):
a: List[int]
print(Model(a=(1, 2)))
+8
View File
@@ -0,0 +1,8 @@
from __future__ import annotations
from typing import List # <-- List is defined in the module's global scope
from pydantic import BaseModel
def this_works():
class Model(BaseModel):
a: List[int]
print(Model(a=(1, 2)))
+16
View File
@@ -709,6 +709,22 @@ to properly set types before the model can be used.
(This script is complete, it should run "as is")
.. warning::
To resolve strings (type names) into annotations (types) *pydantic* needs a dict to lookup,
for this is uses ``module.__dict__`` just as ``get_type_hints`` does. That means *pydantic* does not play well
with types not defined in the global scope of a module.
For example, this works fine:
.. literalinclude:: examples/postponed_works.py
While this will break:
.. literalinclude:: examples/postponed_broken.py
Resolving this is beyond the call for *pydantic*: either remove the future import or declare the types globally.
.. _benchmarks_tag:
Benchmarks
+1 -1
View File
@@ -74,7 +74,7 @@ def _process_class(
fields: Dict[str, Any] = {name: (field.type, field.default) for name, field in cls.__dataclass_fields__.items()}
cls.__post_init_original__ = post_init_original
cls.__pydantic_model__ = create_model(cls.__name__, __config__=config, __base__=None, **fields)
cls.__pydantic_model__ = create_model(cls.__name__, __config__=config, __module__=_cls.__module__, **fields)
cls.__initialised__ = False
cls.__validate__ = classmethod(_validate_dataclass)
+9 -4
View File
@@ -504,7 +504,12 @@ class BaseModel(metaclass=MetaModel):
def create_model(
model_name: str, *, __config__: Type[BaseConfig] = None, __base__: Type[BaseModel] = None, **field_definitions: Any
model_name: str,
*,
__config__: Type[BaseConfig] = None,
__base__: Type[BaseModel] = None,
__module__: Optional[str] = None,
**field_definitions: Any,
) -> BaseModel:
"""
Dynamically create a model.
@@ -542,7 +547,7 @@ def create_model(
annotations[f_name] = f_annotation
fields[f_name] = f_value
namespace: 'DictStrAny' = {'__annotations__': annotations}
namespace: 'DictStrAny' = {'__annotations__': annotations, '__module__': __module__}
namespace.update(fields)
if __config__:
namespace['Config'] = inherit_config(__config__, BaseConfig)
@@ -565,8 +570,8 @@ def validate_model( # noqa: C901 (ignore complexity)
for name, field in model.__fields__.items():
if type(field.type_) == ForwardRef:
raise ConfigError(
f"field {field.name} not yet prepared and type is still a ForwardRef, "
f"you'll need to call {model.__class__.__name__}.update_forward_refs()"
f'field "{field.name}" not yet prepared so type is still a ForwardRef, '
f'you might need to call {model.__class__.__name__}.update_forward_refs().'
)
value = input_data.get(field.alias, _missing)
+18 -1
View File
@@ -122,4 +122,21 @@ class Foo(BaseModel):
)
with pytest.raises(ConfigError) as exc_info:
module.Foo(b=123)
assert str(exc_info.value).startswith('field b not yet prepared and type is still a ForwardRef')
assert str(exc_info.value).startswith('field "b" not yet prepared so type is still a ForwardRef')
@skip_not_37
def test_forward_ref_dataclass(create_module):
module = create_module(
"""
from __future__ import annotations
from pydantic import UrlStr
from pydantic.dataclasses import dataclass
@dataclass
class Dataclass:
url: UrlStr
"""
)
m = module.Dataclass('http://example.com ')
assert m.url == 'http://example.com'