Merge pull request #706 from koxudaxi/support_forwardred_in_python36

support ForwardRef in Python 3.6
This commit is contained in:
Samuel Colvin
2019-08-05 13:27:23 +01:00
committed by GitHub
5 changed files with 131 additions and 34 deletions
+1 -1
View File
@@ -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)
....................
+2 -2
View File
@@ -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 <postponed_annotations>` for support depending on Python
and pydantic versions).
+1 -3
View File
@@ -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'):
+15 -3
View File
@@ -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:
+112 -25
View File
@@ -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