diff --git a/src/graphql/type/definition.py b/src/graphql/type/definition.py index 5c24a861..37254d73 100644 --- a/src/graphql/type/definition.py +++ b/src/graphql/type/definition.py @@ -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 ( @@ -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, @@ -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, diff --git a/src/graphql/utilities/coerce_input_value.py b/src/graphql/utilities/coerce_input_value.py index 07ac680f..afb66d96 100644 --- a/src/graphql/utilities/coerce_input_value.py +++ b/src/graphql/utilities/coerce_input_value.py @@ -18,6 +18,8 @@ is_iterable, ) from ..type import ( + GraphQLArgument, + GraphQLInputField, GraphQLInputType, assert_leaf_type, is_input_object_type, @@ -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"] @@ -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 @@ -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. diff --git a/tests/utilities/test_coerce_input_value.py b/tests/utilities/test_coerce_input_value.py index 97f329fe..a91ba5d3 100644 --- a/tests/utilities/test_coerce_input_value.py +++ b/tests/utilities/test_coerce_input_value.py @@ -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") @@ -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"]