diff --git a/HISTORY.rst b/HISTORY.rst index ac3565f..961e73b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,12 +2,12 @@ History ------- - v0.32 (unreleased) .................. * add model name to ``ValidationError`` error message, #676 by @dmontagu * **breaking change**: remove ``__getattr__`` and rename ``__values__`` to ``__dict__`` on ``BaseModel``, deprecation warning on use ``__values__`` attr, attributes access speed increased up to 14 times, #712 by @MrMrRobat +* support ``ForwardRef`` (without self-referencing annotations) in Python3.6, #706 by @koxudaxi v0.31.1 (2019-07-31) .................... diff --git a/docs/index.rst b/docs/index.rst index 47389fc..75720f2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -277,7 +277,7 @@ The ellipsis ``...`` just means "Required" same as annotation only declarations Self-referencing Models ~~~~~~~~~~~~~~~~~~~~~~~ -Since ``python 3.7``, data structures with self-referencing models are also supported, provided the function +Data structures with self-referencing models are also supported, provided the function ``update_forward_refs()`` is called once the model is created (you will be reminded with a friendly error message if you don't). @@ -287,7 +287,7 @@ Within the model, you can refer to the not-yet-constructed model by a string : (This script is complete, it should run "as is") -You can also refer it by its type, provided you import ``annotations`` (see +Since ``python 3.7``, You can also refer it by its type, provided you import ``annotations`` (see :ref:`the relevant paragraph ` for support depending on Python and pydantic versions). diff --git a/pydantic/main.py b/pydantic/main.py index 749df9f..7bbfff7 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -198,9 +198,7 @@ class MetaModel(ABCMeta): f.prepare() set_extra(config, name) - annotations = namespace.get('__annotations__', {}) - if sys.version_info >= (3, 7): - annotations = resolve_annotations(annotations, namespace.get('__module__', None)) + annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None)) class_vars = set() if (namespace.get('__module__'), namespace.get('__qualname__')) != ('pydantic.main', 'BaseModel'): diff --git a/pydantic/utils.py b/pydantic/utils.py index 79ebd54..56843c6 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -42,9 +42,18 @@ except ImportError: try: from typing import ForwardRef # type: ignore + + def evaluate_forwardref(type_, globalns, localns): # type: ignore + return type_._evaluate(globalns, localns) + + except ImportError: # python 3.6 - ForwardRef = None + from typing import _ForwardRef as ForwardRef # type: ignore + + def evaluate_forwardref(type_, globalns, localns): # type: ignore + return type_._eval_type(globalns, localns) + if TYPE_CHECKING: # pragma: no cover from .main import BaseModel # noqa: F401 @@ -277,7 +286,10 @@ def resolve_annotations(raw_annotations: Dict[str, AnyType], module_name: Option annotations = {} for name, value in raw_annotations.items(): if isinstance(value, str): - value = ForwardRef(value, is_argument=False) + if sys.version_info >= (3, 7): + value = ForwardRef(value, is_argument=False) + else: + value = ForwardRef(value) try: value = _eval_type(value, base_globals, None) except NameError: @@ -335,7 +347,7 @@ def update_field_forward_refs(field: 'Field', globalns: Any, localns: Any) -> No Try to update ForwardRefs on fields based on this Field, globalns and localns. """ if type(field.type_) == ForwardRef: - field.type_ = field.type_._evaluate(globalns, localns or None) # type: ignore + field.type_ = evaluate_forwardref(field.type_, globalns, localns or None) field.prepare() if field.sub_fields: for sub_f in field.sub_fields: diff --git a/tests/test_py37.py b/tests/test_forward_ref.py similarity index 70% rename from tests/test_py37.py rename to tests/test_forward_ref.py index 542ebb0..0fd5bed 100644 --- a/tests/test_py37.py +++ b/tests/test_forward_ref.py @@ -1,6 +1,3 @@ -""" -Tests for python 3.7 behaviour, eg postponed annotations and ForwardRef. -""" import sys import pytest @@ -41,12 +38,12 @@ class Model(BaseModel): assert module.Model().dict() == {'a': None} -@skip_not_37 def test_basic_forward_ref(create_module): module = create_module( """ -from typing import ForwardRef, Optional +from typing import Optional from pydantic import BaseModel +from pydantic.utils import ForwardRef class Foo(BaseModel): a: int @@ -62,18 +59,17 @@ class Bar(BaseModel): assert module.Bar(b={'a': '123'}).dict() == {'b': {'a': 123}} -@skip_not_37 def test_self_forward_ref_module(create_module): module = create_module( """ -from typing import ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef Foo = ForwardRef('Foo') class Foo(BaseModel): a: int = 123 - b: Foo = None + b: 'Foo' = None Foo.update_forward_refs() """ @@ -83,12 +79,12 @@ Foo.update_forward_refs() assert module.Foo(b={'a': '321'}).dict() == {'a': 123, 'b': {'a': 321, 'b': None}} -@skip_not_37 def test_self_forward_ref_collection(create_module): module = create_module( """ -from typing import ForwardRef, List, Dict +from typing import List, Dict from pydantic import BaseModel +from pydantic.utils import ForwardRef Foo = ForwardRef('Foo') @@ -117,12 +113,11 @@ Foo.update_forward_refs() ] -@skip_not_37 def test_self_forward_ref_local(create_module): module = create_module( """ -from typing import ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef def main(): Foo = ForwardRef('Foo') @@ -140,12 +135,11 @@ def main(): assert Foo(b={'a': '321'}).dict() == {'a': 123, 'b': {'a': 321, 'b': None}} -@skip_not_37 def test_missing_update_forward_refs(create_module): module = create_module( """ -from typing import ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef Foo = ForwardRef('Foo') @@ -159,10 +153,25 @@ class Foo(BaseModel): 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 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' + + +@skip_not_37 +def test_forward_ref_dataclass_with_future_annotations(create_module): + module = create_module( + """ from __future__ import annotations from pydantic import UrlStr from pydantic.dataclasses import dataclass @@ -176,14 +185,12 @@ class Dataclass: assert m.url == 'http://example.com' -@skip_not_37 def test_forward_ref_sub_types(create_module): module = create_module( """ -from typing import ForwardRef, Union - +from typing import Union from pydantic import BaseModel - +from pydantic.utils import ForwardRef class Leaf(BaseModel): a: str @@ -210,14 +217,12 @@ Node.update_forward_refs() assert isinstance(node.right, Node) -@skip_not_37 def test_forward_ref_nested_sub_types(create_module): module = create_module( """ -from typing import ForwardRef, Tuple, Union - +from typing import Tuple, Union from pydantic import BaseModel - +from pydantic.utils import ForwardRef class Leaf(BaseModel): a: str @@ -248,10 +253,45 @@ Node.update_forward_refs() assert isinstance(node.right[0], Node) -@skip_not_37 def test_self_reference_json_schema(create_module): module = create_module( """ +from typing import List +from pydantic import BaseModel, Schema + +class Account(BaseModel): + name: str + subaccounts: List['Account'] = [] + +Account.update_forward_refs() + """ + ) + Account = module.Account + assert Account.schema() == { + '$ref': '#/definitions/Account', + 'definitions': { + 'Account': { + 'title': 'Account', + 'type': 'object', + 'properties': { + 'name': {'title': 'Name', 'type': 'string'}, + 'subaccounts': { + 'title': 'Subaccounts', + 'default': [], + 'type': 'array', + 'items': {'$ref': '#/definitions/Account'}, + }, + }, + 'required': ['name'], + } + }, + } + + +@skip_not_37 +def test_self_reference_json_schema_with_future_annotations(create_module): + module = create_module( + """ from __future__ import annotations from typing import List from pydantic import BaseModel, Schema @@ -285,10 +325,57 @@ Account.update_forward_refs() } -@skip_not_37 def test_circular_reference_json_schema(create_module): module = create_module( """ +from typing import List +from pydantic import BaseModel, Schema + +class Owner(BaseModel): + account: 'Account' + +class Account(BaseModel): + name: str + owner: 'Owner' + subaccounts: List['Account'] = [] + +Account.update_forward_refs() +Owner.update_forward_refs() + """ + ) + Account = module.Account + assert Account.schema() == { + '$ref': '#/definitions/Account', + 'definitions': { + 'Account': { + 'title': 'Account', + 'type': 'object', + 'properties': { + 'name': {'title': 'Name', 'type': 'string'}, + 'owner': {'$ref': '#/definitions/Owner'}, + 'subaccounts': { + 'title': 'Subaccounts', + 'default': [], + 'type': 'array', + 'items': {'$ref': '#/definitions/Account'}, + }, + }, + 'required': ['name', 'owner'], + }, + 'Owner': { + 'title': 'Owner', + 'type': 'object', + 'properties': {'account': {'$ref': '#/definitions/Account'}}, + 'required': ['account'], + }, + }, + } + + +@skip_not_37 +def test_circular_reference_json_schema_with_future_annotations(create_module): + module = create_module( + """ from __future__ import annotations from typing import List from pydantic import BaseModel, Schema