Repository: lovasoa/marshmallow_dataclass

Issue:
typing-extensions constraint causes CI failures on dependant projects
marshmallow_dataclass 8.5.3 depends on `typing-extensions (>=3.7.2,<3.8.0)`
https://github.com/lovasoa/marshmallow_dataclass/blob/fa6c28980ccfe45742cdc9430bfb3b737690935f/setup.py#L32

but black 21.9b0 depends on `typing-extensions>=3.10.0.0`
https://github.com/psf/black/blob/79575f3376f043186d8b8c4885ef51c6b3c36246/setup.py#L82

Are there any plans to relax constraints on typing-extensions?

Sample run failure:
https://github.com/hacf-fr/renault-api/pull/375/checks?check_run_id=3728756130

Comments:

Patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 7891d61..878b2bc 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python_version: [3.6, 3.7, 3.8, 3.9, pypy3]
+        python_version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"]
 
     steps:
     - uses: actions/checkout@v2
@@ -31,7 +31,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install --pre -e '.[dev]'
     - name: Pre-commit hooks
-      if: ${{ matrix.python_version != 'pypy3' }}
+      if: ${{ matrix.python_version != 'pypy3' && matrix.python_version != '3.6' }}
       run: pre-commit run --all-files
     - name: Test with pytest
       run: pytest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4a79dc2..dde3d4d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,27 +1,27 @@
 repos:
 - repo: https://github.com/asottile/pyupgrade
-  rev: v2.7.4
+  rev: v2.31.0 # Later versions do not support python 3.6
   hooks:
   - id: pyupgrade
     args: ["--py36-plus"]
 - repo: https://github.com/python/black
-  rev: 20.8b1
+  rev: 22.3.0
   hooks:
   - id: black
     language_version: python3
 - repo: https://gitlab.com/pycqa/flake8
-  rev: 3.8.4
+  rev: 3.9.2
   hooks:
   - id: flake8
     additional_dependencies: ['flake8-bugbear==19.8.0']
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v0.790
+  rev: v0.931 # Later versions do not support python 3.6
   hooks:
   - id: mypy
     additional_dependencies: [marshmallow-enum,typeguard,marshmallow]
     args: [--show-error-codes]
 - repo: https://github.com/asottile/blacken-docs
-  rev: v1.9.1
+  rev: v1.12.0 # Later versions do not support python 3.6
   hooks:
   - id: blacken-docs
     additional_dependencies: [black==19.3b0]
diff --git a/README.md b/README.md
index 47788ef..2079155 100644
--- a/README.md
+++ b/README.md
@@ -92,7 +92,32 @@ PersonSchema = marshmallow_dataclass.class_schema(Person)
 
 The type of your fields must be either basic 
 [types supported by marshmallow](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.TYPE_MAPPING)
