diff --git a/HISTORY.rst b/HISTORY.rst index 2c8bb9d..b725f1e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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) ...................... diff --git a/docs/examples/postponed_broken.py b/docs/examples/postponed_broken.py new file mode 100644 index 0000000..67ea4a8 --- /dev/null +++ b/docs/examples/postponed_broken.py @@ -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))) diff --git a/docs/examples/postponed_works.py b/docs/examples/postponed_works.py new file mode 100644 index 0000000..a4a8243 --- /dev/null +++ b/docs/examples/postponed_works.py @@ -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))) diff --git a/docs/index.rst b/docs/index.rst index be876f9..f6cf551 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py index 4ecac89..9220fca 100644 --- a/pydantic/dataclasses.py +++ b/pydantic/dataclasses.py @@ -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) diff --git a/pydantic/main.py b/pydantic/main.py index 0c08af6..d13b088 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -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) diff --git a/tests/test_py37.py b/tests/test_py37.py index 382a4ca..68ed363 100644 --- a/tests/test_py37.py +++ b/tests/test_py37.py @@ -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'