Repository: strawberry-graphql/strawberry

Issue:
`Annotated` + `strawberry.lazy` + deferred annotations results in `UnresolvedFieldTypeError: Could not resolve the type of ...` errors
<!-- Provide a general summary of the bug in the title above. -->

<!--- This template is entirely optional and can be removed, but is here to help both you and us. -->
<!--- Anything on lines wrapped in comments like these will not show up in the final text. -->

## Describe the Bug

When using `Annotated[..., strawberry.lazy(...)]`, one seems to get spurious errors in a file with `from __future__ import annotations`:

```python
from __future__ import annotations
import strawberry
from typing import Annotated

@strawberry.type
class Query:
    a: Annotated["datetime", strawberry.lazy("datetime")]

schema = strawberry.Schema(Query)
```

Running that like `python example.py` with strawberry 0.277.3 gives errors like:

```
Traceback (most recent call last):
  File "/tmp/tmp.VSRhsBYbmq/example.py", line 9, in <module>
    schema = strawberry.Schema(Query)
  File "/tmp/tmp.VSRhsBYbmq/.venv/lib/python3.10/site-packages/strawberry/schema/schema.py", line 162, in __init__
    raise error.__cause__ from None
  File "/tmp/tmp.VSRhsBYbmq/.venv/lib/python3.10/site-packages/graphql/type/definition.py", line 808, in fields
    fields = resolve_thunk(self._fields)
  File "/tmp/tmp.VSRhsBYbmq/.venv/lib/python3.10/site-packages/graphql/type/definition.py", line 300, in resolve_thunk
    return thunk() if callable(thunk) else thunk
  File "/tmp/tmp.VSRhsBYbmq/.venv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 525, in <lambda>
    fields=lambda: self.get_graphql_fields(object_type),
  File "/tmp/tmp.VSRhsBYbmq/.venv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 382, in get_graphql_fields
    return _get_thunk_mapping(
  File "/tmp/tmp.VSRhsBYbmq/.venv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 132, in _get_thunk_mapping
    raise UnresolvedFieldTypeError(type_definition, field)
strawberry.exceptions.unresolved_field_type.UnresolvedFieldTypeError: Could not resolve the type of 'a'. Check that the class is accessible from the global module scope.
```

Standalone reproducer (run like `bash ./script.sh`):

```shell
# Setup:
cd $(mktemp -d)

python -m venv .venv

.venv/bin/python -m pip install graphql-core==3.2.3 python-dateutil==2.9.0.post0 six==1.16.0 typing_extensions==4.11.0 strawberry-graphql==0.227.3

cat > example.py <<EOF
from __future__ import annotations
import strawberry
from typing import Annotated

@strawberry.type
class Query:
    a: Annotated["datetime", strawberry.lazy("datetime")]

schema = strawberry.Schema(Query)
EOF

# BUG: strawberry.exceptions.unresolved_field_type.UnresolvedFieldTypeError: Could not resolve the type of 'a'. Check that the class is accessible from the global module scope.
.venv/bin/python example.py
```

## System Information

 - Operating system: macOS
 - Strawberry version (if applicable): 0.227.3

## Additional Context

Thank you for strawberry! 🍓 

This seems a bit similar to:

- https://github.com/strawberry-graphql/strawberry/issues/2607
- https://github.com/strawberry-graphql/strawberry/issues/2855

<!-- POLAR PLEDGE BADGE START -->
## Upvote & Fund

- We're using [Polar.sh](https://polar.sh/strawberry-graphql) so you can upvote and help fund this issue.
- We receive the funding once the issue is completed & confirmed by you.
- Thank you in advance for helping prioritize & fund our backlog.

<a href="https://polar.sh/strawberry-graphql/strawberry/issues/3491">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/3491/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/3491/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
Ah, the same applies to "manually" deferred annotations (i.e. an explicit string):

```python
import strawberry
from typing import Annotated

@strawberry.type
class Query:
    a: """Annotated["datetime", strawberry.lazy("datetime")]"""

schema = strawberry.Schema(Query)
```
I think we'll fix this next week with @bellini666 😊

