Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
release type: minor
---

Prefetch callables can now receive resolved field arguments as keyword parameters.

If a `prefetch_related` callable declares parameters beyond `info`, the optimizer
will automatically resolve the field's GraphQL arguments and pass matching ones
as keyword arguments. Existing callables that only accept `info` continue to work
unchanged.

Also adds `strawberry_django.get_field_arguments(info)` — a public utility that
resolves the current field's GraphQL arguments from an `Info` object without
raw AST access.
2 changes: 2 additions & 0 deletions strawberry_django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
)
from .filters import filter_type, process_filters
from .mutations.mutations import input_mutation, mutation
from .optimizer import get_field_arguments
from .ordering import Ordering, order, order_type, process_order
from .resolvers import django_resolver
from .type import input, interface, partial, type # noqa: A004
Expand Down Expand Up @@ -63,6 +64,7 @@
"filter_field",
"filter_type",
"filters",
"get_field_arguments",
"input",
"input_mutation",
"interface",
Expand Down
97 changes: 89 additions & 8 deletions strawberry_django/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import contextvars
import copy
import dataclasses
import inspect
import itertools
from collections.abc import Callable
from typing import (
Expand Down Expand Up @@ -94,6 +95,7 @@
"OptimizerConfig",
"OptimizerStore",
"PrefetchType",
"get_field_arguments",
"optimize",
]

Expand Down Expand Up @@ -211,7 +213,11 @@ def with_hints(
),
)

def with_resolved_callables(self, info: Info):
def with_resolved_callables(
self,
info: Info,
field_kwargs: dict[str, Any] | None = None,
):
"""Resolve any prefetch/annotate callables using the provided info and return a new store.

This is used to resolve callables using the correct info object, scoped to their respective fields.
Expand All @@ -222,7 +228,8 @@ def with_resolved_callables(self, info: Info):
return self

prefetch_related: list[PrefetchType] = [
p(info) if callable(p) else p for p in self.prefetch_related
_invoke_prefetch_callable(p, info, field_kwargs) if callable(p) else p
for p in self.prefetch_related
]
annotate: dict[str, AnnotateType] = {
label: annotation(info) if callable(annotation) else annotation
Expand All @@ -235,7 +242,13 @@ def with_resolved_callables(self, info: Info):
annotate=annotate,
)

def with_prefix(self, prefix: str, *, info: Info):
def with_prefix(
self,
prefix: str,
*,
info: Info,
field_kwargs: dict[str, Any] | None = None,
):
"""Create a copy of this store with the given prefix.

This is useful when we need to apply the same store to a nested field.
Expand All @@ -246,7 +259,7 @@ def with_prefix(self, prefix: str, *, info: Info):
for p in self.prefetch_related:
if isinstance(p, Callable):
assert_type(p, PrefetchCallable)
p = p(info) # noqa: PLW2901
p = _invoke_prefetch_callable(p, info, field_kwargs) # noqa: PLW2901

if isinstance(p, str):
prefetch_related.append(f"{prefix}{LOOKUP_SEP}{p}")
Expand Down Expand Up @@ -518,6 +531,65 @@ def _create_strawberry_info(raw_info: GraphQLResolveInfo) -> Info:
return schema.config.info_class(raw_info, field)


def get_field_arguments(info: Info) -> dict[str, Any]:

@rcybulski1122012 rcybulski1122012 Mar 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the version that Claude generated for me 😅

```python
def _get_field_args(raw_info: GraphQLResolveInfo) -> dict[str, Any]:
    field_def = get_field_def(raw_info.schema, raw_info.parent_type, raw_info.field_nodes[0])
    raw_args = get_argument_values(field_def, raw_info.field_nodes[0], raw_info.variable_values)

    strawberry_schema = getattr(raw_info.schema, "_strawberry_schema", None) or raw_info.schema.extensions.get(
        GraphQLCoreConverter.DEFINITION_BACKREF
    )
    schema_converter = getattr(strawberry_schema, "schema_converter", None)
    strawberry_field = field_def.extensions.get(GraphQLCoreConverter.DEFINITION_BACKREF)

    if not schema_converter:
        raise RuntimeError(
            "Failed to find strawberry schema for field. Ensure the schema is a strawberry.Schema instance."
        )

    if not strawberry_field:
        raise RuntimeError("Failed to find strawberry field definition. Arguments cannot be correctly converted.")

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

I don't know the library internals well enough to tell whether they behave differently

"""Resolve the current field's GraphQL arguments into Python kwargs.

This uses the same argument resolution pipeline as the optimizer
(``get_argument_values`` + ``get_arguments``), so variables, defaults,
and input-type conversions are all handled transparently.

Returns a dict of keyword arguments (excluding ``info`` itself).
"""
raw_info = info._raw_info
strawberry_schema = cast("Schema", raw_info.schema._strawberry_schema) # type: ignore
field = info._field
field_name = strawberry_schema.config.name_converter.from_field(field)
gql_field = raw_info.parent_type.fields.get(field_name)
if gql_field is None:
return {}
_, kwargs = get_arguments(
field=field,
source=None,
info=info,
kwargs=get_argument_values(
gql_field,
raw_info.field_nodes[0],
raw_info.variable_values,
),
config=strawberry_schema.config,
scalar_registry=strawberry_schema.schema_converter.scalar_registry,
)
kwargs.pop("info", None)
return kwargs


def _invoke_prefetch_callable(
p: PrefetchCallable,
info: Info,
field_kwargs: dict[str, Any] | None = None,
) -> Prefetch:
if field_kwargs:
sig = inspect.signature(p, eval_str=True)
params = sig.parameters
has_var_keyword = any(
param.kind == inspect.Parameter.VAR_KEYWORD for param in params.values()
)
if has_var_keyword:
return p(info, **field_kwargs)
named_extra = {
name
for name, param in params.items()
if name != "info"
and param.kind
not in {inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD}
}
if named_extra:
filtered = {k: v for k, v in field_kwargs.items() if k in named_extra}
if filtered:
return p(info, **filtered)
return p(info)


def _get_django_type(
field: StrawberryField,
) -> type[WithStrawberryDjangoObjectDefinition] | None:
Expand Down Expand Up @@ -803,11 +875,15 @@ def _get_hints_from_field(
field.name: field_store.annotate[_annotate_placeholder],
}

field_kwargs: dict[str, Any] | None = None
if any(callable(p) for p in field_store.prefetch_related):
field_kwargs = get_field_arguments(f_info)

# with_prefix also resolves callables, so we only need one or the other
return (
field_store.with_prefix(prefix, info=f_info)
field_store.with_prefix(prefix, info=f_info, field_kwargs=field_kwargs)
if prefix
else field_store.with_resolved_callables(f_info)
else field_store.with_resolved_callables(f_info, field_kwargs)
)


Expand All @@ -824,11 +900,16 @@ def _get_hints_from_model_property(
and model_attr.store
):
attr_store = model_attr.store

field_kwargs: dict[str, Any] | None = None
if any(callable(p) for p in attr_store.prefetch_related):
field_kwargs = get_field_arguments(f_info)

# with_prefix also resolves callables, so we only need one or the other
store = (
attr_store.with_prefix(prefix, info=f_info)
attr_store.with_prefix(prefix, info=f_info, field_kwargs=field_kwargs)
if prefix
else attr_store.with_resolved_callables(f_info)
else attr_store.with_resolved_callables(f_info, field_kwargs)
)
else:
store = None
Expand Down
Loading
Loading