Repository: strawberry-graphql/strawberry

Issue:
Annotated not working properly when using `Info` and future annotations?
This is a bit of a weird one, the following code snippet

```python
from __future__ import annotations
import strawberry
import uuid
from typing_extensions import Annotated
from strawberry.types import Info


@strawberry.type
class Query:
    @strawberry.field
    def get_testing(
        self,
        info: Info[None, None],
        id_: Annotated[uuid.UUID, strawberry.argument(name="id")],
    ) -> str | None:
        return None


schema = strawberry.Schema(query=Query)

print(schema)
```

prints this:


```graphql
type Query {
  getTesting(id_: UUID!): String # ⬅️ see `id_`
}

scalar UUID
```

but if you remove info, it prints correctly:

```graphql
type Query {
  getTesting(id: UUID!): String 
}

scalar UUID
```



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



Discussion:
Moving info after `id_` also works:

```python
from __future__ import annotations
import strawberry
from uuid import UUID
from typing_extensions import Annotated
from strawberry.types import Info


@strawberry.type
class Query:
    @strawberry.field
    def get_testing(
        self,
        id_: Annotated[UUID, strawberry.argument(name="id")],
        info: Info[None, None],
    ) -> str | None:
        return None


schema = strawberry.Schema(query=Query)

print(schema)
```
@patrick91 thanks for the repro! I've found a solution for this problem, will finish and upload the PR tonight 🎉

Resolving patch:
diff --git a/strawberry/annotation.py b/strawberry/annotation.py
--- a/strawberry/annotation.py
+++ b/strawberry/annotation.py
@@ -10,6 +10,7 @@
     Any,
     Dict,
     ForwardRef,
+    List,
     Optional,
     Type,
     TypeVar,
@@ -31,11 +32,7 @@
 )
 from strawberry.types.types import StrawberryObjectDefinition
 from strawberry.unset import UNSET
-from strawberry.utils.typing import (
-    eval_type,
-    is_generic,
-    is_type_var,
-)
+from strawberry.utils.typing import eval_type, is_generic, is_type_var
 
 if TYPE_CHECKING:
     from strawberry.field import StrawberryField
@@ -55,12 +52,19 @@
 
 
 class StrawberryAnnotation:
+    __slots__ = "annotation", "namespace", "__eval_cache__"
+
     def __init__(
-        self, annotation: Union[object, str], *, namespace: Optional[Dict] = None
+        self,
+        annotation: Union[object, str],
+        *,
+        namespace: Optional[Dict[str, Any]] = None,
     ):
         self.annotation = annotation
         self.namespace = namespace
 
+        self.__eval_cache__: Optional[Type[Any]] = None
+
     def __eq__(self, other: object) -> bool:
         if not isinstance(other, StrawberryAnnotation):
             return NotImplemented
@@ -72,7 +76,7 @@ def __hash__(self) -> int:
 
     @staticmethod
     def from_annotation(
-        annotation: object, namespace: Optional[Dict] = None
+        annotation: object, namespace: Optional[Dict[str, Any]] = None
     ) -> Optional[StrawberryAnnotation]:
         if annotation is None:
             return None
