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,695 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from graphlib import TopologicalSorter
|
|
6
|
+
from importlib.resources import files as resource_files
|
|
7
|
+
from pathlib import PurePosixPath
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Final, assert_never, final
|
|
10
|
+
|
|
11
|
+
from graphql import (
|
|
12
|
+
DocumentNode,
|
|
13
|
+
FragmentDefinitionNode,
|
|
14
|
+
FragmentSpreadNode,
|
|
15
|
+
GraphQLSchema,
|
|
16
|
+
Node,
|
|
17
|
+
OperationDefinitionNode,
|
|
18
|
+
Visitor,
|
|
19
|
+
visit,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from graphql_codegen import runtime
|
|
23
|
+
from graphql_codegen._generator._annotation import AnnotationBuilder
|
|
24
|
+
from graphql_codegen._generator._ast_nodes import (
|
|
25
|
+
FUTURE_ANNOTATIONS,
|
|
26
|
+
constant,
|
|
27
|
+
import_as,
|
|
28
|
+
module,
|
|
29
|
+
name,
|
|
30
|
+
qualified,
|
|
31
|
+
relative_import_from,
|
|
32
|
+
unparse,
|
|
33
|
+
)
|
|
34
|
+
from graphql_codegen._generator._data_type import DataTypeEmitter
|
|
35
|
+
from graphql_codegen._generator._document import (
|
|
36
|
+
get_transitively_spread_fragments,
|
|
37
|
+
insert_typename,
|
|
38
|
+
parse_operations,
|
|
39
|
+
)
|
|
40
|
+
from graphql_codegen._generator._document_module import document_modules
|
|
41
|
+
from graphql_codegen._generator._imports import (
|
|
42
|
+
DOCUMENT_PACKAGE,
|
|
43
|
+
INJECTION_MODULE,
|
|
44
|
+
RUNTIME_PACKAGE,
|
|
45
|
+
SCALAR_MODULE,
|
|
46
|
+
SCHEMA_PACKAGE,
|
|
47
|
+
GeneratedModule,
|
|
48
|
+
OutsideModule,
|
|
49
|
+
PackageModule,
|
|
50
|
+
helper_imports,
|
|
51
|
+
)
|
|
52
|
+
from graphql_codegen._generator._injector import (
|
|
53
|
+
emit_injector_module,
|
|
54
|
+
get_injected_types,
|
|
55
|
+
get_injections,
|
|
56
|
+
)
|
|
57
|
+
from graphql_codegen._generator._merge import print_merge_template
|
|
58
|
+
from graphql_codegen._generator._naming import (
|
|
59
|
+
OperationSpellings,
|
|
60
|
+
SchemaSpellings,
|
|
61
|
+
operation_spellings,
|
|
62
|
+
schema_spellings,
|
|
63
|
+
)
|
|
64
|
+
from graphql_codegen._generator._operation import (
|
|
65
|
+
description_text,
|
|
66
|
+
emit_operation_constant,
|
|
67
|
+
emit_variables_type,
|
|
68
|
+
strip_for_server,
|
|
69
|
+
)
|
|
70
|
+
from graphql_codegen._generator._scalar import check_scalars, emit_scalars
|
|
71
|
+
from graphql_codegen._generator._schema import prepare_schema
|
|
72
|
+
from graphql_codegen._generator._schema_type import (
|
|
73
|
+
emit_enums,
|
|
74
|
+
emit_inputs,
|
|
75
|
+
emit_schema_package,
|
|
76
|
+
get_reached_type_names,
|
|
77
|
+
)
|
|
78
|
+
from graphql_codegen._generator._selection import SelectionResolver
|
|
79
|
+
from graphql_codegen._generator._structs import parse_struct_types
|
|
80
|
+
from graphql_codegen._generator.spelling import (
|
|
81
|
+
PendingSpelling,
|
|
82
|
+
SettledSpelling,
|
|
83
|
+
ast_str,
|
|
84
|
+
is_bindable,
|
|
85
|
+
settle_module,
|
|
86
|
+
settle_names,
|
|
87
|
+
)
|
|
88
|
+
from graphql_codegen._metadata import GENERATED_FILE_COMMENT_PREFIX
|
|
89
|
+
from graphql_codegen.config import Config
|
|
90
|
+
from graphql_codegen.document_sibling_module import DocumentSiblingModule
|
|
91
|
+
|
|
92
|
+
_DO_NOT_EDIT: Final = "DO NOT EDIT."
|
|
93
|
+
_GENERATED_FILE_COMMENT: Final = f"{GENERATED_FILE_COMMENT_PREFIX}. {_DO_NOT_EDIT}\n"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _generated(source: str, /) -> str:
|
|
97
|
+
"""*source* under the generated file comment, separated by a blank line unless it is empty."""
|
|
98
|
+
return (
|
|
99
|
+
f"{_GENERATED_FILE_COMMENT}\n{source}"
|
|
100
|
+
if source.strip()
|
|
101
|
+
else _GENERATED_FILE_COMMENT
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _names_by_document(
|
|
106
|
+
definitions: Mapping[str, OperationDefinitionNode | FragmentDefinitionNode],
|
|
107
|
+
/,
|
|
108
|
+
*,
|
|
109
|
+
module_root: PurePosixPath | None,
|
|
110
|
+
) -> dict[PurePosixPath, list[str]]:
|
|
111
|
+
"""A document is the path its source is named by.
|
|
112
|
+
|
|
113
|
+
When *module_root*, where sibling modules are imported from, is not ``None``, every definition must be located in its terms: that is where its module goes.
|
|
114
|
+
"""
|
|
115
|
+
names: dict[PurePosixPath, list[str]] = {}
|
|
116
|
+
|
|
117
|
+
for definition_name, definition in definitions.items():
|
|
118
|
+
# graphql-core names a string parsed without a source so too.
|
|
119
|
+
document = PurePosixPath(
|
|
120
|
+
"GraphQL request" if definition.loc is None else definition.loc.source.name
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if module_root is not None and (
|
|
124
|
+
definition.loc is None
|
|
125
|
+
or document.is_absolute() != module_root.is_absolute()
|
|
126
|
+
):
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"Expected every operation and fragment to come from a source named by its path, absolute or relative to the same directory as the module root, so that its module can go next to its document."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
names.setdefault(document, []).append(definition_name)
|
|
132
|
+
|
|
133
|
+
return names
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _import_module(dotted_name: str, /, *, alias: PendingSpelling) -> ast.stmt:
|
|
137
|
+
"""Import the module *dotted_name* from its package, which a submodule named like the package cannot shadow, as `import a.b.c as alias` lets `a`'s attribute `b` do."""
|
|
138
|
+
package, _, module_name = dotted_name.rpartition(".")
|
|
139
|
+
return (
|
|
140
|
+
ast.ImportFrom(
|
|
141
|
+
module=package,
|
|
142
|
+
names=[ast.alias(name=module_name, asname=ast_str(alias))],
|
|
143
|
+
level=0,
|
|
144
|
+
)
|
|
145
|
+
if package
|
|
146
|
+
else import_as(module_name, alias)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _direct_spread_names(node: Node, /) -> set[str]:
|
|
151
|
+
names: set[str] = set()
|
|
152
|
+
|
|
153
|
+
class _Visitor(Visitor):
|
|
154
|
+
def enter_fragment_spread(
|
|
155
|
+
self, spread: FragmentSpreadNode, *_args: object
|
|
156
|
+
) -> None:
|
|
157
|
+
names.add(spread.name.value)
|
|
158
|
+
|
|
159
|
+
visit(node, _Visitor())
|
|
160
|
+
return names
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _dotted_module_name(
|
|
164
|
+
path: PurePosixPath, /, *, document: PurePosixPath, module_root: PurePosixPath
|
|
165
|
+
) -> str:
|
|
166
|
+
# Outside the module root, a module's path walks up with `..`, which no dotted name holds.
|
|
167
|
+
parts = path.with_suffix("").relative_to(module_root, walk_up=True).parts
|
|
168
|
+
|
|
169
|
+
if all(is_bindable(part) for part in parts):
|
|
170
|
+
return ".".join(parts)
|
|
171
|
+
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"Expected `{document}`'s module, `{path}`, to be importable from the module root, since another document spreads its fragments."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_RUNTIME_MARKER: Final = (
|
|
178
|
+
b"# Copied into each generated package, where this line says not to edit it.\n"
|
|
179
|
+
)
|
|
180
|
+
"""The first line of every runtime module, which the copy swaps for the generated file comment."""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _runtime_copy(source: bytes, /) -> bytes:
|
|
184
|
+
"""Swapped line for line, so that coverage maps each line of the copy to the same line of the runtime."""
|
|
185
|
+
assert source.startswith(_RUNTIME_MARKER), (
|
|
186
|
+
"Expected a runtime module to start with its marker."
|
|
187
|
+
)
|
|
188
|
+
return _GENERATED_FILE_COMMENT.encode() + source.removeprefix(_RUNTIME_MARKER)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
_RUNTIME_FILES: Final = MappingProxyType(
|
|
192
|
+
{
|
|
193
|
+
PurePosixPath(RUNTIME_PACKAGE, resource.name): _runtime_copy(
|
|
194
|
+
resource.read_bytes()
|
|
195
|
+
)
|
|
196
|
+
for resource in sorted(
|
|
197
|
+
resource_files(runtime).iterdir(), key=lambda resource: resource.name
|
|
198
|
+
)
|
|
199
|
+
if resource.name.endswith(".py")
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
"""The runtime, copied so that a generated package depends on nothing but the standard library, and :mod:`typing_extensions` before Python 3.15.
|
|
203
|
+
|
|
204
|
+
Read once, at import, so that generating reads no file.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@final
|
|
209
|
+
class PackageEmitter:
|
|
210
|
+
def __init__(
|
|
211
|
+
self, *, document: DocumentNode, schema: GraphQLSchema, config: Config
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Derive what can fail, so that no emitter holds what it cannot generate from, leaving the rest to first use."""
|
|
214
|
+
schema = prepare_schema(
|
|
215
|
+
schema, non_null_directive_name=config.non_null_directive_name
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
struct_types = (
|
|
219
|
+
None
|
|
220
|
+
if config.struct_interface_name is None
|
|
221
|
+
else parse_struct_types(schema, config.struct_interface_name)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
check_scalars(config.scalars, schema=schema)
|
|
225
|
+
injector_names = frozenset(config.injector_names)
|
|
226
|
+
document = insert_typename(document, schema=schema)
|
|
227
|
+
operations = parse_operations(document, schema=schema)
|
|
228
|
+
fragments = {
|
|
229
|
+
definition.name.value: definition
|
|
230
|
+
for definition in document.definitions
|
|
231
|
+
if isinstance(definition, FragmentDefinitionNode)
|
|
232
|
+
}
|
|
233
|
+
sibling_module = config.document_sibling_module
|
|
234
|
+
module_root = (
|
|
235
|
+
None
|
|
236
|
+
if sibling_module is None
|
|
237
|
+
else sibling_module.package_location.module_root
|
|
238
|
+
)
|
|
239
|
+
operation_names = _names_by_document(operations, module_root=module_root)
|
|
240
|
+
fragment_names = _names_by_document(fragments, module_root=module_root)
|
|
241
|
+
modules = document_modules(
|
|
242
|
+
sorted({*operation_names, *fragment_names}), sibling_module=sibling_module
|
|
243
|
+
)
|
|
244
|
+
# The `document` package re-exports every operation and fragment beside its modules, which keep their names, then the operations theirs.
|
|
245
|
+
module_spellings = (
|
|
246
|
+
{SettledSpelling(path.stem) for path in modules.values()}
|
|
247
|
+
if sibling_module is None
|
|
248
|
+
else set()
|
|
249
|
+
)
|
|
250
|
+
operation_spellings = settle_names(sorted(operations), taken=module_spellings)
|
|
251
|
+
|
|
252
|
+
injections = {
|
|
253
|
+
operation_name: get_injections(
|
|
254
|
+
operation, schema=schema, injector_names=injector_names
|
|
255
|
+
)
|
|
256
|
+
for operation_name, operation in operations.items()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
self._schema: Final = schema
|
|
260
|
+
self._struct_types: Final = struct_types
|
|
261
|
+
self._document: Final = document
|
|
262
|
+
self._document_sibling_module: Final = sibling_module
|
|
263
|
+
self._injector_names: Final = injector_names
|
|
264
|
+
self._non_null_directive_name: Final = config.non_null_directive_name
|
|
265
|
+
self._scalars: Final = config.scalars
|
|
266
|
+
self._operations: Final = operations
|
|
267
|
+
self._operation_spellings: Final = operation_spellings
|
|
268
|
+
self._operation_names_by_document: Final = operation_names
|
|
269
|
+
fragment_documents = {
|
|
270
|
+
fragment_name: fragment_document
|
|
271
|
+
for fragment_document, names in fragment_names.items()
|
|
272
|
+
for fragment_name in names
|
|
273
|
+
}
|
|
274
|
+
self._fragments: Final = fragments
|
|
275
|
+
self._fragment_names_by_document: Final = fragment_names
|
|
276
|
+
self._module_spellings: Final = module_spellings
|
|
277
|
+
self._fragment_documents: Final = fragment_documents
|
|
278
|
+
self._document_modules: Final = modules
|
|
279
|
+
self._imported_module_dotted_names: Final = (
|
|
280
|
+
{}
|
|
281
|
+
if module_root is None
|
|
282
|
+
else {
|
|
283
|
+
spread_document: _dotted_module_name(
|
|
284
|
+
modules[spread_document],
|
|
285
|
+
document=spread_document,
|
|
286
|
+
module_root=module_root,
|
|
287
|
+
)
|
|
288
|
+
for definitions, names in (
|
|
289
|
+
(operations, operation_names),
|
|
290
|
+
(fragments, fragment_names),
|
|
291
|
+
)
|
|
292
|
+
for definition_document, definition_names in names.items()
|
|
293
|
+
for definition_name in definition_names
|
|
294
|
+
for spread_name in _direct_spread_names(definitions[definition_name])
|
|
295
|
+
if (spread_document := fragment_documents[spread_name])
|
|
296
|
+
!= definition_document
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
self._injections: Final = injections
|
|
300
|
+
self._injected_types: Final = get_injected_types(
|
|
301
|
+
injections, injector_names=injector_names
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
@cached_property
|
|
305
|
+
def _resolver(self) -> SelectionResolver:
|
|
306
|
+
return SelectionResolver(self._fragments, schema=self._schema)
|
|
307
|
+
|
|
308
|
+
@cached_property
|
|
309
|
+
def _schema_spellings(self) -> SchemaSpellings:
|
|
310
|
+
return schema_spellings(
|
|
311
|
+
get_reached_type_names(
|
|
312
|
+
self._document,
|
|
313
|
+
schema=self._schema,
|
|
314
|
+
struct_types=self._struct_types,
|
|
315
|
+
injector_names=self._injector_names,
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@cached_property
|
|
320
|
+
def _scalar_spellings(self) -> dict[str, SettledSpelling]:
|
|
321
|
+
return settle_names(sorted(self._scalars), taken=set())
|
|
322
|
+
|
|
323
|
+
@cached_property
|
|
324
|
+
def _fragment_spellings(self) -> dict[str, SettledSpelling]:
|
|
325
|
+
"""Settled where their names meet others: in the `document` package, which re-exports them, and in their sibling module otherwise."""
|
|
326
|
+
if self._document_sibling_module is None:
|
|
327
|
+
return settle_names(
|
|
328
|
+
sorted(self._fragments),
|
|
329
|
+
taken={*self._module_spellings, *self._operation_spellings.values()},
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
fragment_name: spelling
|
|
334
|
+
for document, names in self._fragment_names_by_document.items()
|
|
335
|
+
for fragment_name, spelling in settle_names(
|
|
336
|
+
sorted(names),
|
|
337
|
+
taken={
|
|
338
|
+
self._operation_spellings[operation_name]
|
|
339
|
+
for operation_name in self._operation_names_by_document.get(
|
|
340
|
+
document, []
|
|
341
|
+
)
|
|
342
|
+
},
|
|
343
|
+
).items()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@cached_property
|
|
347
|
+
def _annotations(self) -> AnnotationBuilder:
|
|
348
|
+
return AnnotationBuilder(
|
|
349
|
+
scalar_spellings=self._scalar_spellings,
|
|
350
|
+
schema_spellings=self._schema_spellings,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _module_source(
|
|
354
|
+
self,
|
|
355
|
+
body: list[ast.stmt],
|
|
356
|
+
/,
|
|
357
|
+
*,
|
|
358
|
+
generated_module: GeneratedModule,
|
|
359
|
+
preamble: Sequence[ast.stmt],
|
|
360
|
+
) -> str:
|
|
361
|
+
imports = helper_imports(
|
|
362
|
+
body, module=generated_module, schema_spellings=self._schema_spellings
|
|
363
|
+
)
|
|
364
|
+
tree = module([*preamble, *imports, *body])
|
|
365
|
+
settle_module(tree)
|
|
366
|
+
|
|
367
|
+
for node in ast.walk(tree):
|
|
368
|
+
# Settled like the name it imports, a helper's alias would make the import a re-export, as PEP 484 spells one, when a module only exports what it defines.
|
|
369
|
+
if isinstance(node, ast.alias) and node.asname == node.name:
|
|
370
|
+
node.asname = None
|
|
371
|
+
|
|
372
|
+
return unparse(tree)
|
|
373
|
+
|
|
374
|
+
@cached_property
|
|
375
|
+
def _data_types(self) -> DataTypeEmitter:
|
|
376
|
+
return DataTypeEmitter(
|
|
377
|
+
schema=self._schema,
|
|
378
|
+
annotations=self._annotations,
|
|
379
|
+
schema_spellings=self._schema_spellings,
|
|
380
|
+
resolve_fragment=lambda fragment_name: self._resolver.resolve(
|
|
381
|
+
self._fragments[fragment_name]
|
|
382
|
+
),
|
|
383
|
+
struct_types=self._struct_types,
|
|
384
|
+
non_null_directive_name=self._non_null_directive_name,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def _fragment_order(self, fragment_names: Sequence[str], /) -> list[str]:
|
|
388
|
+
"""*fragment_names*, each after those of them it spreads, since their types may be its bases."""
|
|
389
|
+
return list(
|
|
390
|
+
TopologicalSorter(
|
|
391
|
+
{
|
|
392
|
+
fragment_name: [
|
|
393
|
+
spread.name.value
|
|
394
|
+
for spread in get_transitively_spread_fragments(
|
|
395
|
+
self._fragments[fragment_name], definitions=self._fragments
|
|
396
|
+
)
|
|
397
|
+
if spread.name.value in fragment_names
|
|
398
|
+
]
|
|
399
|
+
for fragment_name in fragment_names
|
|
400
|
+
}
|
|
401
|
+
).static_order()
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def _fragment_types(
|
|
405
|
+
self,
|
|
406
|
+
fragment_name: str,
|
|
407
|
+
/,
|
|
408
|
+
*,
|
|
409
|
+
fragment_reference: Callable[[str], ast.expr],
|
|
410
|
+
) -> list[ast.stmt]:
|
|
411
|
+
definition = self._fragments[fragment_name]
|
|
412
|
+
selection_set = self._resolver.resolve(definition)
|
|
413
|
+
return self._data_types.emit(
|
|
414
|
+
# The fragment's own description, when it has one, says more than its type's.
|
|
415
|
+
replace(selection_set, description=description)
|
|
416
|
+
if (description := description_text(definition.description))
|
|
417
|
+
else selection_set,
|
|
418
|
+
root_spelling=self._fragment_spellings[fragment_name],
|
|
419
|
+
fragment_reference=fragment_reference,
|
|
420
|
+
inheritable=True,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _operation_body(
|
|
424
|
+
self,
|
|
425
|
+
operation_name: str,
|
|
426
|
+
operation: OperationDefinitionNode,
|
|
427
|
+
/,
|
|
428
|
+
*,
|
|
429
|
+
spellings: OperationSpellings,
|
|
430
|
+
fragment_reference: Callable[[str], ast.expr],
|
|
431
|
+
) -> list[ast.stmt]:
|
|
432
|
+
document = strip_for_server(
|
|
433
|
+
operation,
|
|
434
|
+
fragments=get_transitively_spread_fragments(
|
|
435
|
+
operation, definitions=self._fragments
|
|
436
|
+
),
|
|
437
|
+
# The server knows nothing of the directive, which is the client's assertion.
|
|
438
|
+
client_directive_names=frozenset()
|
|
439
|
+
if self._non_null_directive_name is None
|
|
440
|
+
else frozenset({self._non_null_directive_name}),
|
|
441
|
+
)
|
|
442
|
+
return [
|
|
443
|
+
*emit_variables_type(
|
|
444
|
+
operation,
|
|
445
|
+
spellings=spellings,
|
|
446
|
+
schema=self._schema,
|
|
447
|
+
annotations=self._annotations,
|
|
448
|
+
injector_names=self._injector_names,
|
|
449
|
+
),
|
|
450
|
+
*self._data_types.emit(
|
|
451
|
+
# `Query` and `Mutation` describe the schema's entry points, which says nothing about this operation, so the root type's description is dropped rather than repeated on every one.
|
|
452
|
+
replace(self._resolver.resolve(operation), description=""),
|
|
453
|
+
root_spelling=spellings.data,
|
|
454
|
+
fragment_reference=fragment_reference,
|
|
455
|
+
inheritable=False,
|
|
456
|
+
),
|
|
457
|
+
*emit_operation_constant(
|
|
458
|
+
operation,
|
|
459
|
+
graphql_name=operation_name,
|
|
460
|
+
spellings=spellings,
|
|
461
|
+
document=print_merge_template(document),
|
|
462
|
+
injections=self._injections[operation_name],
|
|
463
|
+
),
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
def _emit_document_module(
|
|
467
|
+
self,
|
|
468
|
+
document: PurePosixPath,
|
|
469
|
+
/,
|
|
470
|
+
*,
|
|
471
|
+
generated_module: GeneratedModule,
|
|
472
|
+
) -> str:
|
|
473
|
+
operation_names = self._operation_names_by_document.get(document, [])
|
|
474
|
+
fragment_names = self._fragment_names_by_document.get(document, [])
|
|
475
|
+
spellings = operation_spellings(
|
|
476
|
+
[self._operation_spellings[name] for name in operation_names],
|
|
477
|
+
taken=[self._fragment_spellings[name] for name in fragment_names],
|
|
478
|
+
)
|
|
479
|
+
aliases: dict[PurePosixPath, PendingSpelling] = {}
|
|
480
|
+
|
|
481
|
+
def fragment_reference(fragment_name: str, /) -> ast.expr:
|
|
482
|
+
spelling = self._fragment_spellings[fragment_name]
|
|
483
|
+
|
|
484
|
+
if (
|
|
485
|
+
fragment_document := self._fragment_documents[fragment_name]
|
|
486
|
+
) == document:
|
|
487
|
+
return name(spelling)
|
|
488
|
+
|
|
489
|
+
alias = aliases.setdefault(
|
|
490
|
+
fragment_document,
|
|
491
|
+
PendingSpelling(f"_{self._document_modules[fragment_document].stem}"),
|
|
492
|
+
)
|
|
493
|
+
return qualified(alias, spelling)
|
|
494
|
+
|
|
495
|
+
body = [
|
|
496
|
+
*(
|
|
497
|
+
statement
|
|
498
|
+
for fragment_name in self._fragment_order(fragment_names)
|
|
499
|
+
for statement in self._fragment_types(
|
|
500
|
+
fragment_name, fragment_reference=fragment_reference
|
|
501
|
+
)
|
|
502
|
+
),
|
|
503
|
+
*(
|
|
504
|
+
statement
|
|
505
|
+
for operation_name in operation_names
|
|
506
|
+
for statement in self._operation_body(
|
|
507
|
+
operation_name,
|
|
508
|
+
self._operations[operation_name],
|
|
509
|
+
spellings=spellings[self._operation_spellings[operation_name]],
|
|
510
|
+
fragment_reference=fragment_reference,
|
|
511
|
+
)
|
|
512
|
+
),
|
|
513
|
+
]
|
|
514
|
+
source = self._module_source(
|
|
515
|
+
[
|
|
516
|
+
# After the helpers' imports, which `_module_source()` puts first.
|
|
517
|
+
*(
|
|
518
|
+
self._import_document_module(imported, alias=alias)
|
|
519
|
+
for imported, alias in sorted(aliases.items())
|
|
520
|
+
),
|
|
521
|
+
*body,
|
|
522
|
+
],
|
|
523
|
+
generated_module=generated_module,
|
|
524
|
+
preamble=(),
|
|
525
|
+
)
|
|
526
|
+
return f"{GENERATED_FILE_COMMENT_PREFIX} from `{document.name}`. {_DO_NOT_EDIT}\n\n{source}"
|
|
527
|
+
|
|
528
|
+
def _import_document_module(
|
|
529
|
+
self, document: PurePosixPath, /, *, alias: PendingSpelling
|
|
530
|
+
) -> ast.stmt:
|
|
531
|
+
match self._document_sibling_module:
|
|
532
|
+
case None:
|
|
533
|
+
return ast.ImportFrom(
|
|
534
|
+
module=None,
|
|
535
|
+
names=[
|
|
536
|
+
ast.alias(
|
|
537
|
+
name=self._document_modules[document].stem,
|
|
538
|
+
asname=ast_str(alias),
|
|
539
|
+
)
|
|
540
|
+
],
|
|
541
|
+
level=1,
|
|
542
|
+
)
|
|
543
|
+
case DocumentSiblingModule():
|
|
544
|
+
return _import_module(
|
|
545
|
+
self._imported_module_dotted_names[document], alias=alias
|
|
546
|
+
)
|
|
547
|
+
case _ as never:
|
|
548
|
+
assert_never(never)
|
|
549
|
+
|
|
550
|
+
def _emit_document_reexports(
|
|
551
|
+
self, modules: Mapping[PurePosixPath, PurePosixPath], /
|
|
552
|
+
) -> str:
|
|
553
|
+
"""The `document` subpackage's `__init__.py`, re-exporting every operation and fragment by name, since their module also holds the types of their operations' variables and data.
|
|
554
|
+
|
|
555
|
+
Its modules are listed in `__lazy_modules__`, which Python 3.15 imports on first use (PEP 810) and earlier versions ignore: a star import could not be lazy.
|
|
556
|
+
"""
|
|
557
|
+
stems = sorted((path.stem, document) for document, path in modules.items())
|
|
558
|
+
lazy_modules = ast.Assign(
|
|
559
|
+
targets=[ast.Name(id="__lazy_modules__", ctx=ast.Store())],
|
|
560
|
+
value=ast.List(
|
|
561
|
+
elts=[
|
|
562
|
+
# Fully qualified from `__name__`, so that the package works wherever it is written.
|
|
563
|
+
ast.BinOp(
|
|
564
|
+
left=ast.Name(id="__name__", ctx=ast.Load()),
|
|
565
|
+
op=ast.Add(),
|
|
566
|
+
right=constant(f".{stem}"),
|
|
567
|
+
)
|
|
568
|
+
for stem, _ in stems
|
|
569
|
+
],
|
|
570
|
+
ctx=ast.Load(),
|
|
571
|
+
),
|
|
572
|
+
lineno=0,
|
|
573
|
+
)
|
|
574
|
+
return unparse(
|
|
575
|
+
module(
|
|
576
|
+
[
|
|
577
|
+
lazy_modules,
|
|
578
|
+
*(
|
|
579
|
+
relative_import_from(
|
|
580
|
+
stem,
|
|
581
|
+
sorted(
|
|
582
|
+
[
|
|
583
|
+
*(
|
|
584
|
+
self._operation_spellings[name]
|
|
585
|
+
for name in self._operation_names_by_document.get(
|
|
586
|
+
document, []
|
|
587
|
+
)
|
|
588
|
+
),
|
|
589
|
+
*(
|
|
590
|
+
self._fragment_spellings[name]
|
|
591
|
+
for name in self._fragment_names_by_document.get(
|
|
592
|
+
document, []
|
|
593
|
+
)
|
|
594
|
+
),
|
|
595
|
+
]
|
|
596
|
+
),
|
|
597
|
+
level=1,
|
|
598
|
+
re_export=True,
|
|
599
|
+
)
|
|
600
|
+
for stem, document in stems
|
|
601
|
+
),
|
|
602
|
+
],
|
|
603
|
+
),
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
def _emit_document_modules(self) -> dict[PurePosixPath, str]:
|
|
607
|
+
files: dict[PurePosixPath, str]
|
|
608
|
+
generated_module: GeneratedModule
|
|
609
|
+
|
|
610
|
+
match self._document_sibling_module:
|
|
611
|
+
case None:
|
|
612
|
+
modules = self._document_modules
|
|
613
|
+
files = {
|
|
614
|
+
PurePosixPath(DOCUMENT_PACKAGE, "__init__.py"): _generated(
|
|
615
|
+
self._emit_document_reexports(modules)
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
generated_module = PackageModule(path=(DOCUMENT_PACKAGE,))
|
|
619
|
+
case DocumentSiblingModule(package_location=location):
|
|
620
|
+
directory = location._package_directory
|
|
621
|
+
modules = {
|
|
622
|
+
document: path.relative_to(directory, walk_up=True)
|
|
623
|
+
for document, path in self._document_modules.items()
|
|
624
|
+
}
|
|
625
|
+
files = {}
|
|
626
|
+
generated_module = OutsideModule(package=location.package)
|
|
627
|
+
case _ as never:
|
|
628
|
+
assert_never(never)
|
|
629
|
+
|
|
630
|
+
for document, module_path in modules.items():
|
|
631
|
+
files[module_path] = self._emit_document_module(
|
|
632
|
+
document, generated_module=generated_module
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
return files
|
|
636
|
+
|
|
637
|
+
def emit(self) -> dict[PurePosixPath, bytes]:
|
|
638
|
+
"""The generated package, as the content of each file keyed by its path relative to the package's directory, modules next to documents included: UTF-8, with LF line endings, whatever the platform."""
|
|
639
|
+
schema_spellings = self._schema_spellings
|
|
640
|
+
document_files = self._emit_document_modules()
|
|
641
|
+
files: dict[PurePosixPath, str] = {
|
|
642
|
+
PurePosixPath("__init__.py"): _generated(""),
|
|
643
|
+
PurePosixPath(f"{SCALAR_MODULE}.py"): _generated(
|
|
644
|
+
self._module_source(
|
|
645
|
+
emit_scalars(self._scalars, spellings=self._scalar_spellings),
|
|
646
|
+
generated_module=PackageModule(path=()),
|
|
647
|
+
preamble=(),
|
|
648
|
+
)
|
|
649
|
+
),
|
|
650
|
+
PurePosixPath(SCHEMA_PACKAGE, "__init__.py"): _generated(
|
|
651
|
+
unparse(
|
|
652
|
+
emit_schema_package(self._schema, schema_spellings=schema_spellings)
|
|
653
|
+
)
|
|
654
|
+
),
|
|
655
|
+
PurePosixPath(
|
|
656
|
+
SCHEMA_PACKAGE, f"{schema_spellings.enum_module}.py"
|
|
657
|
+
): _generated(
|
|
658
|
+
self._module_source(
|
|
659
|
+
emit_enums(self._schema, schema_spellings=schema_spellings),
|
|
660
|
+
generated_module=PackageModule(path=(SCHEMA_PACKAGE,)),
|
|
661
|
+
preamble=(),
|
|
662
|
+
)
|
|
663
|
+
),
|
|
664
|
+
PurePosixPath(
|
|
665
|
+
SCHEMA_PACKAGE, f"{schema_spellings.input_module}.py"
|
|
666
|
+
): _generated(
|
|
667
|
+
self._module_source(
|
|
668
|
+
emit_inputs(
|
|
669
|
+
self._schema,
|
|
670
|
+
annotations=self._annotations,
|
|
671
|
+
schema_spellings=schema_spellings,
|
|
672
|
+
injector_names=self._injector_names,
|
|
673
|
+
),
|
|
674
|
+
generated_module=PackageModule(path=(SCHEMA_PACKAGE,)),
|
|
675
|
+
preamble=[FUTURE_ANNOTATIONS],
|
|
676
|
+
)
|
|
677
|
+
),
|
|
678
|
+
**document_files,
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if self._injected_types:
|
|
682
|
+
files[PurePosixPath(f"{INJECTION_MODULE}.py")] = _generated(
|
|
683
|
+
self._module_source(
|
|
684
|
+
emit_injector_module(
|
|
685
|
+
self._injected_types, annotations=self._annotations
|
|
686
|
+
),
|
|
687
|
+
generated_module=PackageModule(path=()),
|
|
688
|
+
preamble=(),
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
**_RUNTIME_FILES,
|
|
694
|
+
**{path: source.encode() for path, source in files.items()},
|
|
695
|
+
}
|