Skip to content
9 changes: 9 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
release type: minor
---

This release adds `Info.field_args`, a cached property that returns the arguments
passed to the current field, already converted to Strawberry types. Scalars are
coerced and input types are converted to their proper dataclasses, mirroring the
values a resolver receives. Both inline literals and variables are handled, and
arguments that were not provided are omitted.
1 change: 1 addition & 0 deletions docs/types/resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ Info objects contain information for the current execution context:
| context | `ContextType` | The value of the context |
| root_value | `RootValueType` | The value for the root type |
| variable_values | `Dict[str, Any]` | The variables for this operation |
| field_args | `Dict[str, Any]` | The current field's arguments, converted to Strawberry types |
| query | `str \| None` | The full GraphQL document string sent in the request |
| operation | `OperationDefinitionNode` | The ast for the current operation (public API might change in future) |
| path | `Path` | The path for the current field |
Expand Down
37 changes: 37 additions & 0 deletions strawberry/types/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
)
from typing_extensions import TypeVar

from graphql import get_argument_values

Comment thread
rcybulski1122012 marked this conversation as resolved.
from .arguments import convert_arguments
from .nodes import convert_selections

if TYPE_CHECKING:
Expand Down Expand Up @@ -102,6 +105,40 @@ def selected_fields(self) -> list[Selection]:
info = self._raw_info
return convert_selections(info, info.field_nodes)

@cached_property
def field_args(self) -> dict[str, Any]:
"""The arguments passed to the current field, converted to strawberry types.

Scalars are coerced and input types are converted to their proper
dataclasses, mirroring the values a resolver receives. Arguments with a
default value that were not provided in the query are included with that
default; arguments without a default that were not provided are omitted.

The arguments are read from the first field node; when a field is
selected multiple times (for example through fragments) GraphQL requires
the arguments to be identical, so the first node is representative.

Comment thread
rcybulski1122012 marked this conversation as resolved.
Returns an empty dict if the field definition cannot be resolved (for
example for introspection fields), since those carry no strawberry
arguments.
"""
raw_info = self._raw_info
field_node = raw_info.field_nodes[0]
field_def = raw_info.parent_type.fields.get(raw_info.field_name)
if field_def is None:
return {}

raw_args = get_argument_values(field_def, field_node, raw_info.variable_values)

schema_converter = self.schema.schema_converter

return convert_arguments(
value=raw_args,
arguments=self._field.arguments,
config=schema_converter.config,
scalar_registry=schema_converter.scalar_registry,
)

@property
def context(self) -> ContextType:
"""The context passed to the query execution."""
Expand Down
269 changes: 269 additions & 0 deletions tests/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,272 @@ def test_cannot_use_info_with_more_than_two_arguments():
match=r"Too many (arguments|parameters) for <class '.*.Info'>; actual 3, expected 2",
):
strawberry.Info[int, str, int] # type: ignore


def test_field_args_direct_scalar():
captured_args = {}

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, arg1: str, second_arg: int) -> str:
nonlocal captured_args
captured_args = info.field_args
return "result"

schema = strawberry.Schema(query=Query)
query = 'query { testField(arg1: "value1", secondArg: 123) }'

result = schema.execute_sync(query)

assert result.errors is None
assert captured_args == {"arg1": "value1", "second_arg": 123}


def test_field_args_variable_scalar():
captured_args = {}

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, arg1: str, arg2: int) -> str:
nonlocal captured_args
captured_args = info.field_args
return "result"

schema = strawberry.Schema(query=Query)
query = "query ($v1: String!, $v2: Int!) { testField(arg1: $v1, arg2: $v2) }"
variable_values = {"v1": "value1", "v2": 123}

result = schema.execute_sync(query, variable_values=variable_values)

assert result.errors is None
assert captured_args == {"arg1": "value1", "arg2": 123}


def test_field_args_direct_input():
captured_args = {}

@strawberry.input
class TestInput:
name: str
value: int

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, data: TestInput) -> str:
nonlocal captured_args
captured_args = info.field_args
return "result"

schema = strawberry.Schema(query=Query)
query = 'query { testField(data: { name: "test", value: 42 }) }'

result = schema.execute_sync(query)

assert result.errors is None
assert captured_args == {"data": TestInput(name="test", value=42)}


def test_field_args_variable_input():
captured_args = {}

@strawberry.input
class TestInput:
name: str
value: int

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, data: TestInput) -> str:
nonlocal captured_args
captured_args = info.field_args
return "result"

