Repository: strawberry-graphql/strawberry

Issue:
Unexpected type 'ForwardRef('ProductCategory')'. Postponed annotations bug
I have a model `Product` and a model `Wholesaler` with cross-referenced themselves in fields definitions. My `Product` model code:

```python
from __future__ import annotations

import typing

import strawberry

if typing.TYPE_CHECKING:
    from src.view.amount import Amount
    from src.view.wholesaler.single import Wholesaler
    from src.view.product.category import ProductCategory


@strawberry.type
class Product:
    id: strawberry.ID
    name: str
    category: "ProductCategory"
    amount: "Amount"
    retail_price: int
    wholesaler: "Wholesaler"
    wholesaler_price: int
    barcode: str
    comment: str
```
My `Wholesaler` model (another file)
```python
from __future__ import annotations

import typing

import strawberry

if typing.TYPE_CHECKING:
    from src.view.product.single import Product
    from src.view.product.sorting import ProductFilters


@strawberry.type
class Wholesaler:
    id: strawberry.ID
    name: str
    phone: str
    email: typing.Optional[str]
    comment: typing.Optional[str]

    @strawberry.field
    def products(self, filters: "ProductFilters") -> "Product":
        pass
```


And I get a error:
```shell
Traceback (most recent call last):
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/graphql/type/definition.py", line 735, in fields
    fields = resolve_thunk(self._fields)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/graphql/type/definition.py", line 264, in resolve_thunk
    return thunk() if callable(thunk) else thunk
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/strawberry/schema/schema_converter.py", line 278, in get_graphql_fields
    graphql_fields[field_name] = self.from_field(field)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/strawberry/schema/schema_converter.py", line 135, in from_field
    field_type = self.from_non_optional(field.type)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/strawberry/schema/schema_converter.py", line 259, in from_non_optional
    of_type = self.from_type(type_)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/strawberry/schema/schema_converter.py", line 464, in from_type
    raise TypeError(f"Unexpected type '{type_}'")
TypeError: Unexpected type 'ForwardRef('ProductCategory')'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/runpy.py", line 193, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/Users/an/Desktop/nto/grocery-server/src/__main__.py", line 5, in <module>
    uvicorn.run(
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/uvicorn/main.py", line 447, in run
    server.run()
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/uvicorn/server.py", line 68, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "uvloop/loop.pyx", line 1501, in uvloop.loop.Loop.run_until_complete
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/uvicorn/server.py", line 76, in serve
    config.load()
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/uvicorn/config.py", line 448, in load
    self.loaded_app = import_from_string(self.app)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/uvicorn/importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/an/Desktop/nto/grocery-server/src/app/main.py", line 3, in <module>
    from src.app.graphql import graphql_app
  File "/Users/an/Desktop/nto/grocery-server/src/app/graphql.py", line 8, in <module>
    schema = strawberry.Schema(Query, Mutation)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/strawberry/schema/schema.py", line 84, in __init__
    self._schema = GraphQLSchema(
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/graphql/type/schema.py", line 208, in __init__
    collect_referenced_types(query)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/graphql/type/schema.py", line 420, in collect_referenced_types
    collect_referenced_types(interface_type)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/graphql/type/schema.py", line 423, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/graphql/type/schema.py", line 422, in collect_referenced_types
    for field in named_type.fields.values():
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/an/Desktop/nto/grocery-server/.venv/lib/python3.8/site-packages/graphql/type/definition.py", line 737, in fields
    raise TypeError(f"{self.name} fields cannot be resolved. {error}")
TypeError: Product fields cannot be resolved. Unexpected type 'ForwardRef('ProductCategory')'
```
***
Suppose strawberry can't resolve postponed annotations when the model defined in another module. I also try removing quotes in the typing, but just get the same error with `None` instead `'ForwardRef('ProductCategory')'`. I can't remove `if typing.TYPE_CHECKING:` because these models reference themselves so I do not see a way to solve the problem
***
I'm using `0.93.4` version of strawberry


