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,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