graphql-codegen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphql_codegen/__init__.py +7 -0
- graphql_codegen/__main__.py +5 -0
- graphql_codegen/_cli/__init__.py +41 -0
- graphql_codegen/_cli/_graphql_config.py +291 -0
- graphql_codegen/_cli/_introspection.py +87 -0
- graphql_codegen/_cli/_introspection_graphql.py +53 -0
- graphql_codegen/_cli/_parsing.py +67 -0
- graphql_codegen/_cli/_schema_pointer.py +108 -0
- graphql_codegen/_cli/_source.py +68 -0
- graphql_codegen/_cli/_write.py +22 -0
- graphql_codegen/_generator/__init__.py +0 -0
- graphql_codegen/_generator/_annotation.py +149 -0
- graphql_codegen/_generator/_ast_nodes.py +235 -0
- graphql_codegen/_generator/_data_type.py +532 -0
- graphql_codegen/_generator/_document.py +108 -0
- graphql_codegen/_generator/_document_module.py +54 -0
- graphql_codegen/_generator/_imports.py +166 -0
- graphql_codegen/_generator/_injector.py +267 -0
- graphql_codegen/_generator/_merge.py +149 -0
- graphql_codegen/_generator/_naming.py +80 -0
- graphql_codegen/_generator/_operation.py +223 -0
- graphql_codegen/_generator/_scalar.py +76 -0
- graphql_codegen/_generator/_schema.py +46 -0
- graphql_codegen/_generator/_schema_type.py +334 -0
- graphql_codegen/_generator/_selection.py +246 -0
- graphql_codegen/_generator/_structs.py +87 -0
- graphql_codegen/_generator/_typed_dict.py +154 -0
- graphql_codegen/_generator/dotted_name.py +48 -0
- graphql_codegen/_generator/package.py +695 -0
- graphql_codegen/_generator/spelling.py +168 -0
- graphql_codegen/_metadata.py +11 -0
- graphql_codegen/_note.py +12 -0
- graphql_codegen/config.py +42 -0
- graphql_codegen/document_sibling_module.py +58 -0
- graphql_codegen/generate.py +19 -0
- graphql_codegen/package_location.py +41 -0
- graphql_codegen/py.typed +0 -0
- graphql_codegen/runtime/__init__.py +20 -0
- graphql_codegen/runtime/_compat.py +27 -0
- graphql_codegen/runtime/_literal.py +18 -0
- graphql_codegen/runtime/_merge.py +153 -0
- graphql_codegen/runtime/_prepare.py +250 -0
- graphql_codegen/runtime/_reflection.py +376 -0
- graphql_codegen/runtime/_sigil.py +11 -0
- graphql_codegen/runtime/_transport.py +21 -0
- graphql_codegen/runtime/client.py +384 -0
- graphql_codegen/runtime/error.py +135 -0
- graphql_codegen/runtime/injection.py +107 -0
- graphql_codegen/runtime/operation.py +135 -0
- graphql_codegen/scalar.py +59 -0
- graphql_codegen-0.1.0.dist-info/METADATA +871 -0
- graphql_codegen-0.1.0.dist-info/RECORD +55 -0
- graphql_codegen-0.1.0.dist-info/WHEEL +4 -0
- graphql_codegen-0.1.0.dist-info/entry_points.txt +3 -0
- graphql_codegen-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from functools import cache
|
|
5
|
+
from typing import Final, final
|
|
6
|
+
|
|
7
|
+
from graphql import (
|
|
8
|
+
GraphQLCompositeType,
|
|
9
|
+
GraphQLObjectType,
|
|
10
|
+
GraphQLSchema,
|
|
11
|
+
is_non_null_type,
|
|
12
|
+
is_type_sub_type_of,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from graphql_codegen._generator._annotation import AnnotationBuilder
|
|
16
|
+
from graphql_codegen._generator._ast_nodes import (
|
|
17
|
+
constant,
|
|
18
|
+
documented,
|
|
19
|
+
name,
|
|
20
|
+
optional,
|
|
21
|
+
qualified,
|
|
22
|
+
subscript,
|
|
23
|
+
type_alias,
|
|
24
|
+
union,
|
|
25
|
+
)
|
|
26
|
+
from graphql_codegen._generator._naming import (
|
|
27
|
+
COMPAT,
|
|
28
|
+
INPUT,
|
|
29
|
+
REFLECTION,
|
|
30
|
+
TYPING,
|
|
31
|
+
SchemaSpellings,
|
|
32
|
+
nested_type_name,
|
|
33
|
+
)
|
|
34
|
+
from graphql_codegen._generator._schema import as_non_null
|
|
35
|
+
from graphql_codegen._generator._selection import (
|
|
36
|
+
SelectedField,
|
|
37
|
+
SelectionSet,
|
|
38
|
+
TypeCondition,
|
|
39
|
+
)
|
|
40
|
+
from graphql_codegen._generator._structs import StructTypes, get_struct_input_name
|
|
41
|
+
from graphql_codegen._generator._typed_dict import Key, emit_data
|
|
42
|
+
from graphql_codegen._generator.spelling import (
|
|
43
|
+
PendingSpelling,
|
|
44
|
+
SettledSpelling,
|
|
45
|
+
Spelling,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@final
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class _DeclaredKey:
|
|
52
|
+
field: SelectedField
|
|
53
|
+
declaring_fragment: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _merge_selection_sets(
|
|
57
|
+
selection_sets: Sequence[SelectionSet], /, *, type_: GraphQLCompositeType
|
|
58
|
+
) -> SelectionSet:
|
|
59
|
+
return SelectionSet(
|
|
60
|
+
type=type_,
|
|
61
|
+
fields=tuple(
|
|
62
|
+
field for selection_set in selection_sets for field in selection_set.fields
|
|
63
|
+
),
|
|
64
|
+
conditions=tuple(
|
|
65
|
+
condition
|
|
66
|
+
for selection_set in selection_sets
|
|
67
|
+
for condition in selection_set.conditions
|
|
68
|
+
),
|
|
69
|
+
fragment_names=tuple(
|
|
70
|
+
dict.fromkeys(
|
|
71
|
+
fragment_name
|
|
72
|
+
for selection_set in selection_sets
|
|
73
|
+
for fragment_name in selection_set.fragment_names
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
description=next(
|
|
77
|
+
(
|
|
78
|
+
selection_set.description
|
|
79
|
+
for selection_set in selection_sets
|
|
80
|
+
if selection_set.description
|
|
81
|
+
),
|
|
82
|
+
"",
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _merge_fields(fields: Sequence[SelectedField], /) -> tuple[SelectedField, ...]:
|
|
88
|
+
"""Validation guarantees that fields sharing a response name are the same field with the same arguments, so only their selection sets and client directives need combining."""
|
|
89
|
+
by_response_name: dict[str, list[SelectedField]] = {}
|
|
90
|
+
|
|
91
|
+
for field in fields:
|
|
92
|
+
by_response_name.setdefault(field.response_name, []).append(field)
|
|
93
|
+
|
|
94
|
+
return tuple(
|
|
95
|
+
group[0] if len(group) == 1 else _merge_field_group(group)
|
|
96
|
+
for group in by_response_name.values()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _merge_field_group(group: Sequence[SelectedField], /) -> SelectedField:
|
|
101
|
+
# Validation guarantees that the fields are all leaves or all select on the same type.
|
|
102
|
+
selection_sets = [
|
|
103
|
+
field.selection_set for field in group if field.selection_set is not None
|
|
104
|
+
]
|
|
105
|
+
return replace(
|
|
106
|
+
group[0],
|
|
107
|
+
directive_names=frozenset().union(*(field.directive_names for field in group)),
|
|
108
|
+
selection_set=_merge_selection_sets(
|
|
109
|
+
selection_sets, type_=selection_sets[0].type
|
|
110
|
+
)
|
|
111
|
+
if selection_sets
|
|
112
|
+
else None,
|
|
113
|
+
# Present as soon as one of the fields is.
|
|
114
|
+
skippable=all(field.skippable for field in group),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@final
|
|
119
|
+
class DataTypeEmitter:
|
|
120
|
+
"""Turn resolved selection sets into TypedDicts, in class syntax, so that a fragment spread can be a base class.
|
|
121
|
+
|
|
122
|
+
Inheritance is what keeps a type name stable: adding a field next to a spread changes the class body, never the name.
|
|
123
|
+
A fragment's own class is open, since an operation spreading it extends it, and every other type is closed, since a response holds exactly the keys its operation selects.
|
|
124
|
+
Types are emitted depth first, children before parents, so a name is always defined before it is used as a base class.
|
|
125
|
+
One emitter serves a whole package, so that each fragment is normalized once, whichever modules spread it.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
*,
|
|
131
|
+
schema: GraphQLSchema,
|
|
132
|
+
annotations: AnnotationBuilder,
|
|
133
|
+
schema_spellings: SchemaSpellings,
|
|
134
|
+
resolve_fragment: Callable[[str], SelectionSet],
|
|
135
|
+
struct_types: StructTypes | None,
|
|
136
|
+
non_null_directive_name: str | None,
|
|
137
|
+
) -> None:
|
|
138
|
+
self._schema: Final = schema
|
|
139
|
+
self._annotations: Final = annotations
|
|
140
|
+
self._schema_spellings: Final = schema_spellings
|
|
141
|
+
self._resolve_fragment: Final = resolve_fragment
|
|
142
|
+
self._struct_types: Final = struct_types
|
|
143
|
+
self._non_null_directive_name: Final = non_null_directive_name
|
|
144
|
+
# Cached for the emitter's life: one serves a whole package.
|
|
145
|
+
self._normalized_fragment: Final = cache(self._normalize_fragment)
|
|
146
|
+
self._declared_keys: Final = cache(self._find_declared_keys)
|
|
147
|
+
|
|
148
|
+
def emit(
|
|
149
|
+
self,
|
|
150
|
+
selection_set: SelectionSet,
|
|
151
|
+
/,
|
|
152
|
+
*,
|
|
153
|
+
root_spelling: SettledSpelling,
|
|
154
|
+
fragment_reference: Callable[[str], ast.expr],
|
|
155
|
+
inheritable: bool,
|
|
156
|
+
) -> list[ast.stmt]:
|
|
157
|
+
"""The outermost type is public and every nested one private, its spelling pending, so that a path spelled like another gets a suffix rather than clashing."""
|
|
158
|
+
return self._emit_node(
|
|
159
|
+
selection_set,
|
|
160
|
+
spelling=root_spelling,
|
|
161
|
+
path=(root_spelling,),
|
|
162
|
+
fragment_reference=fragment_reference,
|
|
163
|
+
inheritable=inheritable,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _emit_node(
|
|
167
|
+
self,
|
|
168
|
+
selection_set: SelectionSet,
|
|
169
|
+
/,
|
|
170
|
+
*,
|
|
171
|
+
spelling: Spelling,
|
|
172
|
+
path: Sequence[str],
|
|
173
|
+
fragment_reference: Callable[[str], ast.expr],
|
|
174
|
+
inheritable: bool,
|
|
175
|
+
) -> list[ast.stmt]:
|
|
176
|
+
"""A class's keys are required, since GraphQL guarantees that a selected field is present in the response, unless `@skip` or `@include` may leave it out, nullability living in the annotation."""
|
|
177
|
+
return self._emit_normalized(
|
|
178
|
+
self._normalize(selection_set),
|
|
179
|
+
spelling=spelling,
|
|
180
|
+
path=path,
|
|
181
|
+
fragment_reference=fragment_reference,
|
|
182
|
+
inheritable=inheritable,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _emit_normalized(
|
|
186
|
+
self,
|
|
187
|
+
normalized: SelectionSet,
|
|
188
|
+
/,
|
|
189
|
+
*,
|
|
190
|
+
spelling: Spelling,
|
|
191
|
+
path: Sequence[str],
|
|
192
|
+
fragment_reference: Callable[[str], ast.expr],
|
|
193
|
+
inheritable: bool,
|
|
194
|
+
) -> list[ast.stmt]:
|
|
195
|
+
if normalized.conditions:
|
|
196
|
+
return self._emit_union(
|
|
197
|
+
normalized,
|
|
198
|
+
spelling=spelling,
|
|
199
|
+
path=path,
|
|
200
|
+
fragment_reference=fragment_reference,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
statements: list[ast.stmt] = []
|
|
204
|
+
keys: list[Key] = []
|
|
205
|
+
|
|
206
|
+
for field in normalized.fields:
|
|
207
|
+
if field.name == "__typename":
|
|
208
|
+
keys.append(
|
|
209
|
+
Key(
|
|
210
|
+
name="__typename",
|
|
211
|
+
annotation=_typename_annotation(
|
|
212
|
+
self._possible_types(normalized.type)
|
|
213
|
+
),
|
|
214
|
+
required=not field.skippable,
|
|
215
|
+
description="",
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
child_statements, annotation = self._emit_field(
|
|
221
|
+
field, path=path, fragment_reference=fragment_reference
|
|
222
|
+
)
|
|
223
|
+
statements.extend(child_statements)
|
|
224
|
+
keys.append(
|
|
225
|
+
Key(
|
|
226
|
+
name=field.response_name,
|
|
227
|
+
annotation=annotation,
|
|
228
|
+
required=not field.skippable,
|
|
229
|
+
description=field.description,
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
statements.extend(
|
|
234
|
+
emit_data(
|
|
235
|
+
spelling,
|
|
236
|
+
keys,
|
|
237
|
+
bases=[
|
|
238
|
+
fragment_reference(fragment_name)
|
|
239
|
+
for fragment_name in normalized.fragment_names
|
|
240
|
+
],
|
|
241
|
+
closed=not inheritable,
|
|
242
|
+
functional_base_spelling=PendingSpelling(
|
|
243
|
+
f"{nested_type_name(path)}_Base"
|
|
244
|
+
),
|
|
245
|
+
description=normalized.description,
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
return statements
|
|
249
|
+
|
|
250
|
+
def _emit_union(
|
|
251
|
+
self,
|
|
252
|
+
selection_set: SelectionSet,
|
|
253
|
+
/,
|
|
254
|
+
*,
|
|
255
|
+
spelling: Spelling,
|
|
256
|
+
path: Sequence[str],
|
|
257
|
+
fragment_reference: Callable[[str], ast.expr],
|
|
258
|
+
) -> list[ast.stmt]:
|
|
259
|
+
"""Emit *selection_set*, which has conditions, as a union with one member per concrete type, discriminated by `__typename`, which the document selects on every abstract type.
|
|
260
|
+
|
|
261
|
+
Each member gathers the fields of every condition its type satisfies, so conditions nested in conditions need no case of their own.
|
|
262
|
+
"""
|
|
263
|
+
statements: list[ast.stmt] = []
|
|
264
|
+
members: list[PendingSpelling] = []
|
|
265
|
+
|
|
266
|
+
for object_type in self._possible_types(selection_set.type):
|
|
267
|
+
member_path = (*path, object_type.name)
|
|
268
|
+
member = PendingSpelling(nested_type_name(member_path))
|
|
269
|
+
members.append(member)
|
|
270
|
+
statements.extend(
|
|
271
|
+
self._emit_node(
|
|
272
|
+
self._selection_on(selection_set, object_type),
|
|
273
|
+
spelling=member,
|
|
274
|
+
path=member_path,
|
|
275
|
+
fragment_reference=fragment_reference,
|
|
276
|
+
inheritable=False,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
statements.extend(
|
|
281
|
+
documented(
|
|
282
|
+
type_alias(spelling, union(*(name(member) for member in members))),
|
|
283
|
+
text=selection_set.description,
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
return statements
|
|
287
|
+
|
|
288
|
+
def _selection_on(
|
|
289
|
+
self, selection_set: SelectionSet, object_type: GraphQLObjectType, /
|
|
290
|
+
) -> SelectionSet:
|
|
291
|
+
gathered = [selection_set]
|
|
292
|
+
|
|
293
|
+
for condition in selection_set.conditions:
|
|
294
|
+
if is_type_sub_type_of(self._schema, object_type, condition.type):
|
|
295
|
+
branch = self._selection_on(condition.selection_set, object_type)
|
|
296
|
+
gathered.append(
|
|
297
|
+
self._as_skippable(branch, object_type=object_type)
|
|
298
|
+
if condition.skippable
|
|
299
|
+
else branch
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
merged = _merge_selection_sets(
|
|
303
|
+
[replace(part, conditions=()) for part in gathered], type_=object_type
|
|
304
|
+
)
|
|
305
|
+
return replace(merged, description=object_type.description or "")
|
|
306
|
+
|
|
307
|
+
def _as_skippable(
|
|
308
|
+
self, selection_set: SelectionSet, /, *, object_type: GraphQLObjectType
|
|
309
|
+
) -> SelectionSet:
|
|
310
|
+
"""Its fragments are expanded, since inheriting one would declare its keys present."""
|
|
311
|
+
fields = [replace(field, skippable=True) for field in selection_set.fields]
|
|
312
|
+
|
|
313
|
+
for fragment_name in selection_set.fragment_names:
|
|
314
|
+
fragment = self._selection_on(
|
|
315
|
+
self._resolve_fragment(fragment_name), object_type
|
|
316
|
+
)
|
|
317
|
+
fields.extend(self._as_skippable(fragment, object_type=object_type).fields)
|
|
318
|
+
|
|
319
|
+
return replace(selection_set, fields=tuple(fields), fragment_names=())
|
|
320
|
+
|
|
321
|
+
def _normalize(self, selection_set: SelectionSet, /) -> SelectionSet:
|
|
322
|
+
"""Merge fields by key, and expand every spread that cannot soundly be inherited, which is always correct, merely less reused.
|
|
323
|
+
|
|
324
|
+
A fragment can soundly be inherited when its own type is a class rather than a union, and every key it shares with the rest of the node is the very same one, declared by the same fragment or as the same leaf.
|
|
325
|
+
"""
|
|
326
|
+
fields = list(selection_set.fields)
|
|
327
|
+
conditions: list[TypeCondition] = list(selection_set.conditions)
|
|
328
|
+
bases: list[str] = []
|
|
329
|
+
expanded: set[str] = set()
|
|
330
|
+
pending = list(dict.fromkeys(selection_set.fragment_names))
|
|
331
|
+
|
|
332
|
+
def expand(fragment_name: str, /) -> None:
|
|
333
|
+
expanded.add(fragment_name)
|
|
334
|
+
fragment = self._resolve_fragment(fragment_name)
|
|
335
|
+
fields.extend(fragment.fields)
|
|
336
|
+
conditions.extend(fragment.conditions)
|
|
337
|
+
pending.extend(fragment.fragment_names)
|
|
338
|
+
|
|
339
|
+
while True:
|
|
340
|
+
while pending:
|
|
341
|
+
fragment_name = pending.pop(0)
|
|
342
|
+
|
|
343
|
+
if fragment_name in bases or fragment_name in expanded:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
if self._normalized_fragment(fragment_name).conditions:
|
|
347
|
+
# A fragment whose type is a union cannot be a base.
|
|
348
|
+
expand(fragment_name)
|
|
349
|
+
else:
|
|
350
|
+
bases.append(fragment_name)
|
|
351
|
+
|
|
352
|
+
merged = _merge_fields(fields)
|
|
353
|
+
conflicting = next(
|
|
354
|
+
(
|
|
355
|
+
base
|
|
356
|
+
for base in bases
|
|
357
|
+
if self._declares_a_key_otherwise(
|
|
358
|
+
base,
|
|
359
|
+
fields=merged,
|
|
360
|
+
other_bases=[other for other in bases if other != base],
|
|
361
|
+
)
|
|
362
|
+
),
|
|
363
|
+
None,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if conflicting is None:
|
|
367
|
+
return replace(
|
|
368
|
+
selection_set,
|
|
369
|
+
fields=merged,
|
|
370
|
+
conditions=tuple(conditions),
|
|
371
|
+
fragment_names=tuple(bases),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
bases.remove(conflicting)
|
|
375
|
+
expand(conflicting)
|
|
376
|
+
|
|
377
|
+
def _normalize_fragment(self, fragment_name: str, /) -> SelectionSet:
|
|
378
|
+
return self._normalize(self._resolve_fragment(fragment_name))
|
|
379
|
+
|
|
380
|
+
def _find_declared_keys(self, fragment_name: str, /) -> Mapping[str, _DeclaredKey]:
|
|
381
|
+
normalized = self._normalized_fragment(fragment_name)
|
|
382
|
+
declared: dict[str, _DeclaredKey] = {}
|
|
383
|
+
|
|
384
|
+
for base in normalized.fragment_names:
|
|
385
|
+
declared.update(self._declared_keys(base))
|
|
386
|
+
|
|
387
|
+
for field in normalized.fields:
|
|
388
|
+
declared[field.response_name] = _DeclaredKey(
|
|
389
|
+
field=field, declaring_fragment=fragment_name
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return declared
|
|
393
|
+
|
|
394
|
+
def _declares_a_key_otherwise(
|
|
395
|
+
self,
|
|
396
|
+
base: str,
|
|
397
|
+
/,
|
|
398
|
+
*,
|
|
399
|
+
fields: Sequence[SelectedField],
|
|
400
|
+
other_bases: Sequence[str],
|
|
401
|
+
) -> bool:
|
|
402
|
+
own = {field.response_name: field for field in fields}
|
|
403
|
+
|
|
404
|
+
for key, declared in self._declared_keys(base).items():
|
|
405
|
+
if key in own and not self._same_leaf(declared.field, own[key]):
|
|
406
|
+
return True
|
|
407
|
+
|
|
408
|
+
for other in other_bases:
|
|
409
|
+
other_declared = self._declared_keys(other).get(key)
|
|
410
|
+
|
|
411
|
+
if (
|
|
412
|
+
other_declared is not None
|
|
413
|
+
and other_declared.declaring_fragment != declared.declaring_fragment
|
|
414
|
+
and not self._same_leaf(declared.field, other_declared.field)
|
|
415
|
+
):
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
def _same_leaf(self, field: SelectedField, other: SelectedField, /) -> bool:
|
|
421
|
+
"""Whether two fields get the very same annotation, which only leaves can."""
|
|
422
|
+
return (
|
|
423
|
+
field.selection_set is None
|
|
424
|
+
and other.selection_set is None
|
|
425
|
+
and self._leaf_signature(field) == self._leaf_signature(other)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def _leaf_signature(self, field: SelectedField, /) -> tuple[object, ...]:
|
|
429
|
+
return (
|
|
430
|
+
field.name,
|
|
431
|
+
str(field.type),
|
|
432
|
+
self._is_non_null(field),
|
|
433
|
+
self._struct_input_name(field),
|
|
434
|
+
field.skippable,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def _struct_input_name(self, field: SelectedField, /) -> str | None:
|
|
438
|
+
return get_struct_input_name(
|
|
439
|
+
field.parent_type,
|
|
440
|
+
field.name,
|
|
441
|
+
struct_types=self._struct_types,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def _is_non_null(self, field: SelectedField, /) -> bool:
|
|
445
|
+
return self._non_null_directive_name in field.directive_names
|
|
446
|
+
|
|
447
|
+
def _emit_field(
|
|
448
|
+
self,
|
|
449
|
+
field: SelectedField,
|
|
450
|
+
/,
|
|
451
|
+
*,
|
|
452
|
+
path: Sequence[str],
|
|
453
|
+
fragment_reference: Callable[[str], ast.expr],
|
|
454
|
+
) -> tuple[list[ast.stmt], ast.expr]:
|
|
455
|
+
non_null = self._is_non_null(field)
|
|
456
|
+
# `@nonNull` is the caller asserting that this query cannot return null here, so the annotation loses its `| None`, and `NON_NULL` tells the runtime to enforce it.
|
|
457
|
+
field_type = as_non_null(field.type) if non_null else field.type
|
|
458
|
+
statements: list[ast.stmt] = []
|
|
459
|
+
struct_input_name = self._struct_input_name(field)
|
|
460
|
+
|
|
461
|
+
if struct_input_name is not None:
|
|
462
|
+
annotation: ast.expr = qualified(
|
|
463
|
+
INPUT,
|
|
464
|
+
self._schema_spellings.type_spellings[struct_input_name],
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if not is_non_null_type(field_type):
|
|
468
|
+
annotation = optional(annotation)
|
|
469
|
+
elif field.selection_set is None:
|
|
470
|
+
annotation = self._annotations.build(
|
|
471
|
+
field_type,
|
|
472
|
+
position="data",
|
|
473
|
+
object_annotation=None,
|
|
474
|
+
bare_inputs=False,
|
|
475
|
+
)
|
|
476
|
+
else:
|
|
477
|
+
normalized = self._normalize(field.selection_set)
|
|
478
|
+
object_annotation: ast.expr
|
|
479
|
+
|
|
480
|
+
match normalized:
|
|
481
|
+
case SelectionSet(
|
|
482
|
+
fields=(), conditions=(), fragment_names=(fragment_name,)
|
|
483
|
+
):
|
|
484
|
+
# The fragment's type itself, rather than a class adding nothing to it.
|
|
485
|
+
object_annotation = fragment_reference(fragment_name)
|
|
486
|
+
case _:
|
|
487
|
+
child_path = (*path, field.response_name)
|
|
488
|
+
child = PendingSpelling(nested_type_name(child_path))
|
|
489
|
+
statements = self._emit_normalized(
|
|
490
|
+
normalized,
|
|
491
|
+
spelling=child,
|
|
492
|
+
path=child_path,
|
|
493
|
+
fragment_reference=fragment_reference,
|
|
494
|
+
inheritable=False,
|
|
495
|
+
)
|
|
496
|
+
object_annotation = name(child)
|
|
497
|
+
|
|
498
|
+
annotation = self._annotations.build(
|
|
499
|
+
field_type,
|
|
500
|
+
position="data",
|
|
501
|
+
object_annotation=object_annotation,
|
|
502
|
+
bare_inputs=False,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
if non_null:
|
|
506
|
+
annotation = subscript(
|
|
507
|
+
qualified(TYPING, "Annotated"),
|
|
508
|
+
annotation,
|
|
509
|
+
qualified(REFLECTION, "NON_NULL"),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return statements, annotation
|
|
513
|
+
|
|
514
|
+
def _possible_types(
|
|
515
|
+
self, type_: GraphQLCompositeType, /
|
|
516
|
+
) -> Sequence[GraphQLObjectType]:
|
|
517
|
+
return (
|
|
518
|
+
(type_,)
|
|
519
|
+
if isinstance(type_, GraphQLObjectType)
|
|
520
|
+
else self._schema.get_possible_types(type_)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _typename_annotation(possible_types: Sequence[GraphQLObjectType], /) -> ast.expr:
|
|
525
|
+
# Read-only, which is what lets a subclass narrow the `__typename` of a fragment it inherits: a mutable key cannot change type in a subclass.
|
|
526
|
+
return subscript(
|
|
527
|
+
qualified(COMPAT, "ReadOnly"),
|
|
528
|
+
subscript(
|
|
529
|
+
qualified(TYPING, "Literal"),
|
|
530
|
+
*(constant(possible_type.name) for possible_type in possible_types),
|
|
531
|
+
),
|
|
532
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
3
|
+
from graphql import (
|
|
4
|
+
DocumentNode,
|
|
5
|
+
FieldNode,
|
|
6
|
+
FragmentDefinitionNode,
|
|
7
|
+
FragmentSpreadNode,
|
|
8
|
+
GraphQLError,
|
|
9
|
+
GraphQLSchema,
|
|
10
|
+
InlineFragmentNode,
|
|
11
|
+
NameNode,
|
|
12
|
+
Node,
|
|
13
|
+
OperationDefinitionNode,
|
|
14
|
+
SelectionSetNode,
|
|
15
|
+
TypeInfo,
|
|
16
|
+
TypeInfoVisitor,
|
|
17
|
+
Visitor,
|
|
18
|
+
is_abstract_type,
|
|
19
|
+
validate,
|
|
20
|
+
visit,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_operations(
|
|
25
|
+
document: DocumentNode, /, *, schema: GraphQLSchema
|
|
26
|
+
) -> dict[str, OperationDefinitionNode]:
|
|
27
|
+
errors = validate(schema, document)
|
|
28
|
+
operations: dict[str, OperationDefinitionNode] = {}
|
|
29
|
+
|
|
30
|
+
for definition in document.definitions:
|
|
31
|
+
if not isinstance(definition, OperationDefinitionNode):
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
if definition.name is None:
|
|
35
|
+
errors.append(
|
|
36
|
+
GraphQLError(
|
|
37
|
+
"Expected the operation to be named to name its constant after it.",
|
|
38
|
+
nodes=definition,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
operations[definition.name.value] = definition
|
|
43
|
+
|
|
44
|
+
if errors:
|
|
45
|
+
raise ExceptionGroup("The operations are invalid", errors)
|
|
46
|
+
|
|
47
|
+
return operations
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def insert_typename(
|
|
51
|
+
document: DocumentNode, /, *, schema: GraphQLSchema
|
|
52
|
+
) -> DocumentNode:
|
|
53
|
+
"""Return *document* selecting `__typename` on every abstract type, since it tells the values apart, for type checkers to narrow and for the parser to convert."""
|
|
54
|
+
type_info = TypeInfo(schema)
|
|
55
|
+
|
|
56
|
+
class _Visitor(Visitor):
|
|
57
|
+
def enter_selection_set(
|
|
58
|
+
self, node: SelectionSetNode, _key: object, parent: object, *_args: object
|
|
59
|
+
) -> SelectionSetNode | None:
|
|
60
|
+
# An inline fragment's selections are gathered into the branches of the selection enclosing it, which that selection's `__typename` tells apart.
|
|
61
|
+
if (
|
|
62
|
+
isinstance(parent, InlineFragmentNode)
|
|
63
|
+
or not is_abstract_type(type_info.get_parent_type())
|
|
64
|
+
or any(
|
|
65
|
+
isinstance(selection, FieldNode)
|
|
66
|
+
and selection.name.value == "__typename"
|
|
67
|
+
and selection.alias is None
|
|
68
|
+
and not selection.directives
|
|
69
|
+
for selection in node.selections
|
|
70
|
+
)
|
|
71
|
+
):
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
return SelectionSetNode(
|
|
75
|
+
selections=(
|
|
76
|
+
FieldNode(
|
|
77
|
+
name=NameNode(value="__typename"), arguments=(), directives=()
|
|
78
|
+
),
|
|
79
|
+
*node.selections,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
result = visit(document, TypeInfoVisitor(type_info, _Visitor()))
|
|
84
|
+
assert isinstance(result, DocumentNode)
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_transitively_spread_fragments(
|
|
89
|
+
node: Node, /, *, definitions: Mapping[str, FragmentDefinitionNode]
|
|
90
|
+
) -> list[FragmentDefinitionNode]:
|
|
91
|
+
spread: set[str] = set()
|
|
92
|
+
|
|
93
|
+
class _Visitor(Visitor):
|
|
94
|
+
def enter_fragment_spread(
|
|
95
|
+
self, spread_node: FragmentSpreadNode, *_args: object
|
|
96
|
+
) -> None:
|
|
97
|
+
fragment_name = spread_node.name.value
|
|
98
|
+
|
|
99
|
+
if fragment_name not in spread:
|
|
100
|
+
spread.add(fragment_name)
|
|
101
|
+
visit(definitions[fragment_name], self)
|
|
102
|
+
|
|
103
|
+
visit(node, _Visitor())
|
|
104
|
+
return [
|
|
105
|
+
definition
|
|
106
|
+
for fragment_name, definition in definitions.items()
|
|
107
|
+
if fragment_name in spread
|
|
108
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import posixpath
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
|
|
5
|
+
from graphql_codegen._generator._imports import DOCUMENT_PACKAGE
|
|
6
|
+
from graphql_codegen._generator.spelling import candidates
|
|
7
|
+
from graphql_codegen.document_sibling_module import DocumentSiblingModule
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _identifier(stem: str, /) -> str:
|
|
11
|
+
identifier = "".join(
|
|
12
|
+
character if f"_{character}".isidentifier() else "_" for character in stem
|
|
13
|
+
)
|
|
14
|
+
return identifier if identifier.isidentifier() else f"_{identifier}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def document_modules(
|
|
18
|
+
documents: Sequence[PurePosixPath],
|
|
19
|
+
/,
|
|
20
|
+
*,
|
|
21
|
+
sibling_module: DocumentSiblingModule | None,
|
|
22
|
+
) -> dict[PurePosixPath, PurePosixPath]:
|
|
23
|
+
"""A module's name depends on its document alone, so that adding a document never renames another's module, which code imports types from: two documents whose modules would be one file are an error rather than one of them getting a suffix.
|
|
24
|
+
|
|
25
|
+
It is relative to the package's directory for a module in the package, and in the documents' own terms for a sibling one.
|
|
26
|
+
"""
|
|
27
|
+
by_file: dict[tuple[PurePosixPath, str], PurePosixPath] = {}
|
|
28
|
+
modules: dict[PurePosixPath, PurePosixPath] = {}
|
|
29
|
+
|
|
30
|
+
for document in sorted(documents):
|
|
31
|
+
if sibling_module is None:
|
|
32
|
+
directory, name = PurePosixPath(DOCUMENT_PACKAGE), document.stem
|
|
33
|
+
else:
|
|
34
|
+
directory = PurePosixPath(posixpath.normpath(document.parent))
|
|
35
|
+
name = sibling_module._module_name(document)
|
|
36
|
+
|
|
37
|
+
spelling = next(candidates(_identifier(name)))
|
|
38
|
+
path = directory / f"{spelling}.py"
|
|
39
|
+
|
|
40
|
+
if spelling == "__init__":
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Expected `{document}` to name a module, but `__init__` is its package's own."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Some file systems ignore case.
|
|
46
|
+
if (other := by_file.get((directory, spelling.casefold()))) is not None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Expected `{other}` and `{document}` to have modules of their own, but both would be `{path}`, case aside."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
by_file[directory, spelling.casefold()] = document
|
|
52
|
+
modules[document] = path
|
|
53
|
+
|
|
54
|
+
return modules
|