Skip to content

Commit 1a29650

Browse files
authored
Merge pull request #3 from healx/DOD-901-python-graphlayer-allow-sort-schema
[DOD 901] Add sort schema
2 parents 90f71ad + d84ab2e commit 1a29650

File tree

5 files changed

+287
-10
lines changed

5 files changed

+287
-10
lines changed

graphlayer/graphql/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
from .schema import create_graphql_schema
77

88

9-
def execute(document_text, *, graph, query_type, mutation_type=None, types=None, variables=None):
9+
def execute(document_text, *, graph, query_type, mutation_type=None, types=None, variables=None, sort_schema=False):
1010
return executor(
1111
query_type=query_type,
1212
mutation_type=mutation_type,
1313
types=types,
14+
sort_schema=sort_schema,
1415
)(document_text, graph=graph, variables=variables)
1516

1617

17-
def executor(*, query_type, mutation_type=None, types=None):
18-
graphql_schema = create_graphql_schema(query_type=query_type, mutation_type=mutation_type, types=types)
18+
def executor(*, query_type, mutation_type=None, types=None, sort_schema=False):
19+
graphql_schema = create_graphql_schema(
20+
query_type=query_type,
21+
mutation_type=mutation_type,
22+
types=types,
23+
sort_schema=sort_schema,
24+
)
1925

2026
def execute(document_text, *, graph, variables=None):
2127
try:

graphlayer/graphql/schema.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import graphql
2+
from graphql import lexicographic_sort_schema
23

34
from .. import iterables, schema
45
from .naming import snake_case_to_camel_case
@@ -12,7 +13,7 @@ def __init__(self, query_type, mutation_type, types, graphql_schema):
1213
self.graphql_schema = graphql_schema
1314

1415

15-
def create_graphql_schema(query_type, mutation_type, types=None):
16+
def create_graphql_schema(query_type, mutation_type=None, types=None, sort_schema=False):
1617
if types is None:
1718
types = ()
1819

@@ -118,15 +119,21 @@ def to_graphql_argument(param):
118119
for extra_type in types:
119120
to_graphql_type(extra_type)
120121

122+
graphql_schema = graphql.GraphQLSchema(
123+
query=graphql_query_type,
124+
mutation=graphql_mutation_type,
125+
types=tuple(map(_to_base_type, graphql_types.values())),
126+
)
127+
128+
if sort_schema:
129+
# Apply lexicographic sorting for consistent field ordering in documentation
130+
graphql_schema = lexicographic_sort_schema(graphql_schema)
131+
121132
return Schema(
122133
query_type=query_type,
123134
mutation_type=mutation_type,
124135
types=types,
125-
graphql_schema=graphql.GraphQLSchema(
126-
query=graphql_query_type,
127-
mutation=graphql_mutation_type,
128-
types=tuple(map(_to_base_type, graphql_types.values())),
129-
),
136+
graphql_schema=graphql_schema,
130137
)
131138

132139

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def read(fname):
99

1010
setup(
1111
name='graphlayer',
12-
version='0.2.8',
12+
version='0.3.0',
1313
description='High-performance library for implementing GraphQL APIs',
1414
long_description=read("README.rst"),
1515
author='Michael Williamson',

tests/graphql/test_graphql.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,78 @@ def is_success(*, data):
240240

241241
def has_str(matcher):
242242
return has_feature("str", str, matcher)
243+
244+
245+
def test_sorted_schema_still_resolves_data_correctly():
246+
"""Test that sorting the schema doesn't affect data resolution."""
247+
Root = g.ObjectType("Root", fields=(
248+
g.field("zebra", g.String),
249+
g.field("apple", g.String),
250+
g.field("mango", g.String),
251+
))
252+
253+
root_resolver = g.root_object_resolver(Root)
254+
255+
@root_resolver.field(Root.fields.zebra)
256+
def root_resolve_zebra(graph, query, args):
257+
return "zebra_value"
258+
259+
@root_resolver.field(Root.fields.apple)
260+
def root_resolve_apple(graph, query, args):
261+
return "apple_value"
262+
263+
@root_resolver.field(Root.fields.mango)
264+
def root_resolve_mango(graph, query, args):
265+
return "mango_value"
266+
267+
graph_definition = g.define_graph(resolvers=(root_resolver, ))
268+
graph = graph_definition.create_graph({})
269+
270+
query = """
271+
query {
272+
zebra
273+
apple
274+
mango
275+
}
276+
"""
277+
278+
execute = graphql.executor(query_type=Root, sort_schema=True)
279+
result = execute(graph=graph, document_text=query)
280+
281+
# Data should be resolved correctly regardless of sorting
282+
assert_that(result, is_success(data=equal_to({
283+
"zebra": "zebra_value",
284+
"apple": "apple_value",
285+
"mango": "mango_value",
286+
})))
287+
288+
289+
def test_sorted_schema_with_field_arguments():
290+
"""Test that sorted schema correctly handles fields with arguments."""
291+
Root = g.ObjectType("Root", fields=(
292+
g.field("greeting", g.String, params=(
293+
g.param("name", g.String),
294+
)),
295+
))
296+
297+
root_resolver = g.root_object_resolver(Root)
298+
299+
@root_resolver.field(Root.fields.greeting)
300+
def root_resolve_greeting(graph, query, args):
301+
return f"Hello, {args.name}!"
302+
303+
graph_definition = g.define_graph(resolvers=(root_resolver, ))
304+
graph = graph_definition.create_graph({})
305+
306+
query = """
307+
query {
308+
greeting(name: "World")
309+
}
310+
"""
311+
312+
execute = graphql.executor(query_type=Root, sort_schema=True)
313+
result = execute(graph=graph, document_text=query)
314+
315+
assert_that(result, is_success(data=equal_to({
316+
"greeting": "Hello, World!",
317+
})))
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""Tests for schema sorting functionality."""
2+
import enum
3+
4+
from precisely import assert_that, contains_exactly, equal_to
5+
6+
import graphlayer as g
7+
from graphlayer.graphql.schema import create_graphql_schema
8+
9+
10+
def test_fields_are_sorted_alphabetically_when_sort_schema_is_true():
11+
"""Test that object type fields are sorted alphabetically."""
12+
Root = g.ObjectType("Root", fields=(
13+
g.field("zebra", g.String),
14+
g.field("apple", g.String),
15+
g.field("mango", g.String),
16+
g.field("banana", g.String),
17+
))
18+
19+
schema = create_graphql_schema(query_type=Root, sort_schema=True)
20+
field_names = list(schema.graphql_schema.query_type.fields.keys())
21+
22+
assert_that(field_names, equal_to(["apple", "banana", "mango", "zebra"]))
23+
24+
25+
def test_fields_preserve_definition_order_when_sort_schema_is_false():
26+
"""Test that object type fields preserve definition order when not sorted."""
27+
Root = g.ObjectType("Root", fields=(
28+
g.field("zebra", g.String),
29+
g.field("apple", g.String),
30+
g.field("mango", g.String),
31+
g.field("banana", g.String),
32+
))
33+
34+
schema = create_graphql_schema(query_type=Root, sort_schema=False)
35+
field_names = list(schema.graphql_schema.query_type.fields.keys())
36+
37+
assert_that(field_names, equal_to(["zebra", "apple", "mango", "banana"]))
38+
39+
40+
def test_field_arguments_are_sorted_alphabetically_when_sort_schema_is_true():
41+
"""Test that field arguments are sorted alphabetically."""
42+
Root = g.ObjectType("Root", fields=(
43+
g.field("value", g.String, params=(
44+
g.param("zebra", g.Int),
45+
g.param("apple", g.Int),
46+
g.param("mango", g.Int),
47+
g.param("banana", g.Int),
48+
)),
49+
))
50+
51+
schema = create_graphql_schema(query_type=Root, sort_schema=True)
52+
arg_names = list(schema.graphql_schema.query_type.fields["value"].args.keys())
53+
54+
assert_that(arg_names, equal_to(["apple", "banana", "mango", "zebra"]))
55+
56+
57+
def test_enum_values_are_sorted_alphabetically_when_sort_schema_is_true():
58+
"""Test that enum values are sorted alphabetically."""
59+
class Color(enum.Enum):
60+
red = "RED"
61+
green = "GREEN"
62+
blue = "BLUE"
63+
yellow = "YELLOW"
64+
65+
ColorType = g.EnumType(Color)
66+
67+
Root = g.ObjectType("Root", fields=(
68+
g.field("color", ColorType),
69+
))
70+
71+
schema = create_graphql_schema(query_type=Root, sort_schema=True)
72+
73+
# Find the Color enum type in the schema
74+
color_enum_type = None
75+
for type_obj in schema.graphql_schema.type_map.values():
76+
if hasattr(type_obj, 'name') and type_obj.name == "Color":
77+
color_enum_type = type_obj
78+
break
79+
80+
assert color_enum_type is not None, "Color enum type not found in schema"
81+
82+
enum_value_names = list(color_enum_type.values.keys())
83+
assert_that(enum_value_names, equal_to(["BLUE", "GREEN", "RED", "YELLOW"]))
84+
85+
86+
def test_nested_type_fields_are_sorted_when_sort_schema_is_true():
87+
"""Test that nested object type fields are also sorted."""
88+
NestedType = g.ObjectType("Nested", fields=(
89+
g.field("zebra", g.String),
90+
g.field("apple", g.String),
91+
))
92+
93+
Root = g.ObjectType("Root", fields=(
94+
g.field("nested", NestedType),
95+
))
96+
97+
schema = create_graphql_schema(query_type=Root, sort_schema=True)
98+
99+
# Get the Nested type from the schema
100+
nested_type = schema.graphql_schema.type_map["Nested"]
101+
field_names = list(nested_type.fields.keys())
102+
103+
assert_that(field_names, equal_to(["apple", "zebra"]))
104+
105+
106+
def test_mutation_fields_are_sorted_when_sort_schema_is_true():
107+
"""Test that mutation type fields are sorted alphabetically."""
108+
Root = g.ObjectType("Root", fields=(
109+
g.field("value", g.String),
110+
))
111+
112+
Mutation = g.ObjectType("Mutation", fields=(
113+
g.field("updateZebra", g.String),
114+
g.field("createApple", g.String),
115+
g.field("deleteMango", g.String),
116+
))
117+
118+
schema = create_graphql_schema(
119+
query_type=Root,
120+
mutation_type=Mutation,
121+
sort_schema=True,
122+
)
123+
124+
mutation_field_names = list(schema.graphql_schema.mutation_type.fields.keys())
125+
assert_that(mutation_field_names, equal_to(["createApple", "deleteMango", "updateZebra"]))
126+
127+
128+
def test_input_object_fields_are_sorted_when_sort_schema_is_true():
129+
"""Test that input object type fields are sorted alphabetically."""
130+
InputType = g.InputObjectType("Input", fields=(
131+
g.input_field("zebra", g.String),
132+
g.input_field("apple", g.String),
133+
g.input_field("mango", g.String),
134+
))
135+
136+
Root = g.ObjectType("Root", fields=(
137+
g.field("process", g.String, params=(
138+
g.param("input", InputType),
139+
)),
140+
))
141+
142+
schema = create_graphql_schema(query_type=Root, sort_schema=True)
143+
144+
input_type = schema.graphql_schema.type_map["Input"]
145+
field_names = list(input_type.fields.keys())
146+
147+
assert_that(field_names, equal_to(["apple", "mango", "zebra"]))
148+
149+
150+
def test_sort_schema_defaults_to_false():
151+
"""Test that sort_schema defaults to False (preserves definition order)."""
152+
Root = g.ObjectType("Root", fields=(
153+
g.field("zebra", g.String),
154+
g.field("apple", g.String),
155+
))
156+
157+
# Call without sort_schema parameter
158+
schema = create_graphql_schema(query_type=Root)
159+
field_names = list(schema.graphql_schema.query_type.fields.keys())
160+
161+
# Should preserve definition order (not sorted)
162+
assert_that(field_names, equal_to(["zebra", "apple"]))
163+
164+
165+
def test_types_are_sorted_in_type_map_when_sort_schema_is_true():
166+
"""Test that type names in the type map are sorted alphabetically."""
167+
TypeZ = g.ObjectType("TypeZ", fields=(g.field("value", g.String),))
168+
TypeA = g.ObjectType("TypeA", fields=(g.field("value", g.String),))
169+
TypeM = g.ObjectType("TypeM", fields=(g.field("value", g.String),))
170+
171+
Root = g.ObjectType("Root", fields=(
172+
g.field("z", TypeZ),
173+
g.field("a", TypeA),
174+
g.field("m", TypeM),
175+
))
176+
177+
schema = create_graphql_schema(query_type=Root, sort_schema=True, types=(TypeZ, TypeA, TypeM))
178+
179+
# Filter out built-in types (those starting with __)
180+
custom_type_names = [
181+
name for name in schema.graphql_schema.type_map.keys()
182+
if not name.startswith("__")
183+
]
184+
185+
# Check that non-introspection types (built-ins and custom object types)
186+
# appear in sorted order: Boolean, Root, String, TypeA, TypeM, TypeZ
187+
assert_that(custom_type_names, contains_exactly(
188+
"Boolean", "Root", "String", "TypeA", "TypeM", "TypeZ"
189+
))

0 commit comments

Comments
 (0)