Skip to content

Commit 0a01ce8

Browse files
committed
Fix introspection issue when using pickle (#173)
1 parent b0c6973 commit 0a01ce8

10 files changed

+243
-112
lines changed

src/graphql/type/definition.py

+17-23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations # Python < 3.10
22

3-
import warnings
43
from enum import Enum
54
from typing import (
65
TYPE_CHECKING,
@@ -232,6 +231,23 @@ class GraphQLNamedType(GraphQLType):
232231
ast_node: Optional[TypeDefinitionNode]
233232
extension_ast_nodes: Tuple[TypeExtensionNode, ...]
234233

234+
reserved_types: Dict[str, "GraphQLNamedType"] = {}
235+
236+
def __new__(cls, name: str, *_args: Any, **_kwargs: Any) -> "GraphQLNamedType":
237+
if name in cls.reserved_types:
238+
raise TypeError(f"Redefinition of reserved type {name!r}")
239+
return super().__new__(cls)
240+
241+
def __reduce__(self) -> Tuple[Callable, Tuple]:
242+
return self._get_instance, (self.name, tuple(self.to_kwargs().items()))
243+
244+
@classmethod
245+
def _get_instance(cls, name: str, args: Tuple) -> "GraphQLNamedType":
246+
try:
247+
return cls.reserved_types[name]
248+
except KeyError:
249+
return cls(**dict(args))
250+
235251
def __init__(
236252
self,
237253
name: str,
@@ -348,28 +364,6 @@ def serialize_odd(value: Any) -> int:
348364
ast_node: Optional[ScalarTypeDefinitionNode]
349365
extension_ast_nodes: Tuple[ScalarTypeExtensionNode, ...]
350366

351-
specified_types: Mapping[str, GraphQLScalarType] = {}
352-
353-
def __new__(cls, name: str, *args: Any, **kwargs: Any) -> "GraphQLScalarType":
354-
if name and name in cls.specified_types:
355-
warnings.warn(
356-
f"Redefinition of specified scalar type {name!r}",
357-
RuntimeWarning,
358-
stacklevel=2,
359-
)
360-
return cls.specified_types[name]
361-
return super().__new__(cls)
362-
363-
def __reduce__(self) -> Tuple[Callable, Tuple]:
364-
return self._get_instance, (self.name, tuple(self.to_kwargs().items()))
365-
366-
@classmethod
367-
def _get_instance(cls, name: str, args: Tuple) -> "GraphQLScalarType":
368-
try:
369-
return cls.specified_types[name]
370-
except KeyError:
371-
return cls(**dict(args))
372-
373367
def __init__(
374368
self,
375369
name: str,

src/graphql/type/introspection.py

+4
Original file line numberDiff line numberDiff line change
@@ -684,3 +684,7 @@ def type_name(_source, info, **_args):
684684
def is_introspection_type(type_: GraphQLNamedType) -> bool:
685685
"""Check whether the given named GraphQL type is an introspection type."""
686686
return type_.name in introspection_types
687+
688+
689+
# register the introspection types to avoid redefinition
690+
GraphQLNamedType.reserved_types.update(introspection_types)

src/graphql/type/scalars.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -322,5 +322,5 @@ def is_specified_scalar_type(type_: GraphQLNamedType) -> bool:
322322
return type_.name in specified_scalar_types
323323

324324

325-
# store the specified instances as class attribute to avoid redefinition
326-
GraphQLScalarType.specified_types = specified_scalar_types
325+
# register the scalar types to avoid redefinition
326+
GraphQLNamedType.reserved_types.update(specified_scalar_types)

src/graphql/utilities/build_client_schema.py

+27-19
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def build_scalar_def(
136136
) -> GraphQLScalarType:
137137
name = scalar_introspection["name"]
138138
try:
139-
return GraphQLScalarType.specified_types[name]
139+
return cast(GraphQLScalarType, GraphQLScalarType.reserved_types[name])
140140
except KeyError:
141141
return GraphQLScalarType(
142142
name=name,
@@ -165,12 +165,16 @@ def build_implementations_list(
165165
def build_object_def(
166166
object_introspection: IntrospectionObjectType,
167167
) -> GraphQLObjectType:
168-
return GraphQLObjectType(
169-
name=object_introspection["name"],
170-
description=object_introspection.get("description"),
171-
interfaces=lambda: build_implementations_list(object_introspection),
172-
fields=lambda: build_field_def_map(object_introspection),
173-
)
168+
name = object_introspection["name"]
169+
try:
170+
return cast(GraphQLObjectType, GraphQLObjectType.reserved_types[name])
171+
except KeyError:
172+
return GraphQLObjectType(
173+
name=name,
174+
description=object_introspection.get("description"),
175+
interfaces=lambda: build_implementations_list(object_introspection),
176+
fields=lambda: build_field_def_map(object_introspection),
177+
)
174178

175179
def build_interface_def(
176180
interface_introspection: IntrospectionInterfaceType,
@@ -204,18 +208,22 @@ def build_enum_def(enum_introspection: IntrospectionEnumType) -> GraphQLEnumType
204208
"Introspection result missing enumValues:"
205209
f" {inspect(enum_introspection)}."
206210
)
207-
return GraphQLEnumType(
208-
name=enum_introspection["name"],
209-
description=enum_introspection.get("description"),
210-
values={
211-
value_introspect["name"]: GraphQLEnumValue(
212-
value=value_introspect["name"],
213-
description=value_introspect.get("description"),
214-
deprecation_reason=value_introspect.get("deprecationReason"),
215-
)
216-
for value_introspect in enum_introspection["enumValues"]
217-
},
218-
)
211+
name = enum_introspection["name"]
212+
try:
213+
return cast(GraphQLEnumType, GraphQLEnumType.reserved_types[name])
214+
except KeyError:
215+
return GraphQLEnumType(
216+
name=name,
217+
description=enum_introspection.get("description"),
218+
values={
219+
value_introspect["name"]: GraphQLEnumValue(
220+
value=value_introspect["name"],
221+
description=value_introspect.get("description"),
222+
deprecation_reason=value_introspect.get("deprecationReason"),
223+
)
224+
for value_introspect in enum_introspection["enumValues"]
225+
},
226+
)
219227

220228
def build_input_object_def(
221229
input_object_introspection: IntrospectionInputObjectType,

tests/type/test_definition.py

+9
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
GraphQLScalarType,
4545
GraphQLString,
4646
GraphQLUnionType,
47+
introspection_types,
4748
)
4849

4950

@@ -1915,3 +1916,11 @@ def fields_have_repr():
19151916
repr(GraphQLField(GraphQLList(GraphQLInt)))
19161917
== "<GraphQLField <GraphQLList <GraphQLScalarType 'Int'>>>"
19171918
)
1919+
1920+
1921+
def describe_type_system_introspection_types():
1922+
def cannot_redefine_introspection_types():
1923+
for name, introspection_type in introspection_types.items():
1924+
assert introspection_type.name == name
1925+
with raises(TypeError, match=f"Redefinition of reserved type '{name}'"):
1926+
introspection_type.__class__(**introspection_type.to_kwargs())

tests/type/test_scalars.py

+11-26
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from math import inf, nan, pi
33
from typing import Any
44

5-
from pytest import raises, warns
5+
from pytest import raises
66

77
from graphql.error import GraphQLError
88
from graphql.language import parse_value as parse_value_to_ast
@@ -175,11 +175,8 @@ def serializes():
175175
assert str(exc_info.value) == "Int cannot represent non-integer value: [5]"
176176

177177
def cannot_be_redefined():
178-
with warns(
179-
RuntimeWarning, match="Redefinition of specified scalar type 'Int'"
180-
):
181-
redefined_int = GraphQLScalarType(name="Int")
182-
assert redefined_int == GraphQLInt
178+
with raises(TypeError, match="Redefinition of reserved type 'Int'"):
179+
GraphQLScalarType(name="Int")
183180

184181
def pickles():
185182
assert pickle.loads(pickle.dumps(GraphQLInt)) is GraphQLInt
@@ -308,11 +305,8 @@ def serializes():
308305
)
309306

310307
def cannot_be_redefined():
311-
with warns(
312-
RuntimeWarning, match="Redefinition of specified scalar type 'Float'"
313-
):
314-
redefined_float = GraphQLScalarType(name="Float")
315-
assert redefined_float == GraphQLFloat
308+
with raises(TypeError, match="Redefinition of reserved type 'Float'"):
309+
GraphQLScalarType(name="Float")
316310

317311
def pickles():
318312
assert pickle.loads(pickle.dumps(GraphQLFloat)) is GraphQLFloat
@@ -424,11 +418,8 @@ def __str__(self):
424418
)
425419

426420
def cannot_be_redefined():
427-
with warns(
428-
RuntimeWarning, match="Redefinition of specified scalar type 'String'"
429-
):
430-
redefined_string = GraphQLScalarType(name="String")
431-
assert redefined_string == GraphQLString
421+
with raises(TypeError, match="Redefinition of reserved type 'String'"):
422+
GraphQLScalarType(name="String")
432423

433424
def pickles():
434425
assert pickle.loads(pickle.dumps(GraphQLString)) is GraphQLString
@@ -576,11 +567,8 @@ def serializes():
576567
)
577568

578569
def cannot_be_redefined():
579-
with warns(
580-
RuntimeWarning, match="Redefinition of specified scalar type 'Boolean'"
581-
):
582-
redefined_boolean = GraphQLScalarType(name="Boolean")
583-
assert redefined_boolean == GraphQLBoolean
570+
with raises(TypeError, match="Redefinition of reserved type 'Boolean'"):
571+
GraphQLScalarType(name="Boolean")
584572

585573
def pickles():
586574
assert pickle.loads(pickle.dumps(GraphQLBoolean)) is GraphQLBoolean
@@ -707,11 +695,8 @@ def __str__(self):
707695
assert str(exc_info.value) == "ID cannot represent value: ['abc']"
708696

709697
def cannot_be_redefined():
710-
with warns(
711-
RuntimeWarning, match="Redefinition of specified scalar type 'ID'"
712-
):
713-
redefined_id = GraphQLScalarType(name="ID")
714-
assert redefined_id == GraphQLID
698+
with raises(TypeError, match="Redefinition of reserved type 'ID'"):
699+
GraphQLScalarType(name="ID")
715700

716701
def pickles():
717702
assert pickle.loads(pickle.dumps(GraphQLID)) is GraphQLID

tests/type/test_schema.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
GraphQLInt,
2121
GraphQLInterfaceType,
2222
GraphQLList,
23+
GraphQLNamedType,
2324
GraphQLObjectType,
2425
GraphQLScalarType,
2526
GraphQLSchema,
@@ -332,14 +333,14 @@ def check_that_query_mutation_and_subscription_are_graphql_types():
332333
def describe_a_schema_must_contain_uniquely_named_types():
333334
def rejects_a_schema_which_redefines_a_built_in_type():
334335
# temporarily allow redefinition of the String scalar type
335-
specified_types = GraphQLScalarType.specified_types
336-
GraphQLScalarType.specified_types = {}
336+
reserved_types = GraphQLNamedType.reserved_types
337+
GraphQLScalarType.reserved_types = {}
337338
try:
338339
# create a redefined String scalar type
339340
FakeString = GraphQLScalarType("String")
340341
finally:
341342
# protect from redefinition again
342-
GraphQLScalarType.specified_types = specified_types
343+
GraphQLScalarType.reserved_types = reserved_types
343344

344345
QueryType = GraphQLObjectType(
345346
"Query",

0 commit comments

Comments
 (0)