Repository: strawberry-graphql/strawberry

Issue:
new strawberry.lazy not compatible with from __future__ import annotations
# Description

The new way of lazy referencing via strawberry.lazy is not compatible with 
`from __future__ import annotations`

It would be nice to have compatibility as typing.Self is not available for python <=3.10 (the main reason to have `from __future__ import annotations`)


Discussion:
Can confirm this: https://gitlab.com/ThirVondukr/strawberry-annotations
```py
Traceback (most recent call last):
  File "strawberry-annnotations\app.py", line 16, in <module>
    schema = Schema(
             ^^^^^^^
  File "strawberry-annnotations\.venv\Lib\site-packages\strawberry\schema\schema.py", line 157, in __init__
    raise error.__cause__ from None
  File "strawberry-annnotations\.venv\Lib\site-packages\graphql\type\definition.py", line 808, in fields
    fields = resolve_thunk(self._fields)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "strawberry-annnotations\.venv\Lib\site-packages\graphql\type\definition.py", line 300, in resolve_thunk
    return thunk() if callable(thunk) else thunk
           ^^^^^^^
  File "strawberry-annnotations\.venv\Lib\site-packages\strawberry\schema\schema_converter.py", line 423, in <lambda>
    fields=lambda: self.get_graphql_fields(object_type),
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "strawberry-annnotations\.venv\Lib\site-packages\strawberry\schema\schema_converter.py", line 312, in get_graphql_fields
    return self._get_thunk_mapping(
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "strawberry-annnotations\.venv\Lib\site-packages\strawberry\schema\schema_converter.py", line 302, in _get_thunk_mapping
    raise UnresolvedFieldTypeError(type_definition, field)
strawberry.exceptions.unresolved_field_type.UnresolvedFieldTypeError: Could not resolve the type of 'b'. Check that the class is accessible from the global module scope.
```

Resolving patch:
diff --git a/strawberry/utils/typing.py b/strawberry/utils/typing.py
--- a/strawberry/utils/typing.py
+++ b/strawberry/utils/typing.py
@@ -1,4 +1,5 @@
 import ast
+import re
 import sys
 import typing
 from collections.abc import AsyncGenerator
@@ -221,6 +222,11 @@ def _ast_replace_union_operation(
     return expr
 
 
+_annotated_re = re.compile(
+    r"(Annotated\[)(?P<type>\w*),(?P<args>.*)(\])",
+)
+
+
 def eval_type(
     type_: Any,
     globalns: Optional[Dict] = None,
@@ -249,6 +255,37 @@ def eval_type(
             assert ast_unparse
             type_ = ForwardRef(ast_unparse(parsed))
 
+        # When using forward refs, the whole
+        # Annotated[SomeType, strabwerry.lazy("type.module")] is a forward ref,
+        # and trying to _eval_type on it will fail. Take a different approach
+        # here to resolve lazy types by execing the annotated args and resolving
+        # the type directly.
+        annotated_match = _annotated_re.search(type_.__forward_arg__)
+        if annotated_match:
+            gdict = annotated_match.groupdict()
+            # FIXME: Eval the remaining annotated args to get their real values
+            # We might want to refactor how we import lazy modules to avoid having
+            # to eval the code in here
+            args = eval(f'({gdict["args"]}, )', globalns, localns)  # noqa: PGH001
+            lazy_ref = next(
+                (arg for arg in args if isinstance(arg, StrawberryLazyReference)),
+                None,
+            )
+            if lazy_ref is not None:
+                remaining = [
+                    a for a in args if not isinstance(a, StrawberryLazyReference)
+                ]
+                type_ = lazy_ref.resolve_forward_ref(ForwardRef(gdict["type"]))
+                # If we only had a StrawberryLazyReference, we can return the type
+                # directly. It already did its job!
+                if not remaining:
+                    return type_
+
+                # Otherwise return the type annotated with the remaining annotations
+                return Annotated.__class_getitem__(  # type: ignore
+                    (type_, *remaining),
+                )
+
         return _eval_type(type_, globalns, localns)
 
     origin = get_origin(type_)


Test patch:
diff --git a/tests/a.py b/tests/a.py
new file mode 100644
--- /dev/null
+++ b/tests/a.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing_extensions import Annotated
+
+import strawberry
+
+if TYPE_CHECKING:
+    from tests.b import B
+
+
+@strawberry.type
+class A:
+    id: strawberry.ID
+
+    @strawberry.field
+    async def b(self) -> Annotated[B, strawberry.lazy("tests.b")]:
+        from tests.b import B
+
+        return B(id=self.id)
diff --git a/tests/b.py b/tests/b.py
new file mode 100644
--- /dev/null
+++ b/tests/b.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing_extensions import Annotated
+
+import strawberry
+
+if TYPE_CHECKING:
+    from tests.a import A
+
+
+@strawberry.type
+class B:
+    id: strawberry.ID
+
+    @strawberry.field
+    async def a(self) -> Annotated[A, strawberry.lazy("tests.a"), object()]:
+        from tests.a import A
+
+        return A(id=self.id)
diff --git a/tests/test_forward_references.py b/tests/test_forward_references.py
--- a/tests/test_forward_references.py
+++ b/tests/test_forward_references.py
@@ -11,6 +11,7 @@
 import strawberry
 from strawberry.printer import print_schema
 from strawberry.type import StrawberryList, StrawberryOptional
+from tests.a import A
 
 
 def test_forward_reference():
@@ -41,6 +42,37 @@ class MyType:
     del MyType
 
 
+@pytest.mark.skipif(
+    sys.version_info < (3, 9),
+    reason="Python 3.8 and previous can't properly resolve this.",
+)
+def test_lazy_forward_reference():
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        async def a(self) -> A:
+            return A(id=strawberry.ID("1"))
+
+    expected_representation = """
+    type A {
+      id: ID!
+      b: B!
+    }
+
+    type B {
+      id: ID!
+      a: A!
+    }
+
+    type Query {
+      a: A!
+    }
+    """
+
+    schema = strawberry.Schema(query=Query)
+    assert print_schema(schema) == textwrap.dedent(expected_representation).strip()
+
+
 def test_with_resolver():
     global User