Skip to content

Commit bb02fcf

Browse files
committed
let graphql inherit all parse/validate/execute options
Replicates graphql/graphql-js@928a321
1 parent e32dda8 commit bb02fcf

2 files changed

Lines changed: 186 additions & 6 deletions

File tree

src/graphql/graphql.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717
)
1818

1919
if TYPE_CHECKING:
20-
from collections.abc import AsyncIterable, Awaitable, Callable
20+
from collections.abc import AsyncIterable, Awaitable, Callable, Collection
2121
from typing import TypeGuard
2222

2323
from .pyutils import AbortSignal, AwaitableOrValue
24+
from .validation import ASTValidationRule
2425

2526
__all__ = ["graphql", "graphql_sync"]
2627

2728

28-
async def graphql(
29+
async def graphql( # noqa: PLR0913
2930
schema: GraphQLSchema,
3031
source: str | Source,
3132
root_value: Any = None,
@@ -40,6 +41,11 @@ async def graphql(
4041
is_async_iterable: Callable[[Any], TypeGuard[AsyncIterable]] | None = None,
4142
hide_suggestions: bool = False,
4243
abort_signal: AbortSignal | None = None,
44+
no_location: bool = False,
45+
max_tokens: int | None = None,
46+
experimental_fragment_arguments: bool = False,
47+
rules: Collection[type[ASTValidationRule]] | None = None,
48+
max_errors: int | None = None,
4349
) -> ExecutionResult:
4450
"""Execute a GraphQL operation asynchronously.
4551
@@ -92,6 +98,17 @@ async def graphql(
9298
:arg abort_signal:
9399
A signal object that can be used to cancel execution, e.g. the signal of an
94100
:class:`~graphql.AbortController`
101+
:arg no_location:
102+
Parse option: do not include location information in the parsed document.
103+
:arg max_tokens:
104+
Parse option: the maximum number of tokens the document may contain.
105+
:arg experimental_fragment_arguments:
106+
Parse option: enable experimental support for fragment arguments.
107+
:arg rules:
108+
The validation rules to use when validating the query.
109+
If not provided, the specified rules are used.
110+
:arg max_errors:
111+
The maximum number of validation errors to report before stopping.
95112
"""
96113
# Always return asynchronously for a consistent API.
97114
result = graphql_impl(
@@ -109,6 +126,11 @@ async def graphql(
109126
is_async_iterable,
110127
hide_suggestions,
111128
abort_signal,
129+
no_location,
130+
max_tokens,
131+
experimental_fragment_arguments,
132+
rules,
133+
max_errors,
112134
)
113135

114136
if default_is_awaitable(result):
@@ -127,7 +149,7 @@ def assume_not_async_iterable(_value: Any) -> TypeGuard[AsyncIterable]:
127149
return False
128150

129151

130-
def graphql_sync(
152+
def graphql_sync( # noqa: PLR0913
131153
schema: GraphQLSchema,
132154
source: str | Source,
133155
root_value: Any = None,
@@ -141,6 +163,11 @@ def graphql_sync(
141163
check_sync: bool = False,
142164
hide_suggestions: bool = False,
143165
abort_signal: AbortSignal | None = None,
166+
no_location: bool = False,
167+
max_tokens: int | None = None,
168+
experimental_fragment_arguments: bool = False,
169+
rules: Collection[type[ASTValidationRule]] | None = None,
170+
max_errors: int | None = None,
144171
) -> ExecutionResult:
145172
"""Execute a GraphQL operation synchronously.
146173
@@ -172,6 +199,11 @@ def graphql_sync(
172199
is_async_iterable,
173200
hide_suggestions,
174201
abort_signal,
202+
no_location,
203+
max_tokens,
204+
experimental_fragment_arguments,
205+
rules,
206+
max_errors,
175207
)
176208

177209
# Assert that the execution was synchronous.
@@ -183,7 +215,7 @@ def graphql_sync(
183215
return cast("ExecutionResult", result)
184216

185217

186-
def graphql_impl(
218+
def graphql_impl( # noqa: PLR0913
187219
schema: GraphQLSchema,
188220
source: str | Source,
189221
root_value: Any,
@@ -198,6 +230,11 @@ def graphql_impl(
198230
is_async_iterable: Callable[[Any], TypeGuard[AsyncIterable]] | None = None,
199231
hide_suggestions: bool = False,
200232
abort_signal: AbortSignal | None = None,
233+
no_location: bool = False,
234+
max_tokens: int | None = None,
235+
experimental_fragment_arguments: bool = False,
236+
rules: Collection[type[ASTValidationRule]] | None = None,
237+
max_errors: int | None = None,
201238
) -> AwaitableOrValue[ExecutionResult]:
202239
"""Execute a query, return asynchronously only if necessary."""
203240
# Validate Schema
@@ -206,15 +243,20 @@ def graphql_impl(
206243

207244
# Parse
208245
try:
209-
document = parse(source)
246+
document = parse(
247+
source,
248+
no_location=no_location,
249+
max_tokens=max_tokens,
250+
experimental_fragment_arguments=experimental_fragment_arguments,
251+
)
210252
except GraphQLError as error:
211253
return ExecutionResult(data=None, errors=[error])
212254

213255
# Validate
214256
from .validation import validate
215257

216258
if validation_errors := validate(
217-
schema, document, hide_suggestions=hide_suggestions
259+
schema, document, rules, max_errors, hide_suggestions=hide_suggestions
218260
):
219261
return ExecutionResult(data=None, errors=validation_errors)
220262

tests/test_graphql.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from asyncio import sleep
2+
3+
import pytest
4+
5+
from graphql import graphql, graphql_sync
6+
from graphql.error import GraphQLError
7+
from graphql.language import Source
8+
from graphql.type import (
9+
GraphQLField,
10+
GraphQLObjectType,
11+
GraphQLSchema,
12+
GraphQLString,
13+
)
14+
from graphql.validation import ValidationRule
15+
16+
from .fixtures import cleanup
17+
18+
pytestmark = [
19+
pytest.mark.anyio,
20+
pytest.mark.filterwarnings("ignore:coroutine .* was never awaited:RuntimeWarning"),
21+
]
22+
23+
24+
def _resolve_context_echo(_source, info):
25+
return str(info.context)
26+
27+
28+
def _resolve_root_value(root_value, _info):
29+
return root_value
30+
31+
32+
async def _resolve_root_value_async(root_value, _info):
33+
return root_value
34+
35+
36+
schema = GraphQLSchema(
37+
query=GraphQLObjectType(
38+
"Query",
39+
{
40+
"a": GraphQLField(GraphQLString, resolve=lambda *_args: "A"),
41+
"b": GraphQLField(GraphQLString, resolve=lambda *_args: "B"),
42+
"contextEcho": GraphQLField(GraphQLString, resolve=_resolve_context_echo),
43+
"syncField": GraphQLField(GraphQLString, resolve=_resolve_root_value),
44+
"asyncField": GraphQLField(
45+
GraphQLString, resolve=_resolve_root_value_async
46+
),
47+
},
48+
)
49+
)
50+
51+
52+
def describe_graphql():
53+
async def passes_source_through_to_parse():
54+
source = Source("{", "custom-query.graphql")
55+
56+
result = await graphql(schema, source)
57+
58+
assert result.errors
59+
assert result.errors[0].source is not None
60+
assert result.errors[0].source.name == "custom-query.graphql"
61+
62+
async def passes_rules_through_to_validate():
63+
class CustomRule(ValidationRule):
64+
def enter_field(self, node, *_args):
65+
self.context.report_error(GraphQLError("custom rule error", node))
66+
67+
result = await graphql(schema, "{ a }", rules=[CustomRule])
68+
69+
assert result.errors
70+
assert result.errors[0].message == "custom rule error"
71+
72+
async def passes_parse_options_through_to_parse():
73+
class CustomRule(ValidationRule):
74+
def enter_operation_definition(self, node, *_args):
75+
message = "no location" if node.loc is None else "has location"
76+
self.context.report_error(GraphQLError(message, node))
77+
78+
result = await graphql(
79+
schema, "{ a }", no_location=True, rules=[CustomRule]
80+
)
81+
82+
assert result.errors
83+
assert result.errors[0].message == "no location"
84+
85+
async def passes_validation_options_through_to_validate():
86+
result = await graphql(schema, "{ contextEho }", hide_suggestions=True)
87+
88+
assert result.errors
89+
assert (
90+
result.errors[0].message
91+
== "Cannot query field 'contextEho' on type 'Query'."
92+
)
93+
94+
async def passes_execution_args_through_to_execute():
95+
result = await graphql(
96+
schema,
97+
"""
98+
query First {
99+
a
100+
}
101+
102+
query Second {
103+
b
104+
}
105+
""",
106+
operation_name="Second",
107+
)
108+
109+
assert result == ({"b": "B"}, None)
110+
111+
async def returns_schema_validation_errors():
112+
bad_schema = GraphQLSchema()
113+
result = await graphql(bad_schema, "{ __typename }")
114+
115+
assert result.errors
116+
assert result.errors[0].message == "Query root type must be provided."
117+
118+
119+
def describe_graphql_sync():
120+
def returns_result_for_synchronous_execution():
121+
result = graphql_sync(schema, "{ syncField }", root_value="rootValue")
122+
123+
assert result == ({"syncField": "rootValue"}, None)
124+
125+
async def throws_for_asynchronous_execution():
126+
# Unlike graphql-js, graphql_sync only detects asynchronous execution when
127+
# check_sync is set; without it, the coroutine is treated as a plain value.
128+
with pytest.raises(RuntimeError) as exc_info:
129+
graphql_sync(
130+
schema, "{ asyncField }", root_value="rootValue", check_sync=True
131+
)
132+
assert (
133+
str(exc_info.value)
134+
== "GraphQL execution failed to complete synchronously."
135+
)
136+
del exc_info
137+
await sleep(0) # let the loop process the cancelled execution task
138+
cleanup()

0 commit comments

Comments
 (0)