Resolving patch:
diff --git a/strawberry/utils/typing.py b/strawberry/utils/typing.py
--- a/strawberry/utils/typing.py
+++ b/strawberry/utils/typing.py
@@ -302,7 +302,7 @@ def _get_namespace_from_ast(
         # here to resolve lazy types by execing the annotated args, resolving the
         # type directly and then adding it to extra namespace, so that _eval_type
         # can properly resolve it later
-        type_name = args[0].strip()
+        type_name = args[0].strip(" '\"\n")
         for arg in args[1:]:
             evaled_arg = eval(arg, globalns, localns)  # noqa: PGH001, S307
             if isinstance(evaled_arg, StrawberryLazyReference):


Test patch:
diff --git a/tests/schema/test_lazy/test_lazy_generic.py b/tests/schema/test_lazy/test_lazy_generic.py
--- a/tests/schema/test_lazy/test_lazy_generic.py
+++ b/tests/schema/test_lazy/test_lazy_generic.py
@@ -37,7 +37,6 @@ class Query:
 
 
 def test_no_generic_type_duplication_with_lazy():
-    from tests.schema.test_lazy.type_a import TypeB_abs, TypeB_rel
     from tests.schema.test_lazy.type_b import TypeB
 
     @strawberry.type
@@ -47,8 +46,10 @@ class Edge(Generic[T]):
     @strawberry.type
     class Query:
         users: Edge[TypeB]
-        relatively_lazy_users: Edge[TypeB_rel]
-        absolutely_lazy_users: Edge[TypeB_abs]
+        relatively_lazy_users: Edge[Annotated["TypeB", strawberry.lazy(".type_b")]]
+        absolutely_lazy_users: Edge[
+            Annotated["TypeB", strawberry.lazy("tests.schema.test_lazy.type_b")]
+        ]
 
     schema = strawberry.Schema(query=Query)
 
diff --git a/tests/schema/test_lazy/type_a.py b/tests/schema/test_lazy/type_a.py
--- a/tests/schema/test_lazy/type_a.py
+++ b/tests/schema/test_lazy/type_a.py
@@ -6,19 +6,15 @@
 if TYPE_CHECKING:
     from .type_b import TypeB
 
-    TypeB_rel = TypeB
-    TypeB_abs = TypeB
-else:
-    TypeB_rel = Annotated["TypeB", strawberry.lazy(".type_b")]
-    TypeB_abs = Annotated["TypeB", strawberry.lazy("tests.schema.test_lazy.type_b")]
-
 
 @strawberry.type
 class TypeA:
-    list_of_b: Optional[List[TypeB_abs]] = None
+    list_of_b: Optional[
+        List[Annotated["TypeB", strawberry.lazy("tests.schema.test_lazy.type_b")]]
+    ] = None
 
     @strawberry.field
-    def type_b(self) -> TypeB_rel:
+    def type_b(self) -> Annotated["TypeB", strawberry.lazy(".type_b")]:
         from .type_b import TypeB
 
         return TypeB()
diff --git a/tests/utils/test_typing.py b/tests/utils/test_typing.py
--- a/tests/utils/test_typing.py
+++ b/tests/utils/test_typing.py
@@ -1,7 +1,10 @@
+import sys
 import typing
 from typing import ClassVar, ForwardRef, Optional, Union
 from typing_extensions import Annotated
 
+import pytest
+
 import strawberry
 from strawberry.lazy_type import LazyType
 from strawberry.utils.typing import eval_type, get_optional_annotation, is_classvar
@@ -62,6 +65,79 @@ class Foo: ...
         )
         == Annotated[strawberry.auto, "foobar"]
     )
+    assert (
+        eval_type(
+            ForwardRef("Annotated[datetime, strawberry.lazy('datetime')]"),
+            {"strawberry": strawberry, "Annotated": Annotated},
+            None,
+        )
+        == Annotated[
+            LazyType("datetime", "datetime"),
+            strawberry.lazy("datetime"),
+        ]
+    )
+
+
+@pytest.mark.skipif(
+    sys.version_info < (3, 9),
+    reason="python 3.8 resolves Annotated differently",
+)
+def test_eval_type_with_deferred_annotations():
+    assert (
+        eval_type(
+            ForwardRef(
+                "Annotated['Fruit', strawberry.lazy('tests.utils.test_typing')]"
+            ),
+            {"strawberry": strawberry, "Annotated": Annotated},
+            None,
+        )
+        == Annotated[
+            LazyType("Fruit", "tests.utils.test_typing"),
+            strawberry.lazy("tests.utils.test_typing"),
+        ]
+    )
+    assert (
+        eval_type(
+            ForwardRef("Annotated['datetime', strawberry.lazy('datetime')]"),
+            {"strawberry": strawberry, "Annotated": Annotated},
+            None,
+        )
+        == Annotated[
+            LazyType("datetime", "datetime"),
+            strawberry.lazy("datetime"),
+        ]
+    )
+
+
+@pytest.mark.skipif(
+    sys.version_info >= (3, 9),
+    reason="python 3.8 resolves Annotated differently",
+)
+def test_eval_type_with_deferred_annotations_3_8():
+    assert (
+        eval_type(
+            ForwardRef(
+                "Annotated['Fruit', strawberry.lazy('tests.utils.test_typing')]"
+            ),
+            {"strawberry": strawberry, "Annotated": Annotated},
+            None,
+        )
+        == Annotated[
+            ForwardRef("Fruit"),
+            strawberry.lazy("tests.utils.test_typing"),
+        ]
+    )
+    assert (
+        eval_type(
+            ForwardRef("Annotated['datetime', strawberry.lazy('datetime')]"),
+            {"strawberry": strawberry, "Annotated": Annotated},
+            None,
+        )
+        == Annotated[
+            ForwardRef("datetime"),
+            strawberry.lazy("datetime"),
+        ]
+    )
 
 
 def test_is_classvar():