From a4292aa24f88925d4c625edbd5340047800e1b68 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Aug 2019 13:22:09 +0900 Subject: [PATCH 1/9] support ForwardRef in Python 3.6 --- pydantic/main.py | 3 +- pydantic/utils.py | 12 +- tests/test_py36.py | 300 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 310 insertions(+), 5 deletions(-) create mode 100644 tests/test_py36.py diff --git a/pydantic/main.py b/pydantic/main.py index 624ba2a..f508174 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -199,8 +199,7 @@ class MetaModel(ABCMeta): 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(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..c73048a 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -44,7 +44,7 @@ try: from typing import ForwardRef # type: ignore except ImportError: # python 3.6 - ForwardRef = None + from typing import _ForwardRef as ForwardRef # type: ignore if TYPE_CHECKING: # pragma: no cover from .main import BaseModel # noqa: F401 @@ -277,7 +277,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 +338,10 @@ 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 + if sys.version_info >= (3, 7): + field.type_ = field.type_._evaluate(globalns, localns or None) # type: ignore + else: + field.type_ = field.type_._eval_type(globalns, localns or None) field.prepare() if field.sub_fields: for sub_f in field.sub_fields: diff --git a/tests/test_py36.py b/tests/test_py36.py new file mode 100644 index 0000000..278320b --- /dev/null +++ b/tests/test_py36.py @@ -0,0 +1,300 @@ +""" +Tests for python 3.6 behaviour, eg _ForwardRef. +""" +import sys + +import pytest + +from pydantic import ConfigError, ValidationError + +skip_not_36 = pytest.mark.skipif(sys.version_info > (3, 6), reason='testing == 3.6.x behaviour only') + + +@skip_not_36 +def test_basic_forward_ref(create_module): + module = create_module( + """ +from typing import _ForwardRef, Optional +from pydantic import BaseModel + +class Foo(BaseModel): + a: int + +FooRef = _ForwardRef('Foo') + +class Bar(BaseModel): + b: Optional[FooRef] +""" + ) + + assert module.Bar().dict() == {'b': None} + assert module.Bar(b={'a': '123'}).dict() == {'b': {'a': 123}} + + +@skip_not_36 +def test_self_forward_ref_module(create_module): + module = create_module( + """ +from typing import _ForwardRef +from pydantic import BaseModel + +Foo = _ForwardRef('Foo') + +class Foo(BaseModel): + a: int = 123 + b: Foo = None + +Foo.update_forward_refs() + """ + ) + + assert module.Foo().dict() == {'a': 123, 'b': None} + assert module.Foo(b={'a': '321'}).dict() == {'a': 123, 'b': {'a': 321, 'b': None}} + + +@skip_not_36 +def test_self_forward_ref_collection(create_module): + module = create_module( + """ +from typing import _ForwardRef, List, Dict +from pydantic import BaseModel + +Foo = _ForwardRef('Foo') + +class Foo(BaseModel): + a: int = 123 + b: Foo = None + c: List[Foo] = [] + d: Dict[str, Foo] = {} + +Foo.update_forward_refs() + """ + ) + + assert module.Foo().dict() == {'a': 123, 'b': None, 'c': [], 'd': {}} + assert module.Foo(b={'a': '321'}, c=[{'a': 234}], d={'bar': {'a': 345}}).dict() == { + 'a': 123, + 'b': {'a': 321, 'b': None, 'c': [], 'd': {}}, + 'c': [{'a': 234, 'b': None, 'c': [], 'd': {}}], + 'd': {'bar': {'a': 345, 'b': None, 'c': [], 'd': {}}}, + } + + with pytest.raises(ValidationError) as exc_info: + module.Foo(b={'a': '321'}, c=[{'b': 234}], d={'bar': {'a': 345}}) + assert exc_info.value.errors() == [ + {'loc': ('c', 0, 'b'), 'msg': 'value is not a valid dict', 'type': 'type_error.dict'} + ] + + +@skip_not_36 +def test_self_forward_ref_local(create_module): + module = create_module( + """ +from typing import _ForwardRef +from pydantic import BaseModel + +def main(): + Foo = _ForwardRef('Foo') + + class Foo(BaseModel): + a: int = 123 + b: Foo = None + + Foo.update_forward_refs() + return Foo + """ + ) + Foo = module.main() + assert Foo().dict() == {'a': 123, 'b': None} + assert Foo(b={'a': '321'}).dict() == {'a': 123, 'b': {'a': 321, 'b': None}} + + +@skip_not_36 +def test_missing_update_forward_refs(create_module): + module = create_module( + """ +from typing import _ForwardRef +from pydantic import BaseModel + +Foo = _ForwardRef('Foo') + +class Foo(BaseModel): + a: int = 123 + b: Foo = None + """ + ) + with pytest.raises(ConfigError) as exc_info: + module.Foo(b=123) + assert str(exc_info.value).startswith('field "b" not yet prepared so type is still a ForwardRef') + + +@skip_not_36 +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_36 +def test_forward_ref_sub_types(create_module): + module = create_module( + """ +from typing import _ForwardRef, Union + +from pydantic import BaseModel + + +class Leaf(BaseModel): + a: str + + +TreeType = Union[_ForwardRef('Node'), Leaf] + + +class Node(BaseModel): + value: int + left: TreeType + right: TreeType + + +Node.update_forward_refs() + """ + ) + Node = module.Node + Leaf = module.Leaf + data = {'value': 3, 'left': {'a': 'foo'}, 'right': {'value': 5, 'left': {'a': 'bar'}, 'right': {'a': 'buzz'}}} + + node = Node(**data) + assert isinstance(node.left, Leaf) + assert isinstance(node.right, Node) + + +@skip_not_36 +def test_forward_ref_nested_sub_types(create_module): + module = create_module( + """ +from typing import _ForwardRef, Tuple, Union + +from pydantic import BaseModel + + +class Leaf(BaseModel): + a: str + + +TreeType = Union[Union[Tuple[_ForwardRef('Node'), str], int], Leaf] + + +class Node(BaseModel): + value: int + left: TreeType + right: TreeType + + +Node.update_forward_refs() + """ + ) + Node = module.Node + Leaf = module.Leaf + data = { + 'value': 3, + 'left': {'a': 'foo'}, + 'right': [{'value': 5, 'left': {'a': 'bar'}, 'right': {'a': 'buzz'}}, 'test'], + } + + node = Node(**data) + assert isinstance(node.left, Leaf) + assert isinstance(node.right[0], Node) + + +@skip_not_36 +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_36 +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'], + }, + }, + } From a1f4f2ada282a2622dc5838be5d92d59c33a932d Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Aug 2019 15:20:15 +0900 Subject: [PATCH 2/9] add evaluate_forwardref() --- pydantic/utils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pydantic/utils.py b/pydantic/utils.py index c73048a..56843c6 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -42,10 +42,19 @@ 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 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 from .main import Field # noqa: F401 @@ -338,10 +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: - if sys.version_info >= (3, 7): - field.type_ = field.type_._evaluate(globalns, localns or None) # type: ignore - else: - field.type_ = field.type_._eval_type(globalns, localns or None) + field.type_ = evaluate_forwardref(field.type_, globalns, localns or None) field.prepare() if field.sub_fields: for sub_f in field.sub_fields: From 45b4223ddb9ae867fef9ca45b248745980b9cfbf Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Aug 2019 15:45:25 +0900 Subject: [PATCH 3/9] update README and History --- HISTORY.rst | 3 +++ docs/index.rst | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a48109c..1a3be61 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,9 @@ History ------- +v0.32 (unreleased) +.................. +* 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). From 572239b6924509783b061212eafaa32283abadff Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Aug 2019 16:01:59 +0900 Subject: [PATCH 4/9] remove unnecessary a assignment --- pydantic/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pydantic/main.py b/pydantic/main.py index f508174..8de06f9 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -198,8 +198,7 @@ class MetaModel(ABCMeta): f.prepare() set_extra(config, name) - annotations = namespace.get('__annotations__', {}) - 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'): From 84d7c4aaf272ffb6130d6f3f1e24e92998338dc6 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Aug 2019 16:24:16 +0900 Subject: [PATCH 5/9] fix skip_not_36 condition --- tests/test_py36.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_py36.py b/tests/test_py36.py index 278320b..9f11419 100644 --- a/tests/test_py36.py +++ b/tests/test_py36.py @@ -7,7 +7,7 @@ import pytest from pydantic import ConfigError, ValidationError -skip_not_36 = pytest.mark.skipif(sys.version_info > (3, 6), reason='testing == 3.6.x behaviour only') +skip_not_36 = pytest.mark.skipif(sys.version_info >= (3, 7), reason='testing == 3.6.x behaviour only') @skip_not_36 From 8e0455c9c6506fa5c4024aec3ac443778180e43d Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Aug 2019 21:42:11 +0900 Subject: [PATCH 6/9] change test_py37.py to test_forward_ref.py --- tests/{test_py37.py => test_forward_ref.py} | 53 ++-- tests/test_py36.py | 300 -------------------- 2 files changed, 33 insertions(+), 320 deletions(-) rename tests/{test_py37.py => test_forward_ref.py} (89%) delete mode 100644 tests/test_py36.py diff --git a/tests/test_py37.py b/tests/test_forward_ref.py similarity index 89% rename from tests/test_py37.py rename to tests/test_forward_ref.py index 542ebb0..d8e28ac 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,11 +38,14 @@ 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 +try: + from typing import ForwardRef +except ImportError: + from typing import _ForwardRef as ForwardRef from pydantic import BaseModel class Foo(BaseModel): @@ -62,18 +62,20 @@ 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 +try: + from typing import ForwardRef +except ImportError: + from typing import _ForwardRef as ForwardRef from pydantic import BaseModel Foo = ForwardRef('Foo') class Foo(BaseModel): a: int = 123 - b: Foo = None + b: 'Foo' = None Foo.update_forward_refs() """ @@ -83,11 +85,14 @@ 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 +try: + from typing import ForwardRef +except ImportError: + from typing import _ForwardRef as ForwardRef from pydantic import BaseModel Foo = ForwardRef('Foo') @@ -117,11 +122,13 @@ Foo.update_forward_refs() ] -@skip_not_37 def test_self_forward_ref_local(create_module): module = create_module( """ -from typing import ForwardRef +try: + from typing import ForwardRef +except ImportError: + from typing import _ForwardRef as ForwardRef from pydantic import BaseModel def main(): @@ -140,11 +147,13 @@ 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 +try: + from typing import ForwardRef +except ImportError: + from typing import _ForwardRef as ForwardRef from pydantic import BaseModel Foo = ForwardRef('Foo') @@ -176,12 +185,14 @@ 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 +try: + from typing import ForwardRef +except ImportError: + from typing import _ForwardRef as ForwardRef from pydantic import BaseModel @@ -210,12 +221,14 @@ 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 +try: + from typing import ForwardRef +except ImportError: + from typing import _ForwardRef as ForwardRef from pydantic import BaseModel diff --git a/tests/test_py36.py b/tests/test_py36.py deleted file mode 100644 index 9f11419..0000000 --- a/tests/test_py36.py +++ /dev/null @@ -1,300 +0,0 @@ -""" -Tests for python 3.6 behaviour, eg _ForwardRef. -""" -import sys - -import pytest - -from pydantic import ConfigError, ValidationError - -skip_not_36 = pytest.mark.skipif(sys.version_info >= (3, 7), reason='testing == 3.6.x behaviour only') - - -@skip_not_36 -def test_basic_forward_ref(create_module): - module = create_module( - """ -from typing import _ForwardRef, Optional -from pydantic import BaseModel - -class Foo(BaseModel): - a: int - -FooRef = _ForwardRef('Foo') - -class Bar(BaseModel): - b: Optional[FooRef] -""" - ) - - assert module.Bar().dict() == {'b': None} - assert module.Bar(b={'a': '123'}).dict() == {'b': {'a': 123}} - - -@skip_not_36 -def test_self_forward_ref_module(create_module): - module = create_module( - """ -from typing import _ForwardRef -from pydantic import BaseModel - -Foo = _ForwardRef('Foo') - -class Foo(BaseModel): - a: int = 123 - b: Foo = None - -Foo.update_forward_refs() - """ - ) - - assert module.Foo().dict() == {'a': 123, 'b': None} - assert module.Foo(b={'a': '321'}).dict() == {'a': 123, 'b': {'a': 321, 'b': None}} - - -@skip_not_36 -def test_self_forward_ref_collection(create_module): - module = create_module( - """ -from typing import _ForwardRef, List, Dict -from pydantic import BaseModel - -Foo = _ForwardRef('Foo') - -class Foo(BaseModel): - a: int = 123 - b: Foo = None - c: List[Foo] = [] - d: Dict[str, Foo] = {} - -Foo.update_forward_refs() - """ - ) - - assert module.Foo().dict() == {'a': 123, 'b': None, 'c': [], 'd': {}} - assert module.Foo(b={'a': '321'}, c=[{'a': 234}], d={'bar': {'a': 345}}).dict() == { - 'a': 123, - 'b': {'a': 321, 'b': None, 'c': [], 'd': {}}, - 'c': [{'a': 234, 'b': None, 'c': [], 'd': {}}], - 'd': {'bar': {'a': 345, 'b': None, 'c': [], 'd': {}}}, - } - - with pytest.raises(ValidationError) as exc_info: - module.Foo(b={'a': '321'}, c=[{'b': 234}], d={'bar': {'a': 345}}) - assert exc_info.value.errors() == [ - {'loc': ('c', 0, 'b'), 'msg': 'value is not a valid dict', 'type': 'type_error.dict'} - ] - - -@skip_not_36 -def test_self_forward_ref_local(create_module): - module = create_module( - """ -from typing import _ForwardRef -from pydantic import BaseModel - -def main(): - Foo = _ForwardRef('Foo') - - class Foo(BaseModel): - a: int = 123 - b: Foo = None - - Foo.update_forward_refs() - return Foo - """ - ) - Foo = module.main() - assert Foo().dict() == {'a': 123, 'b': None} - assert Foo(b={'a': '321'}).dict() == {'a': 123, 'b': {'a': 321, 'b': None}} - - -@skip_not_36 -def test_missing_update_forward_refs(create_module): - module = create_module( - """ -from typing import _ForwardRef -from pydantic import BaseModel - -Foo = _ForwardRef('Foo') - -class Foo(BaseModel): - a: int = 123 - b: Foo = None - """ - ) - with pytest.raises(ConfigError) as exc_info: - module.Foo(b=123) - assert str(exc_info.value).startswith('field "b" not yet prepared so type is still a ForwardRef') - - -@skip_not_36 -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_36 -def test_forward_ref_sub_types(create_module): - module = create_module( - """ -from typing import _ForwardRef, Union - -from pydantic import BaseModel - - -class Leaf(BaseModel): - a: str - - -TreeType = Union[_ForwardRef('Node'), Leaf] - - -class Node(BaseModel): - value: int - left: TreeType - right: TreeType - - -Node.update_forward_refs() - """ - ) - Node = module.Node - Leaf = module.Leaf - data = {'value': 3, 'left': {'a': 'foo'}, 'right': {'value': 5, 'left': {'a': 'bar'}, 'right': {'a': 'buzz'}}} - - node = Node(**data) - assert isinstance(node.left, Leaf) - assert isinstance(node.right, Node) - - -@skip_not_36 -def test_forward_ref_nested_sub_types(create_module): - module = create_module( - """ -from typing import _ForwardRef, Tuple, Union - -from pydantic import BaseModel - - -class Leaf(BaseModel): - a: str - - -TreeType = Union[Union[Tuple[_ForwardRef('Node'), str], int], Leaf] - - -class Node(BaseModel): - value: int - left: TreeType - right: TreeType - - -Node.update_forward_refs() - """ - ) - Node = module.Node - Leaf = module.Leaf - data = { - 'value': 3, - 'left': {'a': 'foo'}, - 'right': [{'value': 5, 'left': {'a': 'bar'}, 'right': {'a': 'buzz'}}, 'test'], - } - - node = Node(**data) - assert isinstance(node.left, Leaf) - assert isinstance(node.right[0], Node) - - -@skip_not_36 -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_36 -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'], - }, - }, - } From 879a24ecb7a855eb617334ce89e042d9c8fe8685 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Aug 2019 15:10:26 +0900 Subject: [PATCH 7/9] delete @skip_not_37 on forward_ref tests --- tests/test_forward_ref.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index d8e28ac..88e140b 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -168,11 +168,9 @@ 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 __future__ import annotations from pydantic import UrlStr from pydantic.dataclasses import dataclass @@ -261,17 +259,15 @@ 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 __future__ import annotations from typing import List from pydantic import BaseModel, Schema class Account(BaseModel): name: str - subaccounts: List[Account] = [] + subaccounts: List['Account'] = [] Account.update_forward_refs() """ @@ -298,21 +294,19 @@ Account.update_forward_refs() } -@skip_not_37 def test_circular_reference_json_schema(create_module): module = create_module( """ -from __future__ import annotations from typing import List from pydantic import BaseModel, Schema class Owner(BaseModel): - account: Account + account: 'Account' class Account(BaseModel): name: str - owner: Owner - subaccounts: List[Account] = [] + owner: 'Owner' + subaccounts: List['Account'] = [] Account.update_forward_refs() Owner.update_forward_refs() From cffcb39c7c3250d05c6e992ca96efcb2295d04e7 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 5 Aug 2019 20:46:56 +0900 Subject: [PATCH 8/9] add unittest for python3.7 --- tests/test_forward_ref.py | 140 ++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 30 deletions(-) diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index 88e140b..0fd5bed 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -42,11 +42,8 @@ def test_basic_forward_ref(create_module): module = create_module( """ from typing import Optional -try: - from typing import ForwardRef -except ImportError: - from typing import _ForwardRef as ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef class Foo(BaseModel): a: int @@ -65,11 +62,8 @@ class Bar(BaseModel): def test_self_forward_ref_module(create_module): module = create_module( """ -try: - from typing import ForwardRef -except ImportError: - from typing import _ForwardRef as ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef Foo = ForwardRef('Foo') @@ -89,11 +83,8 @@ def test_self_forward_ref_collection(create_module): module = create_module( """ from typing import List, Dict -try: - from typing import ForwardRef -except ImportError: - from typing import _ForwardRef as ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef Foo = ForwardRef('Foo') @@ -125,11 +116,8 @@ Foo.update_forward_refs() def test_self_forward_ref_local(create_module): module = create_module( """ -try: - from typing import ForwardRef -except ImportError: - from typing import _ForwardRef as ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef def main(): Foo = ForwardRef('Foo') @@ -150,11 +138,8 @@ def main(): def test_missing_update_forward_refs(create_module): module = create_module( """ -try: - from typing import ForwardRef -except ImportError: - from typing import _ForwardRef as ForwardRef from pydantic import BaseModel +from pydantic.utils import ForwardRef Foo = ForwardRef('Foo') @@ -183,16 +168,29 @@ class Dataclass: 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 + +@dataclass +class Dataclass: + url: UrlStr + """ + ) + m = module.Dataclass('http://example.com ') + assert m.url == 'http://example.com' + + def test_forward_ref_sub_types(create_module): module = create_module( """ from typing import Union -try: - from typing import ForwardRef -except ImportError: - from typing import _ForwardRef as ForwardRef from pydantic import BaseModel - +from pydantic.utils import ForwardRef class Leaf(BaseModel): a: str @@ -223,12 +221,8 @@ def test_forward_ref_nested_sub_types(create_module): module = create_module( """ from typing import Tuple, Union -try: - from typing import ForwardRef -except ImportError: - from typing import _ForwardRef as ForwardRef from pydantic import BaseModel - +from pydantic.utils import ForwardRef class Leaf(BaseModel): a: str @@ -294,6 +288,43 @@ Account.update_forward_refs() } +@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 + +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'], + } + }, + } + + def test_circular_reference_json_schema(create_module): module = create_module( """ @@ -339,3 +370,52 @@ Owner.update_forward_refs() }, }, } + + +@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 + +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'], + }, + }, + } From 75afb3f1783a5d0a8393d48bd1f4ea4ccad40d48 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 5 Aug 2019 21:01:28 +0900 Subject: [PATCH 9/9] merge HISTORY --- HISTORY.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0c94bd8..961e73b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,15 +2,12 @@ History ------- -v0.32 (unreleased) -.................. -* support ``ForwardRef`` (without self-referencing annotations) in Python3.6, #706 by @koxudaxi - 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) ....................