Repository: strawberry-graphql/strawberry

Issue:
Support forward references
See: https://www.python.org/dev/peps/pep-0563/#forward-references

Right now the following code would break:

```python
from __future__ import annotations

import strawberry
import typing

@strawberry.type
class User:
    name: str
    friend: typing.Optional[User]
```

This is the error we get:

```
  File "/Users/patrickarminio/Documents/personal/temp/stra/app.py", line 7, in <module>
    from schema import schema
  File "/Users/patrickarminio/Documents/personal/temp/stra/schema.py", line 10, in <module>
    @strawberry.type
  File "/Users/patrickarminio/.virtualenvs/stra-so-aNvo2/lib/python3.7/site-packages/strawberry/type.py", line 60, in type
    return wrap()
  File "/Users/patrickarminio/.virtualenvs/stra-so-aNvo2/lib/python3.7/site-packages/strawberry/type.py", line 55, in wrap
    cls._fields = _get_fields(cls)
  File "/Users/patrickarminio/.virtualenvs/stra-so-aNvo2/lib/python3.7/site-packages/strawberry/type.py", line 27, in _get_fields
    cls_annotations = typing.get_type_hints(cls)
  File "/Users/patrickarminio/.pyenv/versions/3.7.0/lib/python3.7/typing.py", line 973, in get_type_hints
    value = _eval_type(value, base_globals, localns)
  File "/Users/patrickarminio/.pyenv/versions/3.7.0/lib/python3.7/typing.py", line 260, in _eval_type
    return t._evaluate(globalns, localns)
  File "/Users/patrickarminio/.pyenv/versions/3.7.0/lib/python3.7/typing.py", line 464, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
NameError: name 'User' is not defined
```


Discussion:

Resolving patch:
diff --git a/strawberry/type.py b/strawberry/type.py
--- a/strawberry/type.py
+++ b/strawberry/type.py
@@ -5,7 +5,7 @@
 from graphql.utilities.schema_printer import print_type
 
 from .constants import IS_STRAWBERRY_FIELD
-from .type_converter import get_graphql_type_for_annotation
+from .type_converter import REGISTRY, get_graphql_type_for_annotation
 
 
 def _get_resolver(cls, field_name):
@@ -23,37 +23,39 @@ def _resolver(obj, info):
     return _resolver
 
 
-def _get_fields(cls):
-    cls_annotations = typing.get_type_hints(cls)
-
-    fields = {
-        key: GraphQLField(
-            get_graphql_type_for_annotation(value, field_name=key),
-            resolve=_get_resolver(cls, key),
-        )
-        for key, value in cls_annotations.items()
-    }
-
-    fields.update(
-        {
-            key: value.field
-            for key, value in cls.__dict__.items()
-            if getattr(value, IS_STRAWBERRY_FIELD, False)
-        }
-    )
-
-    return fields
-
-
 def type(cls):
     def wrap():
+        name = cls.__name__
+        REGISTRY[name] = cls
+
         def repr_(self):
             return print_type(self.field)
 
         setattr(cls, "__repr__", repr_)
 
-        cls._fields = _get_fields(cls)
-        cls.field = GraphQLObjectType(name=cls.__name__, fields=cls._fields)
+        annotations = typing.get_type_hints(cls, None, REGISTRY)
+
+        def _get_fields():
+
+            fields = {
+                key: GraphQLField(
+                    get_graphql_type_for_annotation(value, key),
+                    resolve=_get_resolver(cls, key),
+                )
+                for key, value in annotations.items()
+            }
+
+            fields.update(
+                {
+                    key: value.field
+                    for key, value in cls.__dict__.items()
+                    if getattr(value, IS_STRAWBERRY_FIELD, False)
+                }
+            )
+
+            return fields
+
+        cls.field = GraphQLObjectType(name, lambda: _get_fields())
 
         return dataclass(cls, repr=False)
 
diff --git a/strawberry/type_converter.py b/strawberry/type_converter.py
--- a/strawberry/type_converter.py
+++ b/strawberry/type_converter.py
@@ -12,7 +12,7 @@
 from .scalars import ID
 
 
-TYPE_MAP = {
+REGISTRY = {
     str: GraphQLString,
     int: GraphQLInt,
     float: GraphQLFloat,
@@ -27,11 +27,9 @@
 def get_graphql_type_for_annotation(
     annotation, field_name: str, force_optional: bool = False
 ):
-    # TODO: nice error
-
-    is_optional = False
-
     # TODO: this might lead to issues with types that have a field value
+    is_optional = force_optional
+
     if hasattr(annotation, "field"):
         graphql_type = annotation.field
     else:
@@ -49,7 +47,7 @@ def get_graphql_type_for_annotation(
         # populated but the type is not an Union, like in the above case with Lists
         if hasattr(annotation, "__args__"):
             types = annotation.__args__
-            non_none_types = [x for x in types if x != type(None)]  # noqa:E721
+            non_none_types = [x for x in types if x != None.__class__]  # noqa:E721
 
             # optionals are represented as Union[type, None]
             if len(non_none_types) == 1:
@@ -58,7 +56,7 @@ def get_graphql_type_for_annotation(
                     non_none_types[0], field_name, force_optional=True
                 )
             else:
-                is_optional = type(None) in types
+                is_optional = None.__class__ in types
 
                 # TODO: union types don't work with scalar types
                 # so we want to return a nice error
@@ -68,12 +66,12 @@ def get_graphql_type_for_annotation(
                     field_name, [type.field for type in types]
                 )
         else:
-            graphql_type = TYPE_MAP.get(annotation)
+            graphql_type = REGISTRY.get(annotation)
 
     if not graphql_type:
         raise ValueError(f"Unable to get GraphQL type for {annotation}")
 
-    if is_optional or force_optional:
+    if is_optional:
         return graphql_type
 
     return GraphQLNonNull(graphql_type)


Test patch:
diff --git a/tests/test_declaring_types.py b/tests/test_declaring_types.py
--- a/tests/test_declaring_types.py
+++ b/tests/test_declaring_types.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import textwrap
 from typing import Optional
 
@@ -29,6 +31,20 @@ class MyType:
     )
 
 
+def test_recursive_type():
+    @strawberry.type
+    class MyType:
+        s: MyType
+
+    expected_representation = """
+    type MyType {
+      s: MyType!
+    }
+    """
+
+    assert repr(MyType("a")) == textwrap.dedent(expected_representation).strip()
+
+
 def test_optional():
     @strawberry.type
     class MyType: