Skip to content

Commit 6f324d8

Browse files
committed
Schema: keep order of user provided types
Replicates graphql/graphql-js@68a0818
1 parent bafb41e commit 6f324d8

File tree

7 files changed

+177
-201
lines changed

7 files changed

+177
-201
lines changed

docs/diffs.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,4 @@ Registering special types for descriptions
7878
Normally, descriptions for GraphQL types must be strings. However, sometimes you may want to use other kinds of objects which are not strings, but are only resolved to strings at runtime. This is possible if you register the classes of such objects with :func:`pyutils.register_description`.
7979

8080

81-
Overridable type map reducer
82-
----------------------------
83-
84-
It is possible to override the :meth:`GraphQLSchema.type_map_reducer` method with a custom implementation. This is used by Graphene to convert its own types to the GraphQL-core types.
85-
86-
8781
If you notice any other important differences, please let us know so that they can be either removed or listed here.

src/graphql/type/schema.py

Lines changed: 108 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from functools import reduce
21
from typing import (
32
Any,
43
Collection,
@@ -25,7 +24,6 @@
2524
get_named_type,
2625
is_input_object_type,
2726
is_interface_type,
28-
is_named_type,
2927
is_object_type,
3028
is_union_type,
3129
)
@@ -100,7 +98,7 @@ class GraphQLSchema:
10098
ast_node: Optional[ast.SchemaDefinitionNode]
10199
extension_ast_nodes: Optional[FrozenList[ast.SchemaExtensionNode]]
102100

103-
_implementations: Dict[str, InterfaceImplementations]
101+
_implementations_map: Dict[str, InterfaceImplementations]
104102
_sub_type_map: Dict[str, Set[str]]
105103
_validation_errors: Optional[List[GraphQLError]]
106104

@@ -119,18 +117,12 @@ def __init__(
119117
"""Initialize GraphQL schema.
120118
121119
If this schema was built from a source known to be valid, then it may be marked
122-
with `assume_valid` to avoid an additional type system validation. Otherwise
123-
check for common mistakes during construction to produce clear and early error
124-
messages.
120+
with `assume_valid` to avoid an additional type system validation.
125121
"""
126-
# If this schema was built from a source known to be valid, then it may be
127-
# marked with assume_valid to avoid an additional type system validation.
128122
self._validation_errors = [] if assume_valid else None
129123

130124
# Check for common mistakes during construction to produce clear and early
131-
# error messages.
132-
# The query, mutation and subscription types must actually be GraphQL
133-
# object types, but we leave it to the validator to report this error.
125+
# error messages, but we leave the specific tests for the validation.
134126
if query and not isinstance(query, GraphQLType):
135127
raise TypeError("Expected query to be a GraphQL type.")
136128
if mutation and not isinstance(mutation, GraphQLType):
@@ -140,28 +132,16 @@ def __init__(
140132
if types is None:
141133
types = []
142134
else:
143-
if not is_collection(types) or (
144-
# if reducer has been overridden, don't check types
145-
getattr(self.type_map_reducer, "__func__", None)
146-
is GraphQLSchema.type_map_reducer
147-
and not all(is_named_type(type_) for type_ in types)
135+
if not is_collection(types) or not all(
136+
isinstance(type_, GraphQLType) for type_ in types
148137
):
149138
raise TypeError(
150-
"Schema types must be specified as a collection"
151-
" of GraphQLNamedType instances."
139+
"Schema types must be specified as a collection of GraphQL types."
152140
)
153141
if directives is not None:
154142
# noinspection PyUnresolvedReferences
155-
if not is_collection(directives) or (
156-
# if reducer has been overridden, don't check directive types
157-
getattr(self.type_map_directive_reducer, "__func__", None)
158-
is GraphQLSchema.type_map_directive_reducer
159-
and not all(is_directive(directive) for directive in directives)
160-
):
161-
raise TypeError(
162-
"Schema directives must be specified as a collection"
163-
" of GraphQLDirective instances."
164-
)
143+
if not is_collection(directives):
144+
raise TypeError("Schema directives must be a collection.")
165145
if not isinstance(directives, FrozenList):
166146
directives = FrozenList(directives)
167147
if extensions is not None and (
@@ -201,29 +181,85 @@ def __init__(
201181
else cast(FrozenList[GraphQLDirective], directives)
202182
)
203183

204-
# Build type map now to detect any errors within this schema.
205-
initial_types: List[Optional[GraphQLNamedType]] = [
206-
query,
207-
mutation,
208-
subscription,
209-
introspection_types["__Schema"],
210-
]
184+
# To preserve order of user-provided types, we add first to add them to
185+
# the set of "collected" types, so `collect_referenced_types` ignore them.
211186
if types:
212-
initial_types.extend(types)
187+
all_referenced_types = TypeSet.with_initial_types(types)
188+
collect_referenced_types = all_referenced_types.collect_referenced_types
189+
for type_ in types:
190+
# When we are ready to process this type, we remove it from "collected"
191+
# types and then add it together with all dependent types in the correct
192+
# position.
193+
del all_referenced_types[type_]
194+
collect_referenced_types(type_)
195+
else:
196+
all_referenced_types = TypeSet()
197+
collect_referenced_types = all_referenced_types.collect_referenced_types
198+
199+
if query:
200+
collect_referenced_types(query)
201+
if mutation:
202+
collect_referenced_types(mutation)
203+
if subscription:
204+
collect_referenced_types(subscription)
205+
206+
for directive in self.directives:
207+
# Directives are not validated until validate_schema() is called.
208+
if is_directive(directive):
209+
for arg in directive.args.values():
210+
collect_referenced_types(arg.type)
211+
collect_referenced_types(introspection_types["__Schema"])
213212

214-
# Keep track of all types referenced within the schema.
213+
# Storing the resulting map for reference by the schema.
215214
type_map: TypeMap = {}
216-
# First by deeply visiting all initial types.
217-
type_map = reduce(self.type_map_reducer, initial_types, type_map)
218-
# Then by deeply visiting all directive types.
219-
type_map = reduce(self.type_map_directive_reducer, self.directives, type_map)
220-
# Storing the resulting map for reference by the schema
221215
self.type_map = type_map
222216

223217
self._sub_type_map = {}
224218

225219
# Keep track of all implementations by interface name.
226-
self._implementations = collect_implementations(type_map.values())
220+
implementations_map: Dict[str, InterfaceImplementations] = {}
221+
self._implementations_map = implementations_map
222+
223+
for named_type in all_referenced_types:
224+
if not named_type:
225+
continue
226+
227+
type_name = getattr(named_type, "name", None)
228+
if type_name in type_map:
229+
raise TypeError(
230+
"Schema must contain uniquely named types"
231+
f" but contains multiple types named '{type_name}'."
232+
)
233+
type_map[type_name] = named_type
234+
235+
if is_interface_type(named_type):
236+
named_type = cast(GraphQLInterfaceType, named_type)
237+
# Store implementations by interface.
238+
for iface in named_type.interfaces:
239+
if is_interface_type(iface):
240+
iface = cast(GraphQLInterfaceType, iface)
241+
if iface.name in implementations_map:
242+
implementations = implementations_map[iface.name]
243+
else:
244+
implementations = implementations_map[
245+
iface.name
246+
] = InterfaceImplementations(objects=[], interfaces=[])
247+
248+
implementations.interfaces.append(named_type)
249+
elif is_object_type(named_type):
250+
named_type = cast(GraphQLObjectType, named_type)
251+
# Store implementations by objects.
252+
for iface in named_type.interfaces:
253+
if is_interface_type(iface):
254+
iface = cast(GraphQLInterfaceType, iface)
255+
if iface.name in implementations_map:
256+
implementations = implementations_map[iface.name]
257+
else:
258+
implementations = implementations_map[
259+
iface.name
260+
] = InterfaceImplementations(objects=[], interfaces=[])
261+
262+
implementations.objects.append(named_type)
227263

228264
def to_kwargs(self) -> Dict[str, Any]:
229265
return dict(
@@ -256,7 +292,9 @@ def get_possible_types(
256292
def get_implementations(
257293
self, interface_type: GraphQLInterfaceType
258294
) -> InterfaceImplementations:
259-
return self._implementations[interface_type.name]
295+
return self._implementations_map.get(
296+
interface_type.name, InterfaceImplementations(objects=[], interfaces=[])
297+
)
260298

261299
def is_possible_type(
262300
self, abstract_type: GraphQLAbstractType, possible_type: GraphQLObjectType
@@ -301,100 +339,43 @@ def get_directive(self, name: str) -> Optional[GraphQLDirective]:
301339
def validation_errors(self):
302340
return self._validation_errors
303341

304-
def type_map_reducer(
305-
self, map_: TypeMap, type_: Optional[GraphQLNamedType] = None
306-
) -> TypeMap:
307-
"""Reducer function for creating the type map from given types."""
308-
if not type_:
309-
return map_
310342

343+
class TypeSet(Dict[GraphQLNamedType, None]):
344+
"""An ordered set of types that can be collected starting from initial types."""
345+
346+
@classmethod
347+
def with_initial_types(cls, types: Collection[GraphQLType]) -> "TypeSet":
348+
return cast(TypeSet, super().fromkeys(types))
349+
350+
def collect_referenced_types(self, type_: GraphQLType) -> None:
351+
"""Recursive function supplementing the type starting from an initial type."""
311352
named_type = get_named_type(type_)
312-
try:
313-
name = named_type.name
314-
except AttributeError:
315-
# this is how GraphQL.js handles the case
316-
name = None # type: ignore
317-
318-
if name in map_:
319-
if map_[name] is not named_type:
320-
raise TypeError(
321-
"Schema must contain uniquely named types but contains multiple"
322-
f" types named {name!r}."
323-
)
324-
return map_
325-
map_[name] = named_type
326353

354+
if named_type in self:
355+
return
356+
357+
self[named_type] = None
358+
359+
collect_referenced_types = self.collect_referenced_types
327360
if is_union_type(named_type):
328361
named_type = cast(GraphQLUnionType, named_type)
329-
map_ = reduce(self.type_map_reducer, named_type.types, map_)
330-
362+
for member_type in named_type.types:
363+
collect_referenced_types(member_type)
331364
elif is_object_type(named_type) or is_interface_type(named_type):
332365
named_type = cast(
333366
Union[GraphQLObjectType, GraphQLInterfaceType], named_type
334367
)
335-
map_ = reduce(self.type_map_reducer, named_type.interfaces, map_)
336-
for field in cast(GraphQLInterfaceType, named_type).fields.values():
337-
types = [arg.type for arg in field.args.values()]
338-
map_ = reduce(self.type_map_reducer, types, map_)
339-
map_ = self.type_map_reducer(map_, field.type)
368+
for interface_type in named_type.interfaces:
369+
collect_referenced_types(interface_type)
340370

371+
for field in named_type.fields.values():
372+
collect_referenced_types(field.type)
373+
for arg in field.args.values():
374+
collect_referenced_types(arg.type)
341375
elif is_input_object_type(named_type):
342-
for field in cast(GraphQLInputObjectType, named_type).fields.values():
343-
map_ = self.type_map_reducer(map_, field.type)
344-
345-
return map_
346-
347-
def type_map_directive_reducer(
348-
self, map_: TypeMap, directive: Optional[GraphQLDirective] = None
349-
) -> TypeMap:
350-
"""Reducer function for creating the type map from given directives."""
351-
# Directives are not validated until validate_schema() is called.
352-
if not is_directive(directive):
353-
return map_ # pragma: no cover
354-
directive = cast(GraphQLDirective, directive)
355-
return reduce(
356-
lambda prev_map, arg: self.type_map_reducer(
357-
prev_map, cast(GraphQLNamedType, arg.type)
358-
),
359-
directive.args.values(),
360-
map_,
361-
)
362-
363-
364-
def collect_implementations(
365-
types: Collection[GraphQLNamedType],
366-
) -> Dict[str, InterfaceImplementations]:
367-
implementations_map: Dict[str, InterfaceImplementations] = {}
368-
for type_ in types:
369-
if is_interface_type(type_):
370-
type_ = cast(GraphQLInterfaceType, type_)
371-
if type_.name not in implementations_map:
372-
implementations_map[type_.name] = InterfaceImplementations(
373-
objects=[], interfaces=[]
374-
)
375-
# Store implementations by interface.
376-
for interface in type_.interfaces:
377-
if is_interface_type(interface):
378-
implementations = implementations_map.get(interface.name)
379-
if implementations is None:
380-
implementations_map[interface.name] = InterfaceImplementations(
381-
objects=[], interfaces=[type_]
382-
)
383-
else:
384-
implementations.interfaces.append(type_)
385-
elif is_object_type(type_):
386-
type_ = cast(GraphQLObjectType, type_)
387-
# Store implementations by objects.
388-
for interface in type_.interfaces:
389-
if is_interface_type(interface):
390-
implementations = implementations_map.get(interface.name)
391-
if implementations is None:
392-
implementations_map[interface.name] = InterfaceImplementations(
393-
objects=[type_], interfaces=[]
394-
)
395-
else:
396-
implementations.objects.append(type_)
397-
return implementations_map
376+
named_type = cast(GraphQLInputObjectType, named_type)
377+
for field in named_type.fields.values():
378+
collect_referenced_types(field.type)
398379

399380

400381
def is_schema(schema: Any) -> bool:

tests/execution/test_union_interface.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ def can_introspect_on_union_and_intersection_types():
190190
"fields": [{"name": "name"}],
191191
"interfaces": [],
192192
"possibleTypes": [
193-
{"name": "Person"},
194193
{"name": "Dog"},
195194
{"name": "Cat"},
195+
{"name": "Person"},
196196
],
197197
"enumValues": None,
198198
"inputFields": None,
@@ -207,9 +207,9 @@ def can_introspect_on_union_and_intersection_types():
207207
],
208208
"interfaces": [{"name": "Life"}],
209209
"possibleTypes": [
210-
{"name": "Person"},
211210
{"name": "Dog"},
212211
{"name": "Cat"},
212+
{"name": "Person"},
213213
],
214214
"enumValues": None,
215215
"inputFields": None,

tests/test_star_wars_introspection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ def allows_querying_the_schema_for_types():
2929
assert data == {
3030
"__schema": {
3131
"types": [
32-
{"name": "Query"},
33-
{"name": "Episode"},
32+
{"name": "Human"},
3433
{"name": "Character"},
3534
{"name": "String"},
36-
{"name": "Human"},
35+
{"name": "Episode"},
3736
{"name": "Droid"},
37+
{"name": "Query"},
38+
{"name": "Boolean"},
3839
{"name": "__Schema"},
3940
{"name": "__Type"},
4041
{"name": "__TypeKind"},
41-
{"name": "Boolean"},
4242
{"name": "__Field"},
4343
{"name": "__InputValue"},
4444
{"name": "__EnumValue"},

0 commit comments

Comments
 (0)