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.
Files changed (55) hide show
  1. graphql_codegen/__init__.py +7 -0
  2. graphql_codegen/__main__.py +5 -0
  3. graphql_codegen/_cli/__init__.py +41 -0
  4. graphql_codegen/_cli/_graphql_config.py +291 -0
  5. graphql_codegen/_cli/_introspection.py +87 -0
  6. graphql_codegen/_cli/_introspection_graphql.py +53 -0
  7. graphql_codegen/_cli/_parsing.py +67 -0
  8. graphql_codegen/_cli/_schema_pointer.py +108 -0
  9. graphql_codegen/_cli/_source.py +68 -0
  10. graphql_codegen/_cli/_write.py +22 -0
  11. graphql_codegen/_generator/__init__.py +0 -0
  12. graphql_codegen/_generator/_annotation.py +149 -0
  13. graphql_codegen/_generator/_ast_nodes.py +235 -0
  14. graphql_codegen/_generator/_data_type.py +532 -0
  15. graphql_codegen/_generator/_document.py +108 -0
  16. graphql_codegen/_generator/_document_module.py +54 -0
  17. graphql_codegen/_generator/_imports.py +166 -0
  18. graphql_codegen/_generator/_injector.py +267 -0
  19. graphql_codegen/_generator/_merge.py +149 -0
  20. graphql_codegen/_generator/_naming.py +80 -0
  21. graphql_codegen/_generator/_operation.py +223 -0
  22. graphql_codegen/_generator/_scalar.py +76 -0
  23. graphql_codegen/_generator/_schema.py +46 -0
  24. graphql_codegen/_generator/_schema_type.py +334 -0
  25. graphql_codegen/_generator/_selection.py +246 -0
  26. graphql_codegen/_generator/_structs.py +87 -0
  27. graphql_codegen/_generator/_typed_dict.py +154 -0
  28. graphql_codegen/_generator/dotted_name.py +48 -0
  29. graphql_codegen/_generator/package.py +695 -0
  30. graphql_codegen/_generator/spelling.py +168 -0
  31. graphql_codegen/_metadata.py +11 -0
  32. graphql_codegen/_note.py +12 -0
  33. graphql_codegen/config.py +42 -0
  34. graphql_codegen/document_sibling_module.py +58 -0
  35. graphql_codegen/generate.py +19 -0
  36. graphql_codegen/package_location.py +41 -0
  37. graphql_codegen/py.typed +0 -0
  38. graphql_codegen/runtime/__init__.py +20 -0
  39. graphql_codegen/runtime/_compat.py +27 -0
  40. graphql_codegen/runtime/_literal.py +18 -0
  41. graphql_codegen/runtime/_merge.py +153 -0
  42. graphql_codegen/runtime/_prepare.py +250 -0
  43. graphql_codegen/runtime/_reflection.py +376 -0
  44. graphql_codegen/runtime/_sigil.py +11 -0
  45. graphql_codegen/runtime/_transport.py +21 -0
  46. graphql_codegen/runtime/client.py +384 -0
  47. graphql_codegen/runtime/error.py +135 -0
  48. graphql_codegen/runtime/injection.py +107 -0
  49. graphql_codegen/runtime/operation.py +135 -0
  50. graphql_codegen/scalar.py +59 -0
  51. graphql_codegen-0.1.0.dist-info/METADATA +871 -0
  52. graphql_codegen-0.1.0.dist-info/RECORD +55 -0
  53. graphql_codegen-0.1.0.dist-info/WHEEL +4 -0
  54. graphql_codegen-0.1.0.dist-info/entry_points.txt +3 -0
  55. 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
+ }