Repository: oxan/djangorestframework-dataclasses

Issue:
Nested dataclasses and using drf-spectacular.
Hey,
I am using this great lib and found problems with drf-spectacular. These are my issues:

If I have nested dataclass like:

```py
@dataclass
class A():
   foo: str

class B():
   a: A
   
# serializer 
class BSerializer(DataclassSerializer):
  class Meta:
    dataclass = B

# the same results with  DataclassSerializer(B)
 
```
Then final docs do not recognise fields of A saying: `object (Dataclass)`

When I try:
```py
# serializer 
class BSerializer(DataclassSerializer):
  a = DataclassSerializer(A)
  class Meta:
    dataclass = B
```
It does not give any effect, I still have  `a: object (Dataclass)`

In order to have it working I need to define for every nested dataclass its own empty serializer like:
```py
class ASerializer(DataclassSerializer):
   class Meta:
     dataclass = A
     
class BSerializer(DataclassSerializer):
  a = ASerializer()
  class Meta:
    dataclass = B
```
What gives a lot of such proxy code (imagine few such dataclasses inside)
Also docs print all the time comment from the DataclassSerializer, so I see :

> object (Dataclass)A DataclassSerializer is just a regular Serializer, except that:A set of default fields are automatically populated.A set of default validators are automatically populated.Default .create() and .update() implementations are provided.The process of automatically determining a set of serializer fields based on the dataclass fields is slightly complex, but you almost certainly don't need to dig into the implementation.If the DataclassSerializer class doesn't generate the set of fields that you need you should either declare the extra/differing fields explicitly on the serializer class, or simply use a Serializer class.

For every field using DataclassSerializer.

In summary using these two things together looks like quite a struggle. 




Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 7e49293..21969f6 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,7 @@
+1.1.1, TBD
+----------
+* Fix usage of PEP 585 generics with forward references (e.g. ``list["str"]``).
+
 1.1.0, 9 January 2021
 ---------------------
 Features & fixes:
diff --git a/rest_framework_dataclasses/field_utils.py b/rest_framework_dataclasses/field_utils.py
index 636c5e5..1a8d37b 100644
--- a/rest_framework_dataclasses/field_utils.py
+++ b/rest_framework_dataclasses/field_utils.py
@@ -30,9 +30,7 @@ def get_dataclass_definition(dataclass_type: type) -> DataclassDefinition:
     Given a dataclass class, returns a dictionary mapping field names to
     `dataclasses.Field` instances describing all fields on the dataclass.
     """
-    # Resolve the typehint from the dataclass fields (which can be stringified, especially with PEP 563 nowadays) to
-    # actual type objects. Based on the discussion in https://stackoverflow.com/a/55938344.
-    types = typing.get_type_hints(dataclass_type)
+    types = typing_utils.get_resolved_type_hints(dataclass_type)
 
     # Disable PyCharm warning here, as it is wrong.
     # noinspection PyDataclass
diff --git a/rest_framework_dataclasses/typing_utils.py b/rest_framework_dataclasses/typing_utils.py
index 163d06d..d2c0bcd 100644
--- a/rest_framework_dataclasses/typing_utils.py
+++ b/rest_framework_dataclasses/typing_utils.py
@@ -16,15 +16,16 @@ Note that there was some promising development in the `typing_inspect` module, b
 development seems to have stalled. Maybe in the future?
 """
 import collections
+import sys
+import types
 import typing
 
 from .types import Final, Literal
 
 # types.UnionType was added in Python 3.10 for new PEP 604 pipe union syntax
 try:
-    from types import UnionType
-    UNION_TYPES = (typing.Union, UnionType)
-except ImportError:
+    UNION_TYPES = (typing.Union, types.UnionType)
+except AttributeError:
     UNION_TYPES = (typing.Union,)
 
 # Wrappers around typing.get_origin() and typing.get_args() for Python 3.7
@@ -45,6 +46,32 @@ except AttributeError:
 #   if it's a type.
 
 
+def get_resolved_type_hints(tp: type) -> typing.Dict[str, type]:
+    """
+    Get the resolved type hints of an object.
+
+    Resolving the type hints means converting any stringified type hint into an actual type object. These can come from
+    either forward references (PEP 484), or postponed evaluation (PEP 563).
+    """
+    # typing.get_type_hints() does the heavy lifting for us, except when using PEP 585 generic types that contain a
+    # stringified type hint (see https://bugs.python.org/issue41370)
+    def _resolve_type(context_type: type, resolve_type: typing.Union[str, type]) -> type:
+        if isinstance(resolve_type, str):
+            globalsns = sys.modules[context_type.__module__].__dict__
+            return globalsns.get(resolve_type, resolve_type)
+        else:
+            return _resolve_type_hint(context_type, resolve_type)
+
+    def _resolve_type_hint(context_type: type, resolve_type: type) -> type:
+        if not hasattr(types, 'GenericAlias') or not isinstance(resolve_type, types.GenericAlias):
+            return resolve_type
+
+        args = tuple(_resolve_type(context_type, arg) for arg in resolve_type.__args__)
+        return types.GenericAlias(resolve_type.__origin__, args)
+
+    return {k: _resolve_type_hint(tp, v) for k, v in typing.get_type_hints(tp).items()}
+
+
 def is_iterable_type(tp: type) -> bool:
     """
     Test if the given type is iterable.


Test patch:
diff --git a/tests/test_issues.py b/tests/test_issues.py
index 0e6aaa9..18afa82 100644
--- a/tests/test_issues.py
+++ b/tests/test_issues.py
@@ -1,5 +1,7 @@
 import dataclasses
+import sys
 import typing
+import unittest
 
 from unittest import TestCase
 
@@ -75,3 +77,14 @@ class IssuesTest(TestCase):
 
         self.assertIsNot(fields.empty, data[0].value)
         self.assertEqual('default', data[0].value)
+
+    # Issue #51: Forward references do not work with PEP 585 generics
+    @unittest.skipIf(sys.version_info < (3, 9, 0), 'Python 3.9 required')
+    def test_forward_reference_list(self):
+        @dataclasses.dataclass
+        class WithForwardReferences:
+            children: list['Simple']
+            nested_children: list[list['Simple']]
+
+        serializer = DataclassSerializer(dataclass=WithForwardReferences)
+        serializer.get_fields()