Skip to content

Commit 09e4b30

Browse files
authored
Add skip and include directive in introspection schema (#279)
1 parent 2be6aaa commit 09e4b30

File tree

4 files changed

+162
-2
lines changed

4 files changed

+162
-2
lines changed

gql/client.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
ExecutionResult,
88
GraphQLSchema,
99
build_ast_schema,
10-
build_client_schema,
1110
get_introspection_query,
1211
parse,
1312
validate,
@@ -17,6 +16,7 @@
1716
from .transport.exceptions import TransportQueryError
1817
from .transport.local_schema import LocalSchemaTransport
1918
from .transport.transport import Transport
19+
from .utilities import build_client_schema
2020
from .utilities import parse_result as parse_result_fn
2121
from .utilities import serialize_variable_values
2222

gql/utilities/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from .build_client_schema import build_client_schema
12
from .get_introspection_query_ast import get_introspection_query_ast
23
from .parse_result import parse_result
34
from .serialize_variable_values import serialize_value, serialize_variable_values
45
from .update_schema_enum import update_schema_enum
56
from .update_schema_scalars import update_schema_scalar, update_schema_scalars
67

78
__all__ = [
9+
"build_client_schema",
810
"parse_result",
911
"get_introspection_query_ast",
1012
"serialize_variable_values",

gql/utilities/build_client_schema.py

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import Dict
2+
3+
from graphql import GraphQLSchema
4+
from graphql import build_client_schema as build_client_schema_orig
5+
from graphql.pyutils import inspect
6+
7+
__all__ = ["build_client_schema"]
8+
9+
10+
INCLUDE_DIRECTIVE_JSON = {
11+
"name": "include",
12+
"description": (
13+
"Directs the executor to include this field or fragment "
14+
"only when the `if` argument is true."
15+
),
16+
"locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
17+
"args": [
18+
{
19+
"name": "if",
20+
"description": "Included when true.",
21+
"type": {
22+
"kind": "NON_NULL",
23+
"name": "None",
24+
"ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": "None"},
25+
},
26+
"defaultValue": "None",
27+
}
28+
],
29+
}
30+
31+
SKIP_DIRECTIVE_JSON = {
32+
"name": "skip",
33+
"description": (
34+
"Directs the executor to skip this field or fragment "
35+
"when the `if` argument is true."
36+
),
37+
"locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
38+
"args": [
39+
{
40+
"name": "if",
41+
"description": "Skipped when true.",
42+
"type": {
43+
"kind": "NON_NULL",
44+
"name": "None",
45+
"ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": "None"},
46+
},
47+
"defaultValue": "None",
48+
}
49+
],
50+
}
51+
52+
53+
def build_client_schema(introspection: Dict) -> GraphQLSchema:
54+
"""This is an alternative to the graphql-core function
55+
:code:`build_client_schema` but with default include and skip directives
56+
added to the schema to fix
57+
`issue #278 <https://github.com/graphql-python/gql/issues/278>`_
58+
59+
.. warning::
60+
This function will be removed once the issue
61+
`graphql-js#3419 <https://github.com/graphql/graphql-js/issues/3419>`_
62+
has been fixed and ported to graphql-core so don't use it
63+
outside gql.
64+
"""
65+
66+
if not isinstance(introspection, dict) or not isinstance(
67+
introspection.get("__schema"), dict
68+
):
69+
raise TypeError(
70+
"Invalid or incomplete introspection result. Ensure that you"
71+
" are passing the 'data' attribute of an introspection response"
72+
f" and no 'errors' were returned alongside: {inspect(introspection)}."
73+
)
74+
75+
schema_introspection = introspection["__schema"]
76+
77+
directives = schema_introspection.get("directives", None)
78+
79+
if directives is None:
80+
directives = []
81+
schema_introspection["directives"] = directives
82+
83+
if not any(directive["name"] == "skip" for directive in directives):
84+
directives.append(SKIP_DIRECTIVE_JSON)
85+
86+
if not any(directive["name"] == "include" for directive in directives):
87+
directives.append(INCLUDE_DIRECTIVE_JSON)
88+
89+
return build_client_schema_orig(introspection, assume_valid=False)

tests/starwars/test_validation.py

+70-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,35 @@ def introspection_schema():
6060
return Client(introspection=StarWarsIntrospection)
6161

6262

63-
@pytest.fixture(params=["local_schema", "typedef_schema", "introspection_schema"])
63+
@pytest.fixture
64+
def introspection_schema_empty_directives():
65+
introspection = StarWarsIntrospection
66+
67+
# Simulate an empty dictionary for directives
68+
introspection["__schema"]["directives"] = []
69+
70+
return Client(introspection=introspection)
71+
72+
73+
@pytest.fixture
74+
def introspection_schema_no_directives():
75+
introspection = StarWarsIntrospection
76+
77+
# Simulate no directives key
78+
del introspection["__schema"]["directives"]
79+
80+
return Client(introspection=introspection)
81+
82+
83+
@pytest.fixture(
84+
params=[
85+
"local_schema",
86+
"typedef_schema",
87+
"introspection_schema",
88+
"introspection_schema_empty_directives",
89+
"introspection_schema_no_directives",
90+
]
91+
)
6492
def client(request):
6593
return request.getfixturevalue(request.param)
6694

@@ -187,3 +215,44 @@ def test_allows_object_fields_in_inline_fragments(client):
187215
}
188216
"""
189217
assert not validation_errors(client, query)
218+
219+
220+
def test_include_directive(client):
221+
query = """
222+
query fetchHero($with_friends: Boolean!) {
223+
hero {
224+
name
225+
friends @include(if: $with_friends) {
226+
name
227+
}
228+
}
229+
}
230+
"""
231+
assert not validation_errors(client, query)
232+
233+
234+
def test_skip_directive(client):
235+
query = """
236+
query fetchHero($without_friends: Boolean!) {
237+
hero {
238+
name
239+
friends @skip(if: $without_friends) {
240+
name
241+
}
242+
}
243+
}
244+
"""
245+
assert not validation_errors(client, query)
246+
247+
248+
def test_build_client_schema_invalid_introspection():
249+
from gql.utilities import build_client_schema
250+
251+
with pytest.raises(TypeError) as exc_info:
252+
build_client_schema("blah")
253+
254+
assert (
255+
"Invalid or incomplete introspection result. Ensure that you are passing the "
256+
"'data' attribute of an introspection response and no 'errors' were returned "
257+
"alongside: 'blah'."
258+
) in str(exc_info.value)

0 commit comments

Comments
 (0)