-(such as `float`, `str`, `bytes`, `datetime`, ...), or other dataclasses.
+(such as `float`, `str`, `bytes`, `datetime`, ...), `Union`, or other dataclasses.
+
+### Union (de)serialization coercion 
+
+Typically the Union type; `Union[X, Y]` means—from a set theory perspective—either `X` or `Y`, i.e., an unordered set, howevever the order of the sub-types defines the precedence when attempting to ether deserialize or serialize the value per [here](https://github.com/lovasoa/marshmallow_dataclass/blob/master/marshmallow_dataclass/union_field.py). 
+
+For example, 
+
+```python
+from typing import Union
+
+from dataclasses import dataclass
+
+
+@dataclass
+class Person:
+    name: str
+    age: Union[int, float]
+
+
+PersonSchema = marshmallow_dataclass.class_schema(Person)
+PersonSchema().load({"name": "jane", "age": 50.0})
+# => Person(name="jane", age=50)
+```
+
+will first (sucessfully) try to coerce `50.0` to an `int`. If coercion is not desired the `Any` type can be used with the caveat that values will not be type checked without additional [validation](https://marshmallow.readthedocs.io/en/stable/marshmallow.validate.html).
 
 ### Customizing generated fields
 
diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py
index 0459b95..30c2947 100644
--- a/marshmallow_dataclass/__init__.py
+++ b/marshmallow_dataclass/__init__.py
@@ -37,9 +37,10 @@ Full example::
 import collections.abc
 import dataclasses
 import inspect
+import types
 import warnings
 from enum import EnumMeta
-from functools import lru_cache
+from functools import lru_cache, partial
 from typing import (
     Any,
     Callable,
@@ -53,6 +54,7 @@ from typing import (
     TypeVar,
     Union,
     cast,
+    get_type_hints,
     overload,
     Sequence,
     FrozenSet,
@@ -61,6 +63,9 @@ from typing import (
 import marshmallow
 import typing_inspect
 
+from marshmallow_dataclass.lazy_class_attribute import lazy_class_attribute
+
+
 __all__ = ["dataclass", "add_schema", "class_schema", "field_for_schema", "NewType"]
 
 NoneType = type(None)
@@ -83,6 +88,7 @@ def dataclass(
     unsafe_hash: bool = False,
     frozen: bool = False,
     base_schema: Optional[Type[marshmallow.Schema]] = None,
+    cls_frame: Optional[types.FrameType] = None,
 ) -> Type[_U]:
     ...
 
@@ -96,6 +102,7 @@ def dataclass(
     unsafe_hash: bool = False,
     frozen: bool = False,
     base_schema: Optional[Type[marshmallow.Schema]] = None,
+    cls_frame: Optional[types.FrameType] = None,
 ) -> Callable[[Type[_U]], Type[_U]]:
     ...
 
@@ -112,12 +119,15 @@ def dataclass(
     unsafe_hash: bool = False,
     frozen: bool = False,
     base_schema: Optional[Type[marshmallow.Schema]] = None,
+    cls_frame: Optional[types.FrameType] = None,
 ) -> Union[Type[_U], Callable[[Type[_U]], Type[_U]]]:
     """
     This decorator does the same as dataclasses.dataclass, but also applies :func:`add_schema`.
     It adds a `.Schema` attribute to the class object
 
     :param base_schema: marshmallow schema used as a base class when deriving dataclass schema
+    :param cls_frame: frame of cls definition, used to obtain locals with other classes definitions.
+        If None is passed the caller frame will be treated as cls_frame
 
     >>> @dataclass
     ... class Artist:
@@ -140,9 +150,10 @@ def dataclass(
     dc = dataclasses.dataclass(  # type: ignore
         _cls, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen
     )
+    cls_frame = cls_frame or inspect.stack()[1][0]
     if _cls is None:
-        return lambda cls: add_schema(dc(cls), base_schema)
-    return add_schema(dc, base_schema)
+        return lambda cls: add_schema(dc(cls), base_schema, cls_frame=cls_frame)
+    return add_schema(dc, base_schema, cls_frame=cls_frame)
 
 
 @overload
@@ -159,18 +170,21 @@ def add_schema(
 
 @overload
 def add_schema(
-    _cls: Type[_U], base_schema: Type[marshmallow.Schema] = None
+    _cls: Type[_U],
+    base_schema: Type[marshmallow.Schema] = None,
+    cls_frame: types.FrameType = None,
 ) -> Type[_U]:
     ...
 
 
-def add_schema(_cls=None, base_schema=None):
+def add_schema(_cls=None, base_schema=None, cls_frame=None):
     """
     This decorator adds a marshmallow schema as the 'Schema' attribute in a dataclass.
     It uses :func:`class_schema` internally.
 
     :param type _cls: The dataclass to which a Schema should be added
     :param base_schema: marshmallow schema used as a base class when deriving dataclass schema
+    :param cls_frame: frame of cls definition
 
     >>> class BaseSchema(marshmallow.Schema):
     ...   def on_bind_field(self, field_name, field_obj):
@@ -187,20 +201,27 @@ def add_schema(_cls=None, base_schema=None):
 
     def decorator(clazz: Type[_U]) -> Type[_U]:
         # noinspection PyTypeHints
-        clazz.Schema = class_schema(clazz, base_schema)  # type: ignore
+        clazz.Schema = lazy_class_attribute(  # type: ignore
+            partial(class_schema, clazz, base_schema, cls_frame),
+            "Schema",
+            clazz.__name__,
+        )
         return clazz
 
     return decorator(_cls) if _cls else decorator
 
 
 def class_schema(
-    clazz: type, base_schema: Optional[Type[marshmallow.Schema]] = None
+    clazz: type,
+    base_schema: Optional[Type[marshmallow.Schema]] = None,
+    clazz_frame: types.FrameType = None,
 ) -> Type[marshmallow.Schema]:
     """
     Convert a class to a marshmallow schema
 
     :param clazz: A python class (may be a dataclass)
     :param base_schema: marshmallow schema used as a base class when deriving dataclass schema
+    :param clazz_frame: frame of cls definition
     :return: A marshmallow Schema corresponding to the dataclass
 
     .. note::
@@ -315,12 +336,14 @@ def class_schema(
     """
     if not dataclasses.is_dataclass(clazz):
         clazz = dataclasses.dataclass(clazz)
-    return _internal_class_schema(clazz, base_schema)
+    return _internal_class_schema(clazz, base_schema, clazz_frame)
 
 
 @lru_cache(maxsize=MAX_CLASS_SCHEMA_CACHE_SIZE)
 def _internal_class_schema(
-    clazz: type, base_schema: Optional[Type[marshmallow.Schema]] = None
+    clazz: type,
+    base_schema: Optional[Type[marshmallow.Schema]] = None,
+    clazz_frame: types.FrameType = None,
 ) -> Type[marshmallow.Schema]:
     try:
         # noinspection PyDataclass
@@ -339,7 +362,7 @@ def _internal_class_schema(
                 "****** WARNING ******"
             )
             created_dataclass: type = dataclasses.dataclass(clazz)
-            return _internal_class_schema(created_dataclass, base_schema)
+            return _internal_class_schema(created_dataclass, base_schema, clazz_frame)
         except Exception:
             raise TypeError(
                 f"{getattr(clazz, '__name__', repr(clazz))} is not a dataclass and cannot be turned into one."
@@ -351,12 +374,20 @@ def _internal_class_schema(
         for k, v in inspect.getmembers(clazz)
         if hasattr(v, "__marshmallow_hook__") or k in MEMBERS_WHITELIST
     }
+
     # Update the schema members to contain marshmallow fields instead of dataclass fields
+    type_hints = get_type_hints(
+        clazz, localns=clazz_frame.f_locals if clazz_frame else None
+    )
     attributes.update(
         (
             field.name,
             field_for_schema(
-                field.type, _get_field_default(field), field.metadata, base_schema
+                type_hints[field.name],
+                _get_field_default(field),
+                field.metadata,
+                base_schema,
+                clazz_frame,
             ),
         )
         for field in fields
@@ -381,6 +412,7 @@ def _field_by_supertype(
     newtype_supertype: Type,
     metadata: dict,
     base_schema: Optional[Type[marshmallow.Schema]],
+    typ_frame: Optional[types.FrameType],
 ) -> marshmallow.fields.Field:
     """
     Return a new field for fields based on a super field. (Usually spawned from NewType)
@@ -411,6 +443,7 @@ def _field_by_supertype(
             metadata=metadata,
             default=default,
             base_schema=base_schema,
+            typ_frame=typ_frame,
         )
 
 
@@ -432,7 +465,10 @@ def _generic_type_add_any(typ: type) -> type:
 
 
 def _field_for_generic_type(
-    typ: type, base_schema: Optional[Type[marshmallow.Schema]], **metadata: Any
+    typ: type,
+    base_schema: Optional[Type[marshmallow.Schema]],
+    typ_frame: Optional[types.FrameType],
+    **metadata: Any,
 ) -> Optional[marshmallow.fields.Field]:
     """
     If the type is a generic interface, resolve the arguments and construct the appropriate Field.
@@ -444,7 +480,9 @@ def _field_for_generic_type(
         type_mapping = base_schema.TYPE_MAPPING if base_schema else {}
 
         if origin in (list, List):
-            child_type = field_for_schema(arguments[0], base_schema=base_schema)
+            child_type = field_for_schema(
+                arguments[0], base_schema=base_schema, typ_frame=typ_frame
+            )
             list_type = cast(
                 Type[marshmallow.fields.List],
                 type_mapping.get(List, marshmallow.fields.List),
@@ -453,25 +491,32 @@ def _field_for_generic_type(
         if origin in (collections.abc.Sequence, Sequence):
             from . import collection_field
 
-            child_type = field_for_schema(arguments[0], base_schema=base_schema)
+            child_type = field_for_schema(
+                arguments[0], base_schema=base_schema, typ_frame=typ_frame
+            )
             return collection_field.Sequence(cls_or_instance=child_type, **metadata)
         if origin in (set, Set):
             from . import collection_field
 
-            child_type = field_for_schema(arguments[0], base_schema=base_schema)
+            child_type = field_for_schema(
+                arguments[0], base_schema=base_schema, typ_frame=typ_frame
+            )
             return collection_field.Set(
                 cls_or_instance=child_type, frozen=False, **metadata
             )
         if origin in (frozenset, FrozenSet):
             from . import collection_field
 
-            child_type = field_for_schema(arguments[0], base_schema=base_schema)
+            child_type = field_for_schema(
+                arguments[0], base_schema=base_schema, typ_frame=typ_frame
+            )
             return collection_field.Set(
                 cls_or_instance=child_type, frozen=True, **metadata
             )
         if origin in (tuple, Tuple):
             children = tuple(
-                field_for_schema(arg, base_schema=base_schema) for arg in arguments
+                field_for_schema(arg, base_schema=base_schema, typ_frame=typ_frame)
+                for arg in arguments
             )
             tuple_type = cast(
                 Type[marshmallow.fields.Tuple],
@@ -483,8 +528,12 @@ def _field_for_generic_type(
         elif origin in (dict, Dict, collections.abc.Mapping, Mapping):
             dict_type = type_mapping.get(Dict, marshmallow.fields.Dict)
             return dict_type(
-                keys=field_for_schema(arguments[0], base_schema=base_schema),
-                values=field_for_schema(arguments[1], base_schema=base_schema),
+                keys=field_for_schema(
+                    arguments[0], base_schema=base_schema, typ_frame=typ_frame
+                ),
+                values=field_for_schema(
+                    arguments[1], base_schema=base_schema, typ_frame=typ_frame
+                ),
                 **metadata,
             )
         elif typing_inspect.is_union_type(typ):
@@ -497,7 +546,10 @@ def _field_for_generic_type(
             subtypes = [t for t in arguments if t is not NoneType]  # type: ignore
             if len(subtypes) == 1:
                 return field_for_schema(
-                    subtypes[0], metadata=metadata, base_schema=base_schema
+                    subtypes[0],
+                    metadata=metadata,
+                    base_schema=base_schema,
+                    typ_frame=typ_frame,
                 )
             from . import union_field
 
@@ -506,7 +558,10 @@ def _field_for_generic_type(
                     (
                         subtyp,
                         field_for_schema(
-                            subtyp, metadata={"required": True}, base_schema=base_schema
+                            subtyp,
+                            metadata={"required": True},
+                            base_schema=base_schema,
+                            typ_frame=typ_frame,
                         ),
                     )
                     for subtyp in subtypes
@@ -521,6 +576,7 @@ def field_for_schema(
     default=marshmallow.missing,
     metadata: Mapping[str, Any] = None,
     base_schema: Optional[Type[marshmallow.Schema]] = None,
+    typ_frame: Optional[types.FrameType] = None,
 ) -> marshmallow.fields.Field:
     """
     Get a marshmallow Field corresponding to the given python type.
@@ -530,6 +586,7 @@ def field_for_schema(
     :param default: value to use for (de)serialization when the field is missing
     :param metadata: Additional parameters to pass to the marshmallow field constructor
     :param base_schema: marshmallow schema used as a base class when deriving dataclass schema
+    :param typ_frame: frame of type definition
 
     >>> int_field = field_for_schema(int, default=9, metadata=dict(required=True))
     >>> int_field.__class__
@@ -588,22 +645,24 @@ def field_for_schema(
             subtyp = type(default)
         else:
             subtyp = Any
-        return field_for_schema(subtyp, default, metadata, base_schema)
+        return field_for_schema(subtyp, default, metadata, base_schema, typ_frame)
 
     # Generic types
-    generic_field = _field_for_generic_type(typ, base_schema, **metadata)
+    generic_field = _field_for_generic_type(typ, base_schema, typ_frame, **metadata)
     if generic_field:
         return generic_field
 
-    # typing.NewType returns a function with a __supertype__ attribute
+    # typing.NewType returns a function (in python <= 3.9) or a class (python >= 3.10) with a
+    # __supertype__ attribute
     newtype_supertype = getattr(typ, "__supertype__", None)
-    if newtype_supertype and inspect.isfunction(typ):
+    if typing_inspect.is_new_type(typ) and newtype_supertype is not None:
         return _field_by_supertype(
             typ=typ,
             default=default,
             newtype_supertype=newtype_supertype,
             metadata=metadata,
             base_schema=base_schema,
+            typ_frame=typ_frame,
         )
 
     # enumerations
@@ -613,12 +672,15 @@ def field_for_schema(
         return marshmallow_enum.EnumField(typ, **metadata)
 
     # Nested marshmallow dataclass
+    # it would be just a class name instead of actual schema util the schema is not ready yet
     nested_schema = getattr(typ, "Schema", None)
 
     # Nested dataclasses
     forward_reference = getattr(typ, "__forward_arg__", None)
     nested = (
-        nested_schema or forward_reference or _internal_class_schema(typ, base_schema)
+        nested_schema
+        or forward_reference
+        or _internal_class_schema(typ, base_schema, typ_frame)
     )
 
     return marshmallow.fields.Nested(nested, **metadata)
diff --git a/marshmallow_dataclass/lazy_class_attribute.py b/marshmallow_dataclass/lazy_class_attribute.py
new file mode 100644
index 0000000..f930cc0
--- /dev/null
+++ b/marshmallow_dataclass/lazy_class_attribute.py
@@ -0,0 +1,42 @@
+from typing import Any, Callable
+
+
+__all__ = ("lazy_class_attribute",)
+
+
+class LazyClassAttribute:
+    """Descriptor decorator implementing a class-level, read-only
+    property, which caches its results on the class(es) on which it
+    operates.
+    """
+
+    __slots__ = ("func", "name", "called", "forward_value")
+
+    def __init__(
+        self, func: Callable[..., Any], name: str = None, forward_value: Any = None
+    ):
+        self.func = func
+        self.name = name
+        self.called = False
+        self.forward_value = forward_value
+
+    def __get__(self, instance, cls=None):
+        if not cls:
+            cls = type(instance)
+
+        # avoid recursion
+        if self.called:
+            return self.forward_value
+
+        self.called = True
+
+        setattr(cls, self.name, self.func())
+
+        # "getattr" is used to handle bounded methods
+        return getattr(cls, self.name)
+
+    def __set_name__(self, owner, name):
+        self.name = self.name or name
+
+
+lazy_class_attribute = LazyClassAttribute
diff --git a/setup.py b/setup.py
index 01f2d78..880f7d0 100644
--- a/setup.py
+++ b/setup.py
@@ -18,8 +18,8 @@ CLASSIFIERS = [
 EXTRAS_REQUIRE = {
     "enum": ["marshmallow-enum"],
     "union": ["typeguard"],
-    ':python_version == "3.6"': ["dataclasses", "types-dataclasses"],
-    "lint": ["pre-commit~=1.18"],
+    "lint": ["pre-commit~=2.17"],
+    ':python_version == "3.6"': ["dataclasses", "types-dataclasses<0.6.4"],
     "docs": ["sphinx"],
     "tests": [
         "pytest>=5.4",
@@ -29,7 +29,7 @@ EXTRAS_REQUIRE = {
         # `Literal` was introduced in:
         # - Python 3.8 (https://www.python.org/dev/peps/pep-0586)
         # - typing-extensions 3.7.2 (https://github.com/python/typing/pull/591)
-        "typing-extensions~=3.7.2; python_version < '3.8'",
+        "typing-extensions>=3.7.2; python_version < '3.8'",
     ],
 }
 EXTRAS_REQUIRE["dev"] = (


Test patch:
diff --git a/tests/test_class_schema.py b/tests/test_class_schema.py
index 02f3ba3..69f358b 100644
--- a/tests/test_class_schema.py
+++ b/tests/test_class_schema.py
@@ -1,3 +1,4 @@
+import inspect
 import typing
 import unittest
 from typing import Any, TYPE_CHECKING
@@ -38,12 +39,60 @@ class TestClassSchema(unittest.TestCase):
         complex_set = {
             class_schema(ComplexNested),
             class_schema(ComplexNested, base_schema=None),
+            class_schema(ComplexNested, clazz_frame=None),
             class_schema(ComplexNested, None),
+            class_schema(ComplexNested, None, None),
         }
         simple_set = {
             class_schema(Simple),
             class_schema(Simple, base_schema=None),
+            class_schema(Simple, clazz_frame=None),
             class_schema(Simple, None),
+            class_schema(Simple, None, None),
+        }
+        self.assertEqual(len(complex_set), 1)
+        self.assertEqual(len(simple_set), 1)
+
+    def test_nested_schema_with_passed_frame(self):
+        @dataclasses.dataclass
+        class Simple:
+            one: str = dataclasses.field()
+            two: str = dataclasses.field()
+
+        @dataclasses.dataclass
+        class ComplexNested:
+            three: int = dataclasses.field()
+            four: Simple = dataclasses.field()
+
+        frame = inspect.stack()[0][0]
+
+        self.assertIs(
+            class_schema(ComplexNested, clazz_frame=frame),
+            class_schema(ComplexNested, clazz_frame=frame),
+        )
+        self.assertIs(
+            class_schema(Simple, clazz_frame=frame),
+            class_schema(Simple, clazz_frame=frame),
+        )
+        self.assertIs(
+            class_schema(Simple, clazz_frame=frame),
+            class_schema(ComplexNested, clazz_frame=frame)
+            ._declared_fields["four"]
+            .nested,
+        )
+
+        complex_set = {
+            class_schema(ComplexNested, clazz_frame=frame),
+            class_schema(ComplexNested, base_schema=None, clazz_frame=frame),
+            class_schema(ComplexNested, None, clazz_frame=frame),
+            class_schema(ComplexNested, None, frame),
+        }
+        simple_set = {
+            class_schema(Simple, clazz_frame=frame),
+            class_schema(Simple, base_schema=None, clazz_frame=frame),
+            class_schema(Simple, None, clazz_frame=frame),
+            class_schema(Simple, clazz_frame=frame),
+            class_schema(Simple, None, frame),
         }
         self.assertEqual(len(complex_set), 1)
         self.assertEqual(len(simple_set), 1)
diff --git a/tests/test_forward_references.py b/tests/test_forward_references.py
new file mode 100644
index 0000000..fc05b12
--- /dev/null
+++ b/tests/test_forward_references.py
@@ -0,0 +1,135 @@
+import unittest
+from typing import List, Optional
+
+from marshmallow_dataclass import dataclass
+
+
+@dataclass
+class GlobalA:
+    b: "GlobalB"
+
+
+@dataclass
+class GlobalB:
+    pass
+
+
+@dataclass
+class GlobalSelfRecursion:
+    related: "List[GlobalSelfRecursion]"
+
+
+@dataclass
+class GlobalRecursion:
+    related: "List[GlobalRecursion]"
+
+
+@dataclass
+class GlobalCyclicA:
+    b: "Optional[GlobalCyclicB]"
+
+
+@dataclass
+class GlobalCyclicB:
+    a: "Optional[GlobalCyclicA]"
+
+
+class TestForwardReferences(unittest.TestCase):
+    def test_late_evaluated_types(self):
+        @dataclass
+        class MyData:
+            value: int
+
+        self.assertEqual(MyData(1), MyData.Schema().load(dict(value=1)))
+
+    def test_forward_references_for_basic_types(self):
+        @dataclass
+        class Person:
+            name: "str"
+            age: "int"
+
+        self.assertEqual(
+            Person("Jon", 25), Person.Schema().load(dict(name="Jon", age=25))
+        )
+
+    def test_global_forward_references(self):
+        self.assertEqual(GlobalA(GlobalB()), GlobalA.Schema().load(dict(b=dict())))
+
+    def test_global_self_recursive_type(self):
+        self.assertEqual(
+            GlobalSelfRecursion([GlobalSelfRecursion([])]),
+            GlobalSelfRecursion.Schema().load(dict(related=[dict(related=[])])),
+        )
+
+    def test_global_recursive_type(self):
+        self.assertEqual(
+            GlobalRecursion([GlobalRecursion([])]),
+            GlobalRecursion.Schema().load(dict(related=[dict(related=[])])),
+        )
+
+    def test_global_circular_reference(self):
+        self.assertEqual(
+            GlobalCyclicA(GlobalCyclicB(GlobalCyclicA(None))),
+            GlobalCyclicA.Schema().load(dict(b=dict(a=dict(b=None)))),
+        )
+
+    def test_local_self_recursive_type(self):
+        @dataclass
+        class LocalSelfRecursion:
+            related: "List[LocalSelfRecursion]"
+
+        self.assertEqual(
+            LocalSelfRecursion([LocalSelfRecursion([])]),
+            LocalSelfRecursion.Schema().load(dict(related=[dict(related=[])])),
+        )
+
+    def test_local_recursive_type(self):
+        @dataclass
+        class LocalRecursion:
+            related: "List[LocalRecursion]"
+
+        self.assertEqual(
+            LocalRecursion([LocalRecursion([])]),
+            LocalRecursion.Schema().load(dict(related=[dict(related=[])])),
+        )
+
+    def test_local_forward_references(self):
+        @dataclass
+        class LocalA:
+            b: "LocalB"
+
+        @dataclass
+        class LocalB:
+            pass
+
+        self.assertEqual(LocalA(LocalB()), LocalA.Schema().load(dict(b=dict())))
+
+    def test_name_collisions(self):
+        """
+        This is one example about why you should not make local schemas
+        :return:
+        """
+
+        def make_another_a():
+            @dataclass
+            class A:
+                d: int
+
+            A.Schema()
+
+        make_another_a()
+
+        @dataclass
+        class A:
+            c: int
+
+        A.Schema()
+
+        @dataclass
+        class B:
+            a: "A"
+
+        # with self.assertRaises(marshmallow.exceptions.ValidationError):
+        B.Schema().load(dict(a=dict(c=1)))
+        # marshmallow.exceptions.ValidationError:
+        # {'a': {'d': ['Missing data for required field.'], 'c': ['Unknown field.']}}