Discussion:
Hi @deknowny! Can you try with LazyType? It allows to pass the module name too: https://github.com/strawberry-graphql/strawberry/blob/main/tests/types/test_lazy_types.py#L20
@patrick91 Can you please explain how correctly use in such case? Like this?
```python
category: strawberry.LazyType["ProductCategory", "src.view.product.category"]
```
Unfortunately
```shell
Unexpected type '<enum 'ProductCategory'>'
```
My `ProductCategory`:
```python
import enum

import strawberry


@strawberry.enum
class ProductCategory(enum.Enum):
    BREAD = "BREAD"
    DRINKS = "DRINKS"
    FRUIT = "FRUIT"
    MEAT = "MEAT"
    MILK = "MILK"
```
@deknowny that should work 🤔 can you replicate with a couple of files? maybe the module path is wrong?
@patrick91 I guess if it wasn't a correct module path it couldn't be resolved to an enum at all.

Sorry, what do you mean in "replicate with a couple of files"?

***
Now I specially try to pass invalid path and I got `TypeError: Product fields cannot be resolved. 'ProductCategory'` or `TypeError: Product fields cannot be resolved. No module named 'src.view.product.categor'` and it is not such error
By the way, `amount: strawberry.LazyType["Amount", "src.view.amount"]` works correctly. Seems like the bug with enums only
@patrick91 I traced it with breakpoint and see this:
In line where lazy type resolved: https://github.com/strawberry-graphql/strawberry/blob/a08b8beddcb7564c4b55fadbcb7a4caba8fbbc70/strawberry/schema/schema_converter.py#L458
This condition is `False`: https://github.com/strawberry-graphql/strawberry/blob/a08b8beddcb7564c4b55fadbcb7a4caba8fbbc70/strawberry/schema/schema_converter.py#L441
@deknowny I just did a test locally and seems to be working fine, but maybe you have something different than me 🤔 

what is `type_` in your case? :) PS. we can chat on discord if it is easier :D
@patrick91 `LazyType(type_name='ProductCategory', module='src.view.product.category', package=None)` in first call and `<enum 'ProductCategory'>` in the second

Jle4enka#3075 is my discord

Resolving patch:
diff --git a/strawberry/schema/compat.py b/strawberry/schema/compat.py
--- a/strawberry/schema/compat.py
+++ b/strawberry/schema/compat.py
@@ -45,6 +45,11 @@ def is_object_type(type_: Union[StrawberryType, type]) -> TypeGuard[type]:
     return hasattr(type_, "_type_definition")
 
 
+def is_enum(type_: Union[StrawberryType, type]) -> TypeGuard[type]:
+    # isinstance(type_, StrawberryEnumType)  # noqa: E800
+    return hasattr(type_, "_enum_definition")
+
+
 def is_generic(type_: Union[StrawberryType, type]) -> bool:
     if hasattr(type_, "_type_definition"):
 
diff --git a/strawberry/schema/schema_converter.py b/strawberry/schema/schema_converter.py
--- a/strawberry/schema/schema_converter.py
+++ b/strawberry/schema/schema_converter.py
@@ -450,6 +450,9 @@ def from_type(self, type_: Union[StrawberryType, type]) -> GraphQLNullableType:
         elif compat.is_object_type(type_):  # TODO: Replace with StrawberryObject
             type_definition: TypeDefinition = type_._type_definition  # type: ignore
             return self.from_object(type_definition)
+        elif compat.is_enum(type_):  # TODO: Replace with StrawberryEnum
+            enum_definition: EnumDefinition = type_._enum_definition  # type: ignore
+            return self.from_enum(enum_definition)
         elif isinstance(type_, TypeDefinition):  # TODO: Replace with StrawberryObject
             return self.from_object(type_)
         elif isinstance(type_, StrawberryUnion):


Test patch:
diff --git a/tests/test_cyclic/__init__.py b/tests/schema/test_lazy_types/__init__.py
similarity index 100%
rename from tests/test_cyclic/__init__.py
rename to tests/schema/test_lazy_types/__init__.py
diff --git a/tests/test_cyclic/test_cyclic.py b/tests/schema/test_lazy_types/test_cyclic.py
similarity index 100%
rename from tests/test_cyclic/test_cyclic.py
rename to tests/schema/test_lazy_types/test_cyclic.py
diff --git a/tests/schema/test_lazy_types/test_lazy_enums.py b/tests/schema/test_lazy_types/test_lazy_enums.py
new file mode 100644
--- /dev/null
+++ b/tests/schema/test_lazy_types/test_lazy_enums.py
@@ -0,0 +1,37 @@
+import enum
+import textwrap
+from typing import TYPE_CHECKING
+
+import strawberry
+from strawberry.printer import print_schema
+
+
+if TYPE_CHECKING:
+    import tests
+
+
+@strawberry.enum
+class LazyEnum(enum.Enum):
+    BREAD = "BREAD"
+
+
+def test_lazy_enum():
+    @strawberry.type
+    class Query:
+        a: strawberry.LazyType[
+            "LazyEnum", "tests.schema.test_lazy_types.test_lazy_enums"
+        ]
+
+    expected = """
+    enum LazyEnum {
+      BREAD
+    }
+
+    type Query {
+      a: LazyEnum!
+    }
+    """
+
+    schema = strawberry.Schema(Query)
+
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
diff --git a/tests/test_cyclic/type_a.py b/tests/schema/test_lazy_types/type_a.py
similarity index 71%
rename from tests/test_cyclic/type_a.py
rename to tests/schema/test_lazy_types/type_a.py
--- a/tests/test_cyclic/type_a.py
+++ b/tests/schema/test_lazy_types/type_a.py
@@ -4,7 +4,7 @@
 
 
 if typing.TYPE_CHECKING:
-    import tests
+    import tests.schema.test_lazy_types
 
     from .type_b import TypeB
 
@@ -12,7 +12,7 @@
 @strawberry.type
 class TypeA:
     list_of_b: typing.Optional[
-        typing.List[strawberry.LazyType["TypeB", "tests.test_cyclic.type_b"]]
+        typing.List[strawberry.LazyType["TypeB", "tests.schema.test_lazy_types.type_b"]]
     ] = None
 
     @strawberry.field()
diff --git a/tests/test_cyclic/type_b.py b/tests/schema/test_lazy_types/type_b.py
similarity index 67%
rename from tests/test_cyclic/type_b.py
rename to tests/schema/test_lazy_types/type_b.py
--- a/tests/test_cyclic/type_b.py
+++ b/tests/schema/test_lazy_types/type_b.py
@@ -12,7 +12,9 @@
 @strawberry.type
 class TypeB:
     @strawberry.field()
-    def type_a(self) -> strawberry.LazyType["TypeA", "tests.test_cyclic.type_a"]:
+    def type_a(
+        self,
+    ) -> strawberry.LazyType["TypeA", "tests.schema.test_lazy_types.type_a"]:
         from .type_a import TypeA
 
         return TypeA()
diff --git a/tests/types/test_lazy_types.py b/tests/types/test_lazy_types.py
--- a/tests/types/test_lazy_types.py
+++ b/tests/types/test_lazy_types.py
@@ -1,4 +1,5 @@
 # type: ignore
+import enum
 from typing import Generic, TypeVar
 
 import strawberry
@@ -15,6 +16,11 @@ class LaziestType:
     something: bool
 
 
+@strawberry.enum
+class LazyEnum(enum.Enum):
+    BREAD = "BREAD"
+
+
 def test_lazy_type():
     # Module path is short and relative because of the way pytest runs the file
     LazierType = LazyType["LaziestType", "test_lazy_types"]
@@ -30,6 +36,21 @@ def test_lazy_type():
     assert resolved.resolve_type() is LaziestType
 
 
+def test_lazy_type_enum():
+    # Module path is short and relative because of the way pytest runs the file
+    LazierType = LazyType["LazyEnum", "test_lazy_types"]
+
+    annotation = StrawberryAnnotation(LazierType)
+    resolved = annotation.resolve()
+
+    # Currently StrawberryAnnotation(LazyType).resolve() returns the unresolved
+    # LazyType. We may want to find a way to directly return the referenced object
+    # without a second resolving step.
+    assert isinstance(resolved, LazyType)
+    assert resolved is LazierType
+    assert resolved.resolve_type() is LazyEnum
+
+
 def test_lazy_type_argument():
     # Module path is short and relative because of the way pytest runs the file
     LazierType = LazyType["LaziestType", "test_lazy_types"]