From 62574cc9570c5b256949f01cc94a60ed7ed1bbd6 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 6 Oct 2026 18:43:10 +0100 Subject: [PATCH] Only reuse memoized default values for the type they were coerced to --- src/graphql/type/definition.py | 8 +++--- src/graphql/utilities/coerce_input_value.py | 8 ++++-- tests/utilities/test_coerce_input_value.py | 11 ++++++++ tests/utilities/test_extend_schema.py | 31 ++++++++++++++++++++- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/graphql/type/definition.py b/src/graphql/type/definition.py index a2af7f94..5c24a861 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_coerced_value", "literal", "value" + __slots__ = "_memoized_coercion", "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 (see - # coerce_default_value() in the utilities). - self._memoized_coerced_value: Any = Undefined + # 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 def __eq__(self, other: object) -> bool: return self is other or ( diff --git a/src/graphql/utilities/coerce_input_value.py b/src/graphql/utilities/coerce_input_value.py index 02a509b8..07ac680f 100644 --- a/src/graphql/utilities/coerce_input_value.py +++ b/src/graphql/utilities/coerce_input_value.py @@ -355,10 +355,12 @@ def coerce_default_value( # 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. default_input = input_value.default if default_input is not None: - coerced_value = default_input._memoized_coerced_value # noqa: SLF001 - if coerced_value is Undefined: + coerced_type, coerced_value = default_input._memoized_coercion # noqa: SLF001 + if coerced_type is not input_value.type: coerced_value = ( coerce_input_literal(default_input.literal, input_value.type) if default_input.literal is not None @@ -375,7 +377,7 @@ def coerce_default_value( f" to be valid, found: {found}." ) raise TypeError(msg) - default_input._memoized_coerced_value = coerced_value # noqa: SLF001 + default_input._memoized_coercion = input_value.type, 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 0651b412..97f329fe 100644 --- a/tests/utilities/test_coerce_input_value.py +++ b/tests/utilities/test_coerce_input_value.py @@ -716,3 +716,14 @@ def coerce_input_value(value): # Call a second time assert coerce_default_value(input_value) == "hello" assert coerce_input_value_calls == ["hello"] + + def memoizes_coercion_per_type(): + color_enum = GraphQLEnumType("Color", {"RED": 0, "BLUE": 1}) + shared_default = GraphQLDefaultInput("RED") + + color_value = GraphQLInputField(color_enum, default=shared_default) + string_value = GraphQLInputField(GraphQLString, default=shared_default) + + assert coerce_default_value(color_value) == 0 + assert coerce_default_value(string_value) == "RED" + assert coerce_default_value(color_value) == 0 diff --git a/tests/utilities/test_extend_schema.py b/tests/utilities/test_extend_schema.py index 3e1d03b5..29394adf 100644 --- a/tests/utilities/test_extend_schema.py +++ b/tests/utilities/test_extend_schema.py @@ -4,7 +4,7 @@ import pytest -from graphql import graphql_sync +from graphql import ExecutionResult, graphql_sync from graphql.error import GraphQLSyntaxError from graphql.language import parse, print_ast from graphql.type import ( @@ -383,6 +383,35 @@ def extends_inputs_by_adding_new_fields(): ), ) + def coerces_default_values_with_extended_input_types(): + schema = build_schema( + """ + type Query { + someInput(arg: SomeInput = {}): String + } + + input SomeInput { + oldField: String + } + """ + ) + extend_ast = parse( + """ + extend input SomeInput { + newField: String = "new" + } + """ + ) + extended_schema = extend_schema(schema, extend_ast) + root_value = {"someInput": lambda _info, arg: str(arg)} + + def query(schema: GraphQLSchema) -> ExecutionResult: + return graphql_sync(schema, "{ someInput }", root_value) + + assert query(schema) == ({"someInput": "{}"}, None) + assert query(extended_schema) == ({"someInput": "{'newField': 'new'}"}, None) + assert query(schema) == ({"someInput": "{}"}, None) + def extends_scalars_by_adding_new_directives(): schema = build_schema( """