Repository: strawberry-graphql/strawberry

Issue:
Selected fields are not available in Info for Fragments
The following test shows that the selected fields for the Fragment `frag` are not available in `Info.selected_fields`:
```python
def test_info_field_fragment():
    @strawberry.type
    class Result:
        field: str

    @strawberry.type
    class Query:
        @strawberry.field
        def hello(self, info: Info[str, str]) -> Result:
            print(info.selected_fields)
            return Result(field=json.dumps(dataclasses.asdict(*info.selected_fields)))

    schema = strawberry.Schema(query=Query)
    query = """
    fragment frag on Result {
        field
    }
    query {
        hello {
            ...frag
        }
    }
    """
    result = schema.execute_sync(query)
    assert not result.errors
    info = result.data["hello"]
    field = json.loads(info["field"])
    assert field == {
        "name": "hello",
        "directives": {},
        "alias": None,
        "arguments": {},
        "selections": [
            {"name": "frag", "directives": {}},
        ],
    }
```

`selected_fields` is `[SelectedField(name='hello', directives={}, alias=None, arguments={}, selections=[FragmentSpread(name='frag', directives={})])]`
Is this the intended behaviour? I am in the process of rewriting [graphene-django-optimizer](https://github.com/tfoxy/graphene-django-optimizer) to work with strawberry-django. For this use case it would be very helpfull to have the selected fields of the Fragment available in `Info.selected_fields`.


Discussion:
I think this is an oversight. It should be possible to include the selections in the fragment as well, I'll take a look.

Resolving patch:
diff --git a/strawberry/types/info.py b/strawberry/types/info.py
--- a/strawberry/types/info.py
+++ b/strawberry/types/info.py
@@ -14,7 +14,7 @@
 if TYPE_CHECKING:
     from strawberry.field import StrawberryField
 
-from .nodes import SelectedField
+from .nodes import Selection, convert_selections
 
 
 ContextType = TypeVar("ContextType")
@@ -39,9 +39,9 @@ def field_nodes(self) -> List[FieldNode]:  # deprecated
         return self._raw_info.field_nodes
 
     @cached_property
-    def selected_fields(self) -> List[SelectedField]:
+    def selected_fields(self) -> List[Selection]:
         info = self._raw_info
-        return list(map(SelectedField, info.field_nodes))
+        return convert_selections(info, info.field_nodes)
 
     @property
     def context(self) -> ContextType:
diff --git a/strawberry/types/nodes.py b/strawberry/types/nodes.py
--- a/strawberry/types/nodes.py
+++ b/strawberry/types/nodes.py
@@ -1,7 +1,7 @@
 """
 Abstraction layer for graphql-core field nodes.
 
-Call `SelectedField` on a graphql `FieldNode`, such as in `info.field_nodes`.
+Call `convert_sections` on a list of GraphQL `FieldNode`s, such as in `info.field_nodes`.
 
 If a node has only one useful value, it's value is inlined.
 
@@ -10,55 +10,73 @@
 """
 
 import dataclasses
-from typing import Any, Dict, Iterable, List, NewType, Optional, Union
-
-from graphql.language import (  # type: ignore
-    ArgumentNode,
-    DirectiveNode,
-    FieldNode,
-    FragmentSpreadNode,
-    InlineFragmentNode,
-    Node,
-    ValueNode,
+from typing import Any, Dict, Iterable, List, Optional, Union
+
+from graphql import GraphQLResolveInfo
+from graphql.language import (
+    ArgumentNode as GQLArgumentNode,
+    DirectiveNode as GQLDirectiveNode,
+    FieldNode as GQLFieldNode,
+    FragmentSpreadNode as GQLFragmentSpreadNode,
+    InlineFragmentNode as GQLInlineFragment,
+    InlineFragmentNode as GQLInlineFragmentNode,
+    ListValueNode as GQLListValueNode,
+    ObjectValueNode as GQLObjectValueNode,
+    ValueNode as GQLValueNode,
+    VariableNode as GQLVariableNode,
 )
 
 
-Arguments = NewType("Arguments", Dict[str, Any])
-Directives = NewType("Directives", Dict[str, Arguments])
+Arguments = Dict[str, Any]
+Directives = Dict[str, Arguments]
 Selection = Union["SelectedField", "FragmentSpread", "InlineFragment"]
 
 
-def value(node: ValueNode) -> Any:
+def convert_value(info: GraphQLResolveInfo, node: GQLValueNode) -> Any:
     """Return useful value from any node."""
-    if hasattr(node, "fields"):
+    if isinstance(node, GQLVariableNode):
+        # Look up variable
+        name = node.name.value
+        return info.variable_values[name]
+    if isinstance(node, GQLListValueNode):
+        return [convert_value(info, value) for value in node.values]
+    if isinstance(node, GQLObjectValueNode):
         return {
-            field.name.value: value(field.value)
-            for field in node.fields  # type: ignore
+            field.name.value: convert_value(info, field.value) for field in node.fields
         }
-    if hasattr(node, "values"):
-        return list(map(value, node.values))  # type: ignore
-    if hasattr(node, "name"):
-        return node.name.value  # type: ignore
     return getattr(node, "value", None)
 
 
-def arguments(nodes: Iterable[ArgumentNode]) -> Arguments:
+def convert_arguments(
+    info: GraphQLResolveInfo, nodes: Iterable[GQLArgumentNode]
+) -> Arguments:
     """Return mapping of arguments."""
-    return {node.name.value: value(node.value) for node in nodes}  # type: ignore
+    return {node.name.value: convert_value(info, node.value) for node in nodes}
 
 
-def directives(nodes: Iterable[DirectiveNode]) -> Directives:
+def convert_directives(
+    info: GraphQLResolveInfo, nodes: Iterable[GQLDirectiveNode]
+) -> Directives:
     """Return mapping of directives."""
-    return {node.name.value: arguments(node.arguments) for node in nodes}  # type: ignore
+    return {node.name.value: convert_arguments(info, node.arguments) for node in nodes}
 
 
-def selection(node: Node) -> Selection:
+def convert_selections(
+    info: GraphQLResolveInfo, field_nodes: List[GQLFieldNode]
+) -> List[Selection]:
     """Return typed `Selection` based on node type."""
-    if hasattr(node, "alias"):
-        return SelectedField(node)  # type: ignore
-    if hasattr(node, "selection_set"):
-        return InlineFragment(node)  # type: ignore
-    return FragmentSpread(node)  # type: ignore
+    selections: List[Selection] = []
+    for node in field_nodes:
+        if isinstance(node, GQLFieldNode):
+            selections.append(SelectedField.from_node(info, node))
+        elif isinstance(node, GQLInlineFragment):
+            selections.append(InlineFragment.from_node(info, node))
+        elif isinstance(node, GQLFragmentSpreadNode):
+            selections.append(FragmentSpread.from_node(info, node))
+        else:
+            raise TypeError(f"Unknown node type: {node}")
+
+    return selections
 
 
 @dataclasses.dataclass
@@ -66,11 +84,23 @@ class FragmentSpread:
     """Wrapper for a FragmentSpreadNode."""
 
     name: str
+    type_condition: str
     directives: Directives
+    selections: List[Selection]
 
-    def __init__(self, node: FragmentSpreadNode):
-        self.name = node.name.value
-        self.directives = directives(node.directives)
+    @classmethod
+    def from_node(cls, info: GraphQLResolveInfo, node: GQLFragmentSpreadNode):
+        # Look up fragment
+        name = node.name.value
+        fragment = info.fragments[name]
+        return cls(
+            name=name,
+            directives=convert_directives(info, node.directives),
+            type_condition=fragment.type_condition.name.value,
+            selections=convert_selections(
+                info, getattr(fragment.selection_set, "selections", [])
+            ),
+        )
 
 
 @dataclasses.dataclass
@@ -81,26 +111,35 @@ class InlineFragment:
     selections: List[Selection]
     directives: Directives
 
-    def __init__(self, node: InlineFragmentNode):
-        self.type_condition = node.type_condition.name.value
-        self.selections = list(
-            map(selection, getattr(node.selection_set, "selections", []))
+    @classmethod
+    def from_node(cls, info: GraphQLResolveInfo, node: GQLInlineFragmentNode):
+        return cls(
+            type_condition=node.type_condition.name.value,
+            selections=convert_selections(
+                info, getattr(node.selection_set, "selections", [])
+            ),
+            directives=convert_directives(info, node.directives),
         )
-        self.directives = directives(node.directives)
 
 
 @dataclasses.dataclass
-class SelectedField(FragmentSpread):
+class SelectedField:
     """Wrapper for a FieldNode."""
 
-    alias: Optional[str]
+    name: str
+    directives: Directives
     arguments: Arguments
     selections: List[Selection]
-
-    def __init__(self, node: FieldNode):
-        super().__init__(node)  # type: ignore
-        self.alias = getattr(node.alias, "value", None)
-        self.arguments = arguments(node.arguments)
-        self.selections = list(
-            map(selection, getattr(node.selection_set, "selections", []))
+    alias: Optional[str] = None
+
+    @classmethod
+    def from_node(cls, info: GraphQLResolveInfo, node: GQLFieldNode):
+        return cls(
+            name=node.name.value,
+            directives=convert_directives(info, node.directives),
+            alias=getattr(node.alias, "value", None),
+            arguments=convert_arguments(info, node.arguments),
+            selections=convert_selections(
+                info, getattr(node.selection_set, "selections", [])
+            ),
         )


Test patch:
diff --git a/tests/schema/test_info.py b/tests/schema/test_info.py
--- a/tests/schema/test_info.py
+++ b/tests/schema/test_info.py
@@ -5,7 +5,9 @@
 import pytest
 
 import strawberry
+from strawberry.arguments import UNSET
 from strawberry.types import Info
+from strawberry.types.nodes import FragmentSpread, InlineFragment, SelectedField
 
 
 def test_info_has_the_correct_shape():
@@ -84,55 +86,144 @@ def hello(self, info: Info[str, str]) -> Result:
 def test_info_field_fragments():
     @strawberry.type
     class Result:
-        field: str
+        ok: bool
+
+    selected_fields = None
 
     @strawberry.type
     class Query:
         @strawberry.field
         def hello(self, info: Info[str, str]) -> Result:
-            return Result(field=json.dumps(dataclasses.asdict(*info.selected_fields)))
+            nonlocal selected_fields
+            selected_fields = info.selected_fields
+            return Result(ok=True)
 
     schema = strawberry.Schema(query=Query)
     query = """{
         hello {
             ... on Result {
-                f: field @include(if: true)
+                k: ok @include(if: true)
             }
             ...frag
         }
     }
 
     fragment frag on Result {
-        field
+        ok
     }
     """
     result = schema.execute_sync(query)
 
     assert not result.errors
-    info = result.data["hello"]
-    field = json.loads(info.pop("f"))
-    assert field == {
-        "name": "hello",
-        "directives": {},
-        "alias": None,
-        "arguments": {},
-        "selections": [
-            {
-                "type_condition": "Result",
-                "selections": [
-                    {
-                        "name": "field",
-                        "directives": {"include": {"if": True}},
-                        "alias": "f",
-                        "arguments": {},
-                        "selections": [],
-                    }
-                ],
-                "directives": {},
+    assert selected_fields == [
+        SelectedField(
+            name="hello",
+            directives={},
+            alias=None,
+            arguments={},
+            selections=[
+                InlineFragment(
+                    type_condition="Result",
+                    directives={},
+                    selections=[
+                        SelectedField(
+                            name="ok",
+                            alias="k",
+                            arguments={},
+                            directives={
+                                "include": {
+                                    "if": True,
+                                },
+                            },
+                            selections=[],
+                        )
+                    ],
+                ),
+                FragmentSpread(
+                    name="frag",
+                    directives={},
+                    type_condition="Result",
+                    selections=[
+                        SelectedField(
+                            name="ok",
+                            directives={},
+                            arguments={},
+                            selections=[],
+                        )
+                    ],
+                ),
+            ],
+        )
+    ]
+
+
+def test_info_arguments():
+    @strawberry.input
+    class TestInput:
+        name: str
+        age: Optional[int] = UNSET
+
+    selected_fields = None
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def test_arg(
+            self, info: Info[str, str], input: TestInput, another_arg: bool = True
+        ) -> str:
+            nonlocal selected_fields
+            selected_fields = info.selected_fields
+            return "Hi"
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """{
+        testArg(input: {name: "hi"})
+    }
+    """
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert selected_fields == [
+        SelectedField(
+            name="testArg",
+            directives={},
+            arguments={
+                "input": {
+                    "name": "hi",
+                },
             },
-            {"name": "frag", "directives": {}},
-        ],
+            selections=[],
+        )
+    ]
+
+    query = """query TestQuery($input: TestInput!) {
+        testArg(input: $input)
     }
+    """
+    result = schema.execute_sync(
+        query,
+        variable_values={
+            "input": {
+                "name": "hi",
+                "age": 10,
+            },
+        },
+    )
+    assert not result.errors
+    assert selected_fields == [
+        SelectedField(
+            name="testArg",
+            directives={},
+            arguments={
+                "input": {
+                    "name": "hi",
+                    "age": 10,
+                },
+            },
+            selections=[],
+        )
+    ]
 
 
 @pytest.mark.parametrize(