schema = strawberry.Schema(query=Query)
query = "query ($data: TestInput!) { testField(data: $data) }"
variables = {"data": {"name": "var", "value": 7}}

result = schema.execute_sync(query, variable_values=variables)

assert result.errors is None
assert captured_args == {"data": TestInput(name="var", value=7)}


def test_field_args_direct_nested_input():
captured_args = {}

@strawberry.input
class ChildInput:
title: str
count: int

@strawberry.input
class ParentInput:
child: ChildInput

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, data: ParentInput) -> str:
nonlocal captured_args
captured_args = info.field_args
return "done"

schema = strawberry.Schema(query=Query)
query = 'query { testField(data: { child: { title: "x", count: 2 } }) }'

result = schema.execute_sync(query)

assert result.errors is None
assert captured_args == {"data": ParentInput(child=ChildInput(title="x", count=2))}


def test_field_args_variable_nested_input():
captured_args = {}

@strawberry.input
class ChildInput:
title: str
count: int

@strawberry.input
class ParentInput:
child: ChildInput

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, data: ParentInput) -> str:
nonlocal captured_args
captured_args = info.field_args
return "ok"

schema = strawberry.Schema(query=Query)
query = "query ($data: ParentInput!) { testField(data: $data) }"
variables = {"data": {"child": {"title": "y", "count": 5}}}

result = schema.execute_sync(query, variable_values=variables)

assert result.errors is None
assert captured_args == {"data": ParentInput(child=ChildInput(title="y", count=5))}


def test_field_args_direct_list_of_input():
captured_args = {}

@strawberry.input
class ItemInput:
name: str
qty: int

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, items: list[ItemInput]) -> str:
nonlocal captured_args
captured_args = info.field_args
return "ok"

schema = strawberry.Schema(query=Query)
query = 'query { testField(items: [{ name: "a", qty: 1 }, { name: "b", qty: 2 }]) }'

result = schema.execute_sync(query)

assert result.errors is None
assert captured_args == {
"items": [ItemInput(name="a", qty=1), ItemInput(name="b", qty=2)]
}


def test_field_args_variable_list_of_inputs():
captured_args = {}

@strawberry.input
class ItemInput:
name: str
qty: int

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, items: list[ItemInput]) -> str:
nonlocal captured_args
captured_args = info.field_args
return "ok"

schema = strawberry.Schema(query=Query)
query = "query ($items: [ItemInput!]!) { testField(items: $items) }"
variables = {"items": [{"name": "c", "qty": 3}, {"name": "d", "qty": 4}]}

result = schema.execute_sync(query, variable_values=variables)

assert result.errors is None
assert captured_args == {
"items": [ItemInput(name="c", qty=3), ItemInput(name="d", qty=4)]
}


def test_field_args_maybe_unset_handling():
captured_args = {}

@strawberry.input
class MaybeInput:
required: str
maybe: strawberry.Maybe[str]
maybe_null: strawberry.Maybe[str | None]
optional: str | None = strawberry.UNSET

@strawberry.type
class Query:
@strawberry.field
@staticmethod
def test_field(info: strawberry.Info, data: MaybeInput) -> str:
nonlocal captured_args
captured_args = info.field_args
return "ok"

schema = strawberry.Schema(query=Query)

# Case 1: All fields provided
query_1 = (
'query { testField(data: { required: "req", optional: "opt", '
'maybe: "may", maybeNull: "mayNull" }) }'
)
result = schema.execute_sync(query_1)
assert result.errors is None
assert captured_args["data"] == MaybeInput(
required="req",
optional="opt",
maybe=strawberry.Some("may"),
maybe_null=strawberry.Some("mayNull"),
)

# Case 2: Optional and Maybe fields omitted (UNSET)
query_2 = 'query { testField(data: { required: "req" }) }'
result = schema.execute_sync(query_2)
assert result.errors is None
assert captured_args["data"] == MaybeInput(
required="req", maybe=None, maybe_null=None, optional=strawberry.UNSET
)

# Case 3: Explicit null for maybeNull
query_3 = 'query { testField(data: { required: "req", maybeNull: null }) }'
result = schema.execute_sync(query_3)
assert result.errors is None
assert captured_args["data"] == MaybeInput(
required="req",
maybe=None,
maybe_null=strawberry.Some(None),
optional=strawberry.UNSET,
)
Loading