Skip to content

Commit 27e0eb7

Browse files
committed
Another refactor to allow multiple operations in documents
1 parent 3201c05 commit 27e0eb7

File tree

6 files changed

+299
-103
lines changed

6 files changed

+299
-103
lines changed

docs/advanced/dsl_module.rst

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ The following code:
1111
ds = DSLSchema(StarWarsSchema)
1212
1313
query = dsl_gql(
14-
ds.Query.hero.select(
15-
ds.Character.id,
16-
ds.Character.name,
17-
ds.Character.friends.select(ds.Character.name),
14+
DSLQuery(
15+
ds.Query.hero.select(
16+
ds.Character.id,
17+
ds.Character.name,
18+
ds.Character.friends.select(ds.Character.name),
19+
)
1820
)
1921
)
2022
@@ -81,18 +83,34 @@ As you can select children fields of any object type, you can construct your com
8183
ds.Character.friends.select(ds.Character.name),
8284
)
8385

84-
Once your query is completed and you have selected all the fields you want,
85-
use the :func:`dsl_gql <gql.dsl.dsl_gql>` function to convert your query into
86-
a document which will be able to get executed in the client or a session::
86+
Once your root query fields are defined, you can put them in an operation using
87+
:class:`DSLQuery <gql.dsl.DSLQuery>`,
88+
:class:`DSLMutation <gql.dsl.DSLMutation>` or
89+
:class:`DSLSubscription <gql.dsl.DSLSubscription>`::
8790

88-
query = dsl_gql(
91+
DSLQuery(
8992
ds.Query.hero.select(
9093
ds.Character.id,
9194
ds.Character.name,
9295
ds.Character.friends.select(ds.Character.name),
9396
)
9497
)
9598

99+
100+
Once your operations are defined,
101+
use the :func:`dsl_gql <gql.dsl.dsl_gql>` function to convert your operations into
102+
a document which will be able to get executed in the client or a session::
103+
104+
query = dsl_gql(
105+
DSLQuery(
106+
ds.Query.hero.select(
107+
ds.Character.id,
108+
ds.Character.name,
109+
ds.Character.friends.select(ds.Character.name),
110+
)
111+
)
112+
)
113+
96114
result = client.execute(query)
97115

98116
Arguments
@@ -107,36 +125,91 @@ It can also be done using the :meth:`args <gql.dsl.DSLField.args>` method::
107125

108126
ds.Query.human.args(id="1000").select(ds.Human.name)
109127

110-
Alias
111-
^^^^^
128+
Aliases
129+
^^^^^^^
112130

113131
You can set an alias of a field using the :meth:`alias <gql.dsl.DSLField.alias>` method::
114132

115133
ds.Query.human.args(id=1000).alias("luke").select(ds.Character.name)
116134

135+
It is also possible to set the alias directly using keyword arguments of an operation::
136+
137+
DSLQuery(
138+
luke=ds.Query.human.args(id=1000).select(ds.Character.name)
139+
)
140+
141+
Or using keyword arguments in the :meth:`select <gql.dsl.DSLField.select>` method::
142+
143+
ds.Query.hero.select(
144+
my_name=ds.Character.name
145+
)
146+
117147
Mutations
118148
^^^^^^^^^
119149

120-
It works the same way for mutations. Example::
150+
For the mutations, you need to start from root fields starting from :code:`ds.Mutation`
151+
then you need to create the GraphQL operation using the class
152+
:class:`DSLMutation <gql.dsl.DSLMutation>`. Example::
121153

122154
query = dsl_gql(
123-
ds.Mutation.createReview.args(
124-
episode=6, review={"stars": 5, "commentary": "This is a great movie!"}
125-
).select(ds.Review.stars, ds.Review.commentary)
155+
DSLMutation(
156+
ds.Mutation.createReview.args(
157+
episode=6, review={"stars": 5, "commentary": "This is a great movie!"}
158+
).select(ds.Review.stars, ds.Review.commentary)
159+
)
126160
)
127161

128-
Multiple requests
129-
^^^^^^^^^^^^^^^^^
162+
Subscriptions
163+
^^^^^^^^^^^^^
130164

131-
It is possible to create a document with multiple requests::
165+
For the subscriptions, you need to start from root fields starting from :code:`ds.Subscription`
166+
then you need to create the GraphQL operation using the class
167+
:class:`DSLSubscription <gql.dsl.DSLSubscription>`. Example::
132168

133169
query = dsl_gql(
170+
DSLSubscription(
171+
ds.Subscription.reviewAdded(episode=6).select(ds.Review.stars, ds.Review.commentary)
172+
)
173+
)
174+
175+
Multiple fields in an operation
176+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
177+
178+
It is possible to create an operation with multiple fields::
179+
180+
DSLQuery(
134181
ds.Query.hero.select(ds.Character.name),
135-
ds.Query.hero(episode=5).alias("hero_of_episode_5").select(ds.Character.name),
182+
hero_of_episode_5=ds.Query.hero(episode=5).select(ds.Character.name),
183+
)
184+
185+
Operation name
186+
^^^^^^^^^^^^^^
187+
188+
You can set the operation name of an operation using a keyword argument
189+
to :func:`dsl_gql <gql.dsl.dsl_gql>`::
190+
191+
query = dsl_gql(
192+
GetHeroName=DSLQuery(ds.Query.hero.select(ds.Character.name))
136193
)
137194

138-
But you have to take care that the root type is always the same. It is not possible
139-
to mix queries and mutations for example.
195+
will generate the request::
196+
197+
query GetHeroName {
198+
hero {
199+
name
200+
}
201+
}
202+
203+
Multiple operations in a document
204+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
205+
206+
It is possible to create an Document with multiple operations::
207+
208+
query = dsl_gql(
209+
operation_name_1=DSLQuery( ... ),
210+
operation_name_2=DSLQuery( ... ),
211+
operation_name_3=DSLMutation( ... ),
212+
)
140213

141214
Executable examples
142215
-------------------

docs/code_examples/aiohttp_async_dsl.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncio
22

33
from gql import Client
4-
from gql.dsl import DSLSchema, dsl_gql
4+
from gql.dsl import DSLQuery, DSLSchema, dsl_gql
55
from gql.transport.aiohttp import AIOHTTPTransport
66

77

@@ -22,8 +22,10 @@ async def main():
2222

2323
# Create the query using dynamically generated attributes from ds
2424
query = dsl_gql(
25-
ds.Query.continents(filter={"code": {"eq": "EU"}}).select(
26-
ds.Continent.code, ds.Continent.name
25+
DSLQuery(
26+
ds.Query.continents(filter={"code": {"eq": "EU"}}).select(
27+
ds.Continent.code, ds.Continent.name
28+
)
2729
)
2830
)
2931

@@ -43,7 +45,7 @@ async def main():
4345
query_continents.select(ds.Continent.name)
4446

4547
# I generate a document from my query to be able to execute it
46-
query = dsl_gql(query_continents)
48+
query = dsl_gql(DSLQuery(query_continents))
4749

4850
# Execute the query
4951
result = await session.execute(query)

docs/code_examples/requests_sync_dsl.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from gql import Client
2-
from gql.dsl import DSLSchema, dsl_gql
2+
from gql.dsl import DSLQuery, DSLSchema, dsl_gql
33
from gql.transport.requests import RequestsHTTPTransport
44

55
transport = RequestsHTTPTransport(
@@ -21,7 +21,9 @@
2121
ds = DSLSchema(client.schema)
2222

2323
# Create the query using dynamically generated attributes from ds
24-
query = dsl_gql(ds.Query.continents.select(ds.Continent.code, ds.Continent.name))
24+
query = dsl_gql(
25+
DSLQuery(ds.Query.continents.select(ds.Continent.code, ds.Continent.name))
26+
)
2527

2628
result = session.execute(query)
2729
print(result)

gql/dsl.py

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from abc import ABC
23
from typing import Dict, Iterable, List, Optional, Tuple, Union
34

45
from graphql import (
@@ -26,70 +27,59 @@
2627

2728

2829
def dsl_gql(
29-
*fields: "DSLField",
30-
operation_name: Optional[str] = None,
31-
**fields_with_alias: "DSLField",
30+
*operations: "DSLOperation", **operations_with_name: "DSLOperation"
3231
) -> DocumentNode:
33-
r"""Given arguments of type :class:`DSLField` containing GraphQL requests,
32+
r"""Given arguments instances of :class:`DSLOperation`
33+
containing GraphQL operations,
3434
generate a Document which can be executed later in a
3535
gql client or a gql session.
3636
3737
Similar to the :func:`gql.gql` function but instead of parsing a python
38-
string to describe the request, we are using requests which have been generated
38+
string to describe the request, we are using operations which have been generated
3939
dynamically using instances of :class:`DSLField`, generated
4040
by instances of :class:`DSLType` which themselves originated from
4141
a :class:`DSLSchema` class.
4242
43-
The fields arguments should be fields of root GraphQL types
44-
(Query, Mutation or Subscription).
43+
:param \*operations: the GraphQL operations
44+
:type \*operations: DSLOperation (DSLQuery, DSLMutation, DSLSubscription)
45+
:param \**operations_with_name: the GraphQL operations with an operation name
46+
:type \**operations_with_name: DSLOperation (DSLQuery, DSLMutation, DSLSubscription)
4547
46-
They should all have the same root type
47-
(you can't mix queries with mutations for example).
48-
49-
:param \*fields: root instances of the dynamically generated requests
50-
:type \*fields: DSLField
51-
:param \**fields_with_alias: root instances fields with alias as key
52-
:type \**fields_with_alias: DSLField
53-
:param operation_name: optional operation name
54-
:type operation_name: str
5548
:return: a Document which can be later executed or subscribed by a
5649
:class:`Client <gql.client.Client>`, by an
5750
:class:`async session <gql.client.AsyncClientSession>` or by a
5851
:class:`sync session <gql.client.SyncClientSession>`
5952
60-
:raises TypeError: if an argument is not an instance of :class:`DSLField`
61-
:raises AssertionError: if an argument is not a field of a root type
53+
:raises TypeError: if an argument is not an instance of :class:`DSLOperation`
6254
"""
6355

64-
all_fields: Tuple["DSLField", ...] = DSLField.get_aliased_fields(
65-
fields, fields_with_alias
56+
# Concatenate operations without and with name
57+
all_operations: Tuple["DSLOperation", ...] = (
58+
*operations,
59+
*(operation for operation in operations_with_name.values()),
6660
)
6761

68-
# Check that we receive only arguments of type DSLField
69-
# And that they are a root type
70-
for field in all_fields:
71-
if not isinstance(field, DSLField):
62+
# Set the operation name
63+
for name, operation in operations_with_name.items():
64+
operation.name = name
65+
66+
# Check the type
67+
for operation in all_operations:
68+
if not isinstance(operation, DSLOperation):
7269
raise TypeError(
73-
f"fields must be instances of DSLField. Received type: {type(field)}"
70+
"Operations should be instances of DSLOperation "
71+
"(DSLQuery, DSLMutation or DSLSubscription).\n"
72+
f"Received: {type(operation)}."
7473
)
75-
assert field.type_name in ["Query", "Mutation", "Subscription"], (
76-
"fields should be root types (Query, Mutation or Subscription)\n"
77-
f"Received: {field.type_name}"
78-
)
79-
80-
# Get the operation from the first field
81-
# All the fields must have the same operation
82-
operation = all_fields[0].type_name.lower()
8374

8475
return DocumentNode(
8576
definitions=[
8677
OperationDefinitionNode(
87-
operation=OperationType(operation),
88-
selection_set=SelectionSetNode(
89-
selections=FrozenList(DSLField.get_ast_fields(all_fields))
90-
),
91-
**({"name": NameNode(value=operation_name)} if operation_name else {}),
78+
operation=OperationType(operation.operation_type),
79+
selection_set=operation.selection_set,
80+
**({"name": NameNode(value=operation.name)} if operation.name else {}),
9281
)
82+
for operation in all_operations
9383
]
9484
)
9585

@@ -133,6 +123,77 @@ def __getattr__(self, name: str) -> "DSLType":
133123
return DSLType(type_def)
134124

135125

126+
class DSLOperation(ABC):
127+
"""Interface for GraphQL operations.
128+
129+
Inherited by
130+
:class:`DSLQuery <gql.dsl.DSLQuery>`,
131+
:class:`DSLMutation <gql.dsl.DSLMutation>` and
132+
:class:`DSLSubscription <gql.dsl.DSLSubscription>`
133+
"""
134+
135+
operation_type: OperationType
136+
137+
def __init__(
138+
self, *fields: "DSLField", **fields_with_alias: "DSLField",
139+
):
140+
r"""Given arguments of type :class:`DSLField` containing GraphQL requests,
141+
generate an operation which can be converted to a Document
142+
using the :func:`dsl_gql <gql.dsl.dsl_gql>`.
143+
144+
The fields arguments should be fields of root GraphQL types
145+
(Query, Mutation or Subscription) and correspond to the
146+
operation_type of this operation.
147+
148+
:param \*fields: root instances of the dynamically generated requests
149+
:type \*fields: DSLField
150+
:param \**fields_with_alias: root instances fields with alias as key
151+
:type \**fields_with_alias: DSLField
152+
153+
:raises TypeError: if an argument is not an instance of :class:`DSLField`
154+
:raises AssertionError: if an argument is not a field which correspond
155+
to the operation type
156+
"""
157+
158+
self.name: Optional[str] = None
159+
160+
# Concatenate fields without and with alias
161+
all_fields: Tuple["DSLField", ...] = DSLField.get_aliased_fields(
162+
fields, fields_with_alias
163+
)
164+
165+
# Check that we receive only arguments of type DSLField
166+
# And that the root type correspond to the operation
167+
for field in all_fields:
168+
if not isinstance(field, DSLField):
169+
raise TypeError(
170+
(
171+
"fields must be instances of DSLField. "
172+
f"Received type: {type(field)}"
173+
)
174+
)
175+
assert field.type_name.upper() == self.operation_type.name, (
176+
f"Invalid root field for operation {self.operation_type.name}.\n"
177+
f"Received: {field.type_name}"
178+
)
179+
180+
self.selection_set: SelectionSetNode = SelectionSetNode(
181+
selections=FrozenList(DSLField.get_ast_fields(all_fields))
182+
)
183+
184+
185+
class DSLQuery(DSLOperation):
186+
operation_type = OperationType.QUERY
187+
188+
189+
class DSLMutation(DSLOperation):
190+
operation_type = OperationType.MUTATION
191+
192+
193+
class DSLSubscription(DSLOperation):
194+
operation_type = OperationType.SUBSCRIPTION
195+
196+
136197
class DSLType:
137198
"""The DSLType represents a GraphQL type for the DSL code.
138199
@@ -270,6 +331,7 @@ def select(
270331
of the :class:`DSLField` class.
271332
"""
272333

334+
# Concatenate fields without and with alias
273335
added_fields: Tuple["DSLField", ...] = self.get_aliased_fields(
274336
fields, fields_with_alias
275337
)

0 commit comments

Comments
 (0)