@@ -81,18 +85,29 @@ def from_annotation(
             return StrawberryAnnotation(annotation, namespace=namespace)
         return annotation
 
-    def resolve(self) -> Union[StrawberryType, type]:
+    def evaluate(self) -> type:
+        """Return evaluated annotation using `strawberry.util.typing.eval_type`."""
+        evaled_type = self.__eval_cache__
+        if evaled_type:
+            return evaled_type
+
         annotation = self.annotation
         if isinstance(annotation, str):
             annotation = ForwardRef(annotation)
 
-        args = []
-
         evaled_type = eval_type(annotation, self.namespace, None)
 
+        self.__eval_cache__ = evaled_type
+        return evaled_type
+
+    def resolve(self) -> Union[StrawberryType, type]:
+        """Return resolved (transformed) annotation."""
+        evaled_type = cast(Any, self.evaluate())
+
         if is_private(evaled_type):
             return evaled_type
 
+        args: List[Any] = []
         if get_origin(evaled_type) is Annotated:
             evaled_type, *args = get_args(evaled_type)
 
@@ -102,7 +117,7 @@ def resolve(self) -> Union[StrawberryType, type]:
             return evaled_type
 
         if self._is_generic(evaled_type):
-            if any(is_type_var(type_) for type_ in evaled_type.__args__):
+            if any(is_type_var(type_) for type_ in get_args(evaled_type)):
                 return evaled_type
             return self.create_concrete_type(evaled_type)
 
@@ -131,6 +146,8 @@ def set_namespace_from_field(self, field: StrawberryField) -> None:
         module = sys.modules[field.origin.__module__]
         self.namespace = module.__dict__
 
+        self.__eval_cache__ = None  # Invalidate cache to allow re-evaluation
+
     def create_concrete_type(self, evaled_type: type) -> type:
         if has_object_definition(evaled_type):
             return evaled_type.__strawberry_definition__.resolve_generic(evaled_type)
@@ -143,15 +160,16 @@ def create_enum(self, evaled_type: Any) -> EnumDefinition:
             raise NotAStrawberryEnumError(evaled_type)
 
     def create_list(self, evaled_type: Any) -> StrawberryList:
+        item_type, *_ = get_args(evaled_type)
         of_type = StrawberryAnnotation(
-            annotation=evaled_type.__args__[0],
+            annotation=item_type,
             namespace=self.namespace,
         ).resolve()
 
         return StrawberryList(of_type)
 
     def create_optional(self, evaled_type: Any) -> StrawberryOptional:
-        types = evaled_type.__args__
+        types = get_args(evaled_type)
         non_optional_types = tuple(
             filter(
                 lambda x: x is not type(None) and x is not type(UNSET),
@@ -184,7 +202,7 @@ def create_union(self, evaled_type: Type[Any], args: list[Any]) -> StrawberryUni
         if isinstance(evaled_type, StrawberryUnion):
             return evaled_type
 
-        types = evaled_type.__args__
+        types = get_args(evaled_type)
 
         union = StrawberryUnion(
             type_annotations=tuple(StrawberryAnnotation(type_) for type_ in types),
@@ -240,7 +258,7 @@ def _is_optional(cls, annotation: Any) -> bool:
         if not cls._is_union(annotation):
             return False
 
-        types = annotation.__args__
+        types = get_args(annotation)
 
         # A Union to be optional needs to have at least one None type
         return any(x is type(None) for x in types)
@@ -249,7 +267,7 @@ def _is_optional(cls, annotation: Any) -> bool:
     def _is_list(cls, annotation: Any) -> bool:
         """Returns True if annotation is a List"""
 
-        annotation_origin = getattr(annotation, "__origin__", None)
+        annotation_origin = get_origin(annotation)
 
         return (annotation_origin in (list, tuple)) or annotation_origin is abc.Sequence
 
diff --git a/strawberry/arguments.py b/strawberry/arguments.py
--- a/strawberry/arguments.py
+++ b/strawberry/arguments.py
@@ -18,16 +18,12 @@
 from strawberry.annotation import StrawberryAnnotation
 from strawberry.enum import EnumDefinition
 from strawberry.lazy_type import LazyType, StrawberryLazyReference
-from strawberry.type import (
-    StrawberryList,
-    StrawberryOptional,
-    has_object_definition,
-)
+from strawberry.type import StrawberryList, StrawberryOptional, has_object_definition
 
 from .exceptions import MultipleStrawberryArgumentsError, UnsupportedTypeError
 from .scalars import is_scalar
 from .unset import UNSET as _deprecated_UNSET
-from .unset import _deprecated_is_unset  # noqa
+from .unset import _deprecated_is_unset  # noqa # type: ignore
 
 if TYPE_CHECKING:
     from strawberry.custom_scalar import ScalarDefinition, ScalarWrapper
@@ -83,7 +79,6 @@ def __init__(
         self.graphql_name = graphql_name
         self.is_subscription = is_subscription
         self.description = description
-        self._type: Optional[StrawberryType] = None
         self.type_annotation = type_annotation
         self.deprecation_reason = deprecation_reason
         self.directives = directives
@@ -94,48 +89,48 @@ def __init__(
             _deprecated_UNSET if default is inspect.Parameter.empty else default
         )
 
-        if self._annotation_is_annotated(type_annotation):
-            self._parse_annotated()
+        try:
+            evaled_type = type_annotation.evaluate()
+        except NameError:
+            # Evaluation failures can happen when importing types within a TYPE_CHECKING
+            # block or if the type is declared later on in the current module.
+            pass
+        else:
+            if get_origin(evaled_type) is Annotated:
+                first, *rest = get_args(evaled_type)
+
+                # The first argument to Annotated is always the underlying type
+                self.type_annotation = StrawberryAnnotation(first)
+
+                # Find any instances of StrawberryArgumentAnnotation
+                # in the other Annotated args, raising an exception if there
+                # are multiple StrawberryArgumentAnnotations
+                argument_annotation_seen = False
+
+                for arg in rest:
+                    if isinstance(arg, StrawberryArgumentAnnotation):
+                        if argument_annotation_seen:
+                            raise MultipleStrawberryArgumentsError(
+                                argument_name=python_name
+                            )
+
+                        argument_annotation_seen = True
+
+                        self.description = arg.description
+                        self.graphql_name = arg.name
+                        self.deprecation_reason = arg.deprecation_reason
+                        self.directives = arg.directives
+                        self.metadata = arg.metadata
+
+                    if isinstance(arg, StrawberryLazyReference):
+                        self.type_annotation = StrawberryAnnotation(
+                            arg.resolve_forward_ref(first)
+                        )
 
     @property
     def type(self) -> Union[StrawberryType, type]:
         return self.type_annotation.resolve()
 
-    @classmethod
-    def _annotation_is_annotated(cls, annotation: StrawberryAnnotation) -> bool:
-        return get_origin(annotation.annotation) is Annotated
-
-    def _parse_annotated(self):
-        annotated_args = get_args(self.type_annotation.annotation)
-
-        # The first argument to Annotated is always the underlying type
-        self.type_annotation = StrawberryAnnotation(annotated_args[0])
-
-        # Find any instances of StrawberryArgumentAnnotation
-        # in the other Annotated args, raising an exception if there
-        # are multiple StrawberryArgumentAnnotations
-        argument_annotation_seen = False
-
-        for arg in annotated_args[1:]:
-            if isinstance(arg, StrawberryArgumentAnnotation):
-                if argument_annotation_seen:
-                    raise MultipleStrawberryArgumentsError(
-                        argument_name=self.python_name
-                    )
-
-                argument_annotation_seen = True
-
-                self.description = arg.description
-                self.graphql_name = arg.name
-                self.deprecation_reason = arg.deprecation_reason
-                self.directives = arg.directives
-                self.metadata = arg.metadata
-
-            if isinstance(arg, StrawberryLazyReference):
-                self.type_annotation = StrawberryAnnotation(
-                    arg.resolve_forward_ref(annotated_args[0])
-                )
-
 
 def convert_argument(
     value: object,
@@ -257,7 +252,7 @@ def __getattr__(name: str) -> Any:
 __all__ = [  # noqa: F822
     "StrawberryArgument",
     "StrawberryArgumentAnnotation",
-    "UNSET",  # for backwards compatibility
+    "UNSET",  # for backwards compatibility  # type: ignore
     "argument",
-    "is_unset",  # for backwards compatibility
+    "is_unset",  # for backwards compatibility  # type: ignore
 ]
diff --git a/strawberry/types/fields/resolver.py b/strawberry/types/fields/resolver.py
--- a/strawberry/types/fields/resolver.py
+++ b/strawberry/types/fields/resolver.py
@@ -9,14 +9,12 @@
     Any,
     Callable,
     Dict,
-    ForwardRef,
     Generic,
     List,
     Mapping,
     NamedTuple,
     Optional,
     Tuple,
-    Type,
     TypeVar,
     Union,
     cast,
@@ -29,7 +27,6 @@
 from strawberry.type import StrawberryType, has_object_definition
 from strawberry.types.info import Info
 from strawberry.utils.cached_property import cached_property
-from strawberry.utils.typing import eval_type
 
 if TYPE_CHECKING:
     import builtins
@@ -59,7 +56,9 @@ class Signature(inspect.Signature):
 
 class ReservedParameterSpecification(Protocol):
     def find(
-        self, parameters: Tuple[inspect.Parameter, ...], resolver: StrawberryResolver
+        self,
+        parameters: Tuple[inspect.Parameter, ...],
+        resolver: StrawberryResolver[Any],
     ) -> Optional[inspect.Parameter]:
         """Finds the reserved parameter from ``parameters``."""
 
@@ -68,8 +67,11 @@ class ReservedName(NamedTuple):
     name: str
 
     def find(
-        self, parameters: Tuple[inspect.Parameter, ...], _: StrawberryResolver
+        self,
+        parameters: Tuple[inspect.Parameter, ...],
+        resolver: StrawberryResolver[Any],
     ) -> Optional[inspect.Parameter]:
+        del resolver
         return next((p for p in parameters if p.name == self.name), None)
 
 
@@ -77,8 +79,11 @@ class ReservedNameBoundParameter(NamedTuple):
     name: str
 
     def find(
-        self, parameters: Tuple[inspect.Parameter, ...], _: StrawberryResolver
+        self,
+        parameters: Tuple[inspect.Parameter, ...],
+        resolver: StrawberryResolver[Any],
     ) -> Optional[inspect.Parameter]:
+        del resolver
         if parameters:  # Add compatibility for resolvers with no arguments
             first_parameter = parameters[0]
             return first_parameter if first_parameter.name == self.name else None
@@ -94,27 +99,23 @@ class ReservedType(NamedTuple):
     """
 
     name: str
-    type: Type
+    type: type
 
     def find(
-        self, parameters: Tuple[inspect.Parameter, ...], resolver: StrawberryResolver
+        self,
+        parameters: Tuple[inspect.Parameter, ...],
+        resolver: StrawberryResolver[Any],
     ) -> Optional[inspect.Parameter]:
         for parameter in parameters:
-            annotation = parameter.annotation
-            try:
-                resolved_annotation = eval_type(
-                    ForwardRef(annotation)
-                    if isinstance(annotation, str)
-                    else annotation,
-                    resolver._namespace,
-                    None,
-                )
-                resolver._resolved_annotations[parameter] = resolved_annotation
-            except NameError:
-                # Type-annotation could not be resolved
-                resolved_annotation = annotation
-            if self.is_reserved_type(resolved_annotation):
-                return parameter
+            annotation = resolver.strawberry_annotations[parameter]
+            if isinstance(annotation, StrawberryAnnotation):
+                try:
+                    evaled_annotation = annotation.evaluate()
+                except NameError:
+                    continue
+                else:
+                    if self.is_reserved_type(evaled_annotation):
+                        return parameter
 
         # Fallback to matching by name
         reserved_name = ReservedName(name=self.name).find(parameters, resolver)
@@ -130,7 +131,7 @@ def find(
         else:
             return None
 
-    def is_reserved_type(self, other: Type) -> bool:
+    def is_reserved_type(self, other: builtins.type) -> bool:
         origin = cast(type, get_origin(other)) or other
         if origin is Annotated:
             # Handle annotated arguments such as Private[str] and DirectiveValue[str]
@@ -174,11 +175,6 @@ def __init__(
 
         This is used when creating copies of types w/ generics
         """
-        self._resolved_annotations: Dict[inspect.Parameter, Any] = {}
-        """Populated during reserved parameter determination.
-
-        Caching resolved annotations this way prevents evaling them repeatedly.
-        """
 
     # TODO: Use this when doing the actual resolving? How to deal with async resolvers?
     def __call__(self, *args: str, **kwargs: Any) -> T:
@@ -190,6 +186,20 @@ def __call__(self, *args: str, **kwargs: Any) -> T:
     def signature(self) -> inspect.Signature:
         return Signature.from_callable(self._unbound_wrapped_func, follow_wrapped=True)
 
+    # TODO: find better name
+    @cached_property
+    def strawberry_annotations(
+        self,
+    ) -> Dict[inspect.Parameter, Union[StrawberryAnnotation, None]]:
+        return {
+            p: (
+                StrawberryAnnotation(p.annotation, namespace=self._namespace)
+                if p.annotation is not inspect.Signature.empty
+                else None
+            )
+            for p in self.signature.parameters.values()
+        }
+
     @cached_property
     def reserved_parameters(
         self,
@@ -204,21 +214,19 @@ def arguments(self) -> List[StrawberryArgument]:
         parameters = self.signature.parameters.values()
         reserved_parameters = set(self.reserved_parameters.values())
 
-        missing_annotations = []
-        arguments = []
+        missing_annotations: List[str] = []
+        arguments: List[StrawberryArgument] = []
         user_parameters = (p for p in parameters if p not in reserved_parameters)
 
         for param in user_parameters:
-            annotation = self._resolved_annotations.get(param, param.annotation)
-            if annotation is inspect.Signature.empty:
+            annotation = self.strawberry_annotations[param]
+            if annotation is None:
                 missing_annotations.append(param.name)
             else:
                 argument = StrawberryArgument(
                     python_name=param.name,
                     graphql_name=None,
-                    type_annotation=StrawberryAnnotation(
-                        annotation=annotation, namespace=self._namespace
-                    ),
+                    type_annotation=annotation,
                     default=param.default,
                 )
                 arguments.append(argument)
@@ -243,6 +251,7 @@ def name(self) -> str:
         # TODO: What to do if resolver is a lambda?
         return self._unbound_wrapped_func.__name__
 
+    # TODO: consider deprecating
     @cached_property
     def annotations(self) -> Dict[str, object]:
         """Annotations for the resolver.


Test patch:
diff --git a/tests/schema/test_annotated/__init__.py b/tests/schema/test_annotated/__init__.py
new file mode 100644
diff --git a/tests/schema/test_annotated/type_a.py b/tests/schema/test_annotated/type_a.py
new file mode 100644
--- /dev/null
+++ b/tests/schema/test_annotated/type_a.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Annotated
+from uuid import UUID
+
+import strawberry
+from strawberry.types import Info
+
+
+@strawberry.type
+class Query:
+    @strawberry.field
+    def get_testing(
+        self,
+        info: Info[None, None],
+        id_: Annotated[UUID, strawberry.argument(name="id")],
+    ) -> Optional[str]:
+        ...
diff --git a/tests/schema/test_annotated/type_b.py b/tests/schema/test_annotated/type_b.py
new file mode 100644
--- /dev/null
+++ b/tests/schema/test_annotated/type_b.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Annotated
+from uuid import UUID
+
+import strawberry
+from strawberry.types import Info
+
+
+@strawberry.type
+class Query:
+    @strawberry.field
+    def get_testing(
+        self,
+        id_: Annotated[UUID, strawberry.argument(name="id")],
+        info: Info[None, None],
+    ) -> Optional[str]:
+        ...
diff --git a/tests/schema/test_arguments.py b/tests/schema/test_arguments.py
--- a/tests/schema/test_arguments.py
+++ b/tests/schema/test_arguments.py
@@ -1,3 +1,4 @@
+import textwrap
 from textwrap import dedent
 from typing import Optional
 from typing_extensions import Annotated
@@ -199,3 +200,40 @@ def hello(
     assert field_definition.arguments[0].metadata == {
         "test": "foo",
     }
+
+
+def test_argument_parse_order():
+    """Check early early exit from argument parsing due to finding ``info``.
+
+    Reserved argument parsing, which interally also resolves annotations, exits early
+    after detecting the ``info`` argumnent. As a result, the annotation of the ``id_``
+    argument in `tests.schema.test_annotated.type_a.Query` is never resolved. This
+    results in `StrawberryArgument` not being able to detect that ``id_`` makes use of
+    `typing.Annotated` and `strawberry.argument`.
+
+    This behavior is fixed by by ensuring that `StrawberryArgument` makes use of the new
+    `StrawberryAnnotation.evaluate` method instead of consuming the raw annotation.
+
+    An added benefit of this fix is that by removing annotation resolving code from
+    `StrawberryResolver` and making it a part of `StrawberryAnnotation`, it makes it
+    possible for `StrawberryArgument` and `StrawberryResolver` to share the same type
+    evaluation cache.
+
+    Refer to: https://github.com/strawberry-graphql/strawberry/issues/2855
+    """
+
+    from tests.schema.test_annotated import type_a, type_b
+
+    expected = """
+    type Query {
+      getTesting(id: UUID!): String
+    }
+
+    scalar UUID
+    """
+
+    schema_a = strawberry.Schema(type_a.Query)
+    schema_b = strawberry.Schema(type_b.Query)
+
+    assert str(schema_a) == str(schema_b)
+    assert str(schema_a) == textwrap.dedent(expected).strip()