Skip to content
Merged
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
16 changes: 12 additions & 4 deletions src/graphql/type/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1254,16 +1254,16 @@ class GraphQLDefaultInput: # noqa: PLW1641
literal: ConstValueNode | None
"""GraphQL literal default value, or None if a runtime value is provided instead."""

__slots__ = "_memoized_coercion", "literal", "value"
__slots__ = "_memoized_coerced_value", "literal", "value"

def __init__(
self, value: Any = Undefined, literal: ConstValueNode | None = None
) -> None:
self.value = value
self.literal = literal
# Used to memoize the result of coercing the default value, together with
# the type it was coerced to (see coerce_default_value() in the utilities).
self._memoized_coercion: tuple[GraphQLInputType | None, Any] = None, Undefined
# Used to memoize the result of coercing the default value of a variable
# signature (see coerce_default_value() in the utilities).
self._memoized_coerced_value: Any = Undefined

def __eq__(self, other: object) -> bool:
return self is other or (
Expand Down Expand Up @@ -1352,6 +1352,10 @@ class GraphQLArgument: # noqa: PLW1641
ast_node: InputValueDefinitionNode | None
"""AST node from which this schema element was built, if available."""

# Used to memoize the result of coercing the default value
# (see coerce_default_value() in the utilities).
_memoized_coerced_value: Any = Undefined

def __init__(
self,
type_: GraphQLInputType,
Expand Down Expand Up @@ -3326,6 +3330,10 @@ class GraphQLInputField: # noqa: PLW1641
ast_node: InputValueDefinitionNode | None
"""AST node from which this schema element was built, if available."""

# Used to memoize the result of coercing the default value
# (see coerce_default_value() in the utilities).
_memoized_coerced_value: Any = Undefined

def __init__(
self,
type_: GraphQLInputType,
Expand Down
26 changes: 16 additions & 10 deletions src/graphql/utilities/coerce_input_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
is_iterable,
)
from ..type import (
GraphQLArgument,
GraphQLInputField,
GraphQLInputType,
assert_leaf_type,
is_input_object_type,
Expand All @@ -31,7 +33,7 @@
from ..execution.get_variable_signature import GraphQLVariableSignature
from ..execution.values import FragmentVariableValues, VariableValues
from ..language import ConstValueNode
from ..type import GraphQLArgument, GraphQLInputField
from ..type import GraphQLDefaultInput

__all__ = ["coerce_default_value", "coerce_input_literal", "coerce_input_value"]

Expand Down Expand Up @@ -351,16 +353,20 @@ def coerce_default_value(

:meta private:
"""
# The external default value is coerced; the result is memoized in a hidden
# field on the GraphQLDefaultInput object. (Contrary to GraphQL.js, which
# memoizes on the input value itself, this also works for the immutable
# variable signatures that reuse this function for fragment arguments.)
# It is only reused for the same type, as a default can be used with
# different types, e.g. after extend_schema() replaced the type.
# The external default value is coerced; like in GraphQL.js, the result is
# memoized in a hidden field on the input value itself, since the same default
# can be used with different types, e.g. after extend_schema() replaced the
# type. The immutable variable signatures that reuse this function for fragment
# arguments memoize it on their default instead, which is not shared.
default_input = input_value.default
if default_input is not None:
coerced_type, coerced_value = default_input._memoized_coercion # noqa: SLF001
if coerced_type is not input_value.type:
memo: GraphQLArgument | GraphQLInputField | GraphQLDefaultInput = (
input_value
if isinstance(input_value, (GraphQLArgument, GraphQLInputField))
else default_input
)
coerced_value = memo._memoized_coerced_value # noqa: SLF001
if coerced_value is Undefined:
coerced_value = (
coerce_input_literal(default_input.literal, input_value.type)
if default_input.literal is not None
Expand All @@ -377,7 +383,7 @@ def coerce_default_value(
f" to be valid, found: {found}."
)
raise TypeError(msg)
default_input._memoized_coercion = input_value.type, coerced_value # noqa: SLF001
memo._memoized_coerced_value = coerced_value # noqa: SLF001
return coerced_value

# The deprecated internal default value is used as is.
Expand Down
29 changes: 28 additions & 1 deletion tests/utilities/test_coerce_input_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ def coerce_input_value(value):
assert coerce_default_value(input_value) == "hello"
assert coerce_input_value_calls == ["hello"]

def memoizes_coercion_per_type():
def coerces_shared_default_per_type():
color_enum = GraphQLEnumType("Color", {"RED": 0, "BLUE": 1})
shared_default = GraphQLDefaultInput("RED")

Expand All @@ -727,3 +727,30 @@ def memoizes_coercion_per_type():
assert coerce_default_value(color_value) == 0
assert coerce_default_value(string_value) == "RED"
assert coerce_default_value(color_value) == 0

def memoizes_coercion_of_shared_default_per_input_value():
coerce_input_value_calls: list[Any] = []

def coerce_input_value(value):
coerce_input_value_calls.append(value)
return value

spy_scalar = GraphQLScalarType(
"SpyScalar", coerce_input_value=coerce_input_value
)
other_spy_scalar = GraphQLScalarType(
"SpyScalar", coerce_input_value=coerce_input_value
)

shared_default = GraphQLDefaultInput(literal=StringValueNode(value="hello"))

input_value = GraphQLInputField(spy_scalar, default=shared_default)
other_input_value = GraphQLInputField(other_spy_scalar, default=shared_default)

assert coerce_default_value(input_value) == "hello"
assert coerce_default_value(other_input_value) == "hello"

# Call a second time
assert coerce_default_value(input_value) == "hello"
assert coerce_default_value(other_input_value) == "hello"
assert coerce_input_value_calls == ["hello", "hello"]
Loading