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,166 @@
1
+ import ast
2
+ from collections.abc import Sequence
3
+ from dataclasses import dataclass
4
+ from typing import assert_never, final
5
+
6
+ from graphql_codegen._generator._ast_nodes import import_as
7
+ from graphql_codegen._generator._naming import (
8
+ ABC,
9
+ BUILTINS,
10
+ COMPAT,
11
+ ENUM,
12
+ INJECTION,
13
+ INPUT,
14
+ OMITTED,
15
+ REFLECTION,
16
+ RUNTIME,
17
+ SCALAR,
18
+ TYPING,
19
+ SchemaSpellings,
20
+ )
21
+ from graphql_codegen._generator.spelling import ast_str
22
+
23
+ SCHEMA_PACKAGE = "schema"
24
+ DOCUMENT_PACKAGE = "document"
25
+ SCALAR_MODULE = "_scalar"
26
+ INJECTION_MODULE = "injection"
27
+ RUNTIME_PACKAGE = "runtime"
28
+
29
+
30
+ @final
31
+ @dataclass(frozen=True, kw_only=True)
32
+ class PackageModule:
33
+ path: tuple[str, ...]
34
+
35
+
36
+ @final
37
+ @dataclass(frozen=True, kw_only=True)
38
+ class OutsideModule:
39
+ package: str
40
+
41
+
42
+ type GeneratedModule = PackageModule | OutsideModule
43
+ """A generated module, by where it sits, which decides how it imports the package's own modules.
44
+
45
+ - :class:`PackageModule`: relatively, so that the package works wherever it is written.
46
+ - :class:`OutsideModule`: absolutely, since a relative import cannot leave its top-level package.
47
+
48
+ """
49
+
50
+
51
+ def _import_from(
52
+ module: str | None, /, *, name: str, alias: str, level: int
53
+ ) -> ast.ImportFrom:
54
+ return ast.ImportFrom(
55
+ module=module, names=[ast.alias(name=name, asname=alias)], level=level
56
+ )
57
+
58
+
59
+ def _from_package(
60
+ submodule: str | None, /, *, name: str, alias: str, module: GeneratedModule
61
+ ) -> ast.ImportFrom:
62
+ match module:
63
+ case PackageModule(path=path):
64
+ # How far up the package is.
65
+ return _import_from(submodule, name=name, alias=alias, level=len(path) + 1)
66
+ case OutsideModule(package=package):
67
+ return _import_from(
68
+ package if submodule is None else f"{package}.{submodule}",
69
+ name=name,
70
+ alias=alias,
71
+ level=0,
72
+ )
73
+ case _ as never:
74
+ assert_never(never)
75
+
76
+
77
+ def helper_imports(
78
+ statements: Sequence[ast.stmt],
79
+ /,
80
+ *,
81
+ module: GeneratedModule,
82
+ schema_spellings: SchemaSpellings,
83
+ ) -> list[ast.stmt]:
84
+ used = {
85
+ node.id
86
+ for statement in statements
87
+ for node in ast.walk(statement)
88
+ if isinstance(node, ast.Name)
89
+ }
90
+ imports: list[ast.stmt] = []
91
+
92
+ for helper, module_name in (
93
+ (BUILTINS, "builtins"),
94
+ (ABC, "collections.abc"),
95
+ (TYPING, "typing"),
96
+ ):
97
+ if ast_str(helper) in used:
98
+ imports.append(import_as(module_name, helper))
99
+
100
+ # From the runtime's copy, so that the package needs `typing_extensions` only before Python 3.15.
101
+ if ast_str(COMPAT) in used:
102
+ imports.append(
103
+ _from_package(
104
+ RUNTIME_PACKAGE, name="_compat", alias=ast_str(COMPAT), module=module
105
+ )
106
+ )
107
+
108
+ if ast_str(INJECTION) in used:
109
+ imports.append(
110
+ _from_package(
111
+ RUNTIME_PACKAGE,
112
+ name="injection",
113
+ alias=ast_str(INJECTION),
114
+ module=module,
115
+ )
116
+ )
117
+
118
+ if ast_str(REFLECTION) in used:
119
+ imports.append(
120
+ _from_package(
121
+ RUNTIME_PACKAGE,
122
+ name="_reflection",
123
+ alias=ast_str(REFLECTION),
124
+ module=module,
125
+ )
126
+ )
127
+
128
+ if ast_str(RUNTIME) in used:
129
+ imports.append(
130
+ _from_package(
131
+ None, name=RUNTIME_PACKAGE, alias=ast_str(RUNTIME), module=module
132
+ )
133
+ )
134
+
135
+ if ast_str(OMITTED) in used:
136
+ imports.append(
137
+ _from_package(
138
+ RUNTIME_PACKAGE, name="OMITTED", alias=ast_str(OMITTED), module=module
139
+ )
140
+ )
141
+
142
+ for helper, schema_module in (
143
+ (ENUM, schema_spellings.enum_module),
144
+ (INPUT, schema_spellings.input_module),
145
+ ):
146
+ if ast_str(helper) in used:
147
+ imports.append(
148
+ # A module of the `schema` package reaches its siblings directly.
149
+ _import_from(None, name=schema_module, alias=ast_str(helper), level=1)
150
+ if module == PackageModule(path=(SCHEMA_PACKAGE,))
151
+ else _from_package(
152
+ SCHEMA_PACKAGE,
153
+ name=schema_module,
154
+ alias=ast_str(helper),
155
+ module=module,
156
+ ),
157
+ )
158
+
159
+ if ast_str(SCALAR) in used:
160
+ imports.append(
161
+ _from_package(
162
+ None, name=SCALAR_MODULE, alias=ast_str(SCALAR), module=module
163
+ )
164
+ )
165
+
166
+ return imports
@@ -0,0 +1,267 @@
1
+ import ast
2
+ from collections.abc import Mapping, Set as AbstractSet
3
+ from functools import reduce
4
+ from typing import cast
5
+
6
+ from graphql import (
7
+ GraphQLError,
8
+ GraphQLInputObjectType,
9
+ GraphQLInputType,
10
+ GraphQLList,
11
+ GraphQLNonNull,
12
+ GraphQLSchema,
13
+ GraphQLType,
14
+ OperationDefinitionNode,
15
+ VariableDefinitionNode,
16
+ get_named_type,
17
+ is_list_type,
18
+ is_non_null_type,
19
+ type_from_ast,
20
+ )
21
+
22
+ from graphql_codegen._generator._annotation import AnnotationBuilder
23
+ from graphql_codegen._generator._ast_nodes import (
24
+ docstring,
25
+ name,
26
+ qualified,
27
+ subscript,
28
+ union,
29
+ )
30
+ from graphql_codegen._generator._naming import ABC, INJECTION, OMITTED
31
+ from graphql_codegen._generator._schema import as_non_null
32
+ from graphql_codegen._generator._typed_dict import Key, emit_closed
33
+ from graphql_codegen._generator.spelling import SettledSpelling
34
+
35
+
36
+ def get_injections(
37
+ operation: OperationDefinitionNode,
38
+ /,
39
+ *,
40
+ schema: GraphQLSchema,
41
+ injector_names: AbstractSet[str],
42
+ ) -> dict[str, dict[tuple[str, ...], GraphQLInputType]]:
43
+ """An injected variable goes in the variables themselves, `()`.
44
+
45
+ An injected field goes in every input object holding it, at any depth, since a mutation takes the transaction it belongs to inside its input rather than beside it.
46
+ A static path can say neither "every element of this list" nor the unbounded depth of a recursive input type, so an injected field reached that way fails generation rather than being silently left out.
47
+ """
48
+ injections: dict[str, dict[tuple[str, ...], GraphQLInputType]] = {}
49
+
50
+ def holds_injected_field(
51
+ input_type: GraphQLInputObjectType, /, *, seen: set[str]
52
+ ) -> bool:
53
+ if input_type.name in seen:
54
+ return False
55
+
56
+ seen.add(input_type.name)
57
+ return any(
58
+ field_name in injector_names
59
+ or (
60
+ isinstance(
61
+ named_type := get_named_type(field.type), GraphQLInputObjectType
62
+ )
63
+ and holds_injected_field(named_type, seen=seen)
64
+ )
65
+ for field_name, field in input_type.fields.items()
66
+ )
67
+
68
+ def visit_input(
69
+ input_type: GraphQLInputObjectType,
70
+ wrapped_type: GraphQLType,
71
+ /,
72
+ *,
73
+ path: tuple[str, ...],
74
+ ancestors: tuple[str, ...],
75
+ definition: VariableDefinitionNode,
76
+ ) -> None:
77
+ nullable_type = (
78
+ wrapped_type.of_type
79
+ if isinstance(wrapped_type, GraphQLNonNull)
80
+ else wrapped_type
81
+ )
82
+
83
+ if is_list_type(nullable_type) or input_type.name in ancestors:
84
+ if holds_injected_field(input_type, seen=set()):
85
+ raise GraphQLError(
86
+ f"Cannot inject into `${path[0]}`: an injected field is reached through a list or a recursive input type, which no static path can express.",
87
+ nodes=definition,
88
+ )
89
+ return
90
+
91
+ for field_name, field in input_type.fields.items():
92
+ if field_name in injector_names:
93
+ injections.setdefault(field_name, {})[path] = field.type
94
+ continue
95
+
96
+ named_type = get_named_type(field.type)
97
+
98
+ if isinstance(named_type, GraphQLInputObjectType):
99
+ visit_input(
100
+ named_type,
101
+ field.type,
102
+ path=(*path, field_name),
103
+ ancestors=(*ancestors, input_type.name),
104
+ definition=definition,
105
+ )
106
+
107
+ for definition in operation.variable_definitions or ():
108
+ variable_name = definition.variable.name.value
109
+
110
+ variable_type = type_from_ast(schema, definition.type)
111
+ assert variable_type is not None, (
112
+ "Validation guarantees that a variable's type exists."
113
+ )
114
+
115
+ if variable_name in injector_names:
116
+ # Validation guarantees that a variable's type is an input type.
117
+ injections.setdefault(variable_name, {})[()] = cast(
118
+ "GraphQLInputType", variable_type
119
+ )
120
+ continue
121
+ named_type = get_named_type(variable_type)
122
+
123
+ if isinstance(named_type, GraphQLInputObjectType):
124
+ visit_input(
125
+ named_type,
126
+ variable_type,
127
+ path=(variable_name,),
128
+ ancestors=(),
129
+ definition=definition,
130
+ )
131
+
132
+ return injections
133
+
134
+
135
+ def get_injected_types(
136
+ injections: Mapping[str, Mapping[str, Mapping[tuple[str, ...], GraphQLInputType]]],
137
+ /,
138
+ *,
139
+ injector_names: AbstractSet[str],
140
+ ) -> dict[str, GraphQLInputType]:
141
+ """An injector supplies one value for every position it is injected into, so they must all have the same type, nullability aside.
142
+
143
+ Its type is then the strictest: non-null wherever any position is, since a non-null value suits a nullable position too, while an injector cannot tell which position it is called for.
144
+ """
145
+ positions: dict[str, dict[str, str]] = {}
146
+ types: dict[str, list[GraphQLInputType]] = {}
147
+
148
+ for operation_name, operation_injections in injections.items():
149
+ for injector_name, targets in operation_injections.items():
150
+ for path, type_ in targets.items():
151
+ position = "$" + ".".join((*path, injector_name))
152
+ # Spelled without `!`, so that positions differing only by nullability agree.
153
+ spelling = str(type_).replace("!", "")
154
+ positions.setdefault(injector_name, {}).setdefault(
155
+ spelling, f"`{position}` in `{operation_name}`"
156
+ )
157
+ types.setdefault(injector_name, []).append(type_)
158
+
159
+ for injector_name, by_type in positions.items():
160
+ if len(by_type) > 1:
161
+ raise GraphQLError(
162
+ f"Expected `{injector_name}` to have one type, nullability aside, wherever it is injected, but it is "
163
+ + " and ".join(
164
+ f"`{type_}` at {position}" for type_, position in by_type.items()
165
+ )
166
+ + "."
167
+ )
168
+
169
+ # An injector injected nowhere, a misspelled one for instance, has no position to take its type from.
170
+ if nowhere := sorted(injector_names - types.keys()):
171
+ raise ValueError(
172
+ f"Expected `{nowhere[0]}` to be injected somewhere, but no operation sends a variable or an input field of that name."
173
+ )
174
+
175
+ return {
176
+ injector_name: reduce(_strictest, injector_types)
177
+ for injector_name, injector_types in types.items()
178
+ }
179
+
180
+
181
+ def _strictest(left: GraphQLInputType, right: GraphQLInputType, /) -> GraphQLInputType:
182
+ non_null = isinstance(left, GraphQLNonNull) or isinstance(right, GraphQLNonNull)
183
+ left = left.of_type if isinstance(left, GraphQLNonNull) else left
184
+ right = right.of_type if isinstance(right, GraphQLNonNull) else right
185
+ merged = (
186
+ GraphQLList(_strictest(left.of_type, right.of_type))
187
+ if isinstance(left, GraphQLList) and isinstance(right, GraphQLList)
188
+ else left
189
+ )
190
+ return as_non_null(merged) if non_null else merged
191
+
192
+
193
+ def emit_injector_module(
194
+ injected_types: Mapping[str, GraphQLInputType], /, *, annotations: AnnotationBuilder
195
+ ) -> list[ast.stmt]:
196
+ """The mapping of functions is a :class:`typing.TypedDict`, rather than keyword arguments, so that a name no keyword can spell is injected too, and a misspelled name fails type checking with every type checker.
197
+
198
+ It builds the runtime's private :class:`~graphql_codegen.runtime.injection._Injectors` rather than being given to a client, so that a client only calls injectors that were type checked.
199
+ """
200
+ functions_spelling, function_spelling = (
201
+ SettledSpelling("InjectorFunctions"),
202
+ SettledSpelling("injectors"),
203
+ )
204
+ # Local to the function, so that nothing can clash with it.
205
+ functions = SettledSpelling("functions")
206
+ keys: list[Key] = []
207
+
208
+ for injector_name, type_ in sorted(injected_types.items()):
209
+ value = annotations.build(
210
+ type_, position="input", object_annotation=None, bare_inputs=False
211
+ )
212
+
213
+ # A nullable input accepts omission too, while a non-null one accepts neither.
214
+ if not is_non_null_type(type_):
215
+ value = union(value, name(OMITTED))
216
+
217
+ keys.append(
218
+ Key(
219
+ name=injector_name,
220
+ annotation=subscript(
221
+ qualified(ABC, "Callable"), ast.List(elts=[], ctx=ast.Load()), value
222
+ ),
223
+ required=False,
224
+ description="",
225
+ ),
226
+ )
227
+
228
+ return [
229
+ *emit_closed(
230
+ functions_spelling,
231
+ keys,
232
+ description="The functions supplying each injected value, by name.\n\nWhere the value may be null, one returning `OMITTED` leaves it out, and one returning `None` sends `null`.",
233
+ bare_names=frozenset({functions_spelling, function_spelling}),
234
+ ),
235
+ ast.FunctionDef(
236
+ name=function_spelling,
237
+ args=ast.arguments(
238
+ posonlyargs=[
239
+ ast.arg(arg=functions, annotation=name(functions_spelling))
240
+ ],
241
+ args=[],
242
+ kwonlyargs=[],
243
+ kw_defaults=[],
244
+ defaults=[],
245
+ ),
246
+ body=[
247
+ docstring(
248
+ "Return what a client calls to supply the injected values, each serialized as its type says."
249
+ ),
250
+ ast.Return(
251
+ value=ast.Call(
252
+ func=qualified(INJECTION, "_Injectors"),
253
+ args=[name(functions)],
254
+ keywords=[
255
+ ast.keyword(
256
+ arg="injector_functions_type",
257
+ value=name(functions_spelling),
258
+ )
259
+ ],
260
+ ),
261
+ ),
262
+ ],
263
+ decorator_list=[],
264
+ returns=qualified(INJECTION, "_Injectors"),
265
+ type_params=[],
266
+ ),
267
+ ]
@@ -0,0 +1,149 @@
1
+ from dataclasses import replace
2
+ from functools import cache
3
+ from typing import Final
4
+
5
+ from graphql import (
6
+ DocumentNode,
7
+ FieldNode,
8
+ FragmentDefinitionNode,
9
+ FragmentSpreadNode,
10
+ InlineFragmentNode,
11
+ NameNode,
12
+ Node,
13
+ OperationDefinitionNode,
14
+ OperationType,
15
+ SelectionNode,
16
+ SelectionSetNode,
17
+ VariableNode,
18
+ Visitor,
19
+ print_ast,
20
+ visit,
21
+ )
22
+
23
+ from graphql_codegen._generator._document import get_transitively_spread_fragments
24
+ from graphql_codegen.runtime._sigil import SIGIL
25
+
26
+ # No printed name can hold it, and a string escapes it, so it only ever stands for a sigil.
27
+ _PLACEHOLDER: Final = "\x00"
28
+ _ESCAPED_SIGIL: Final = "\\u" + f"{ord(SIGIL):04x}"
29
+
30
+
31
+ class _References(Visitor):
32
+ def __init__(self) -> None:
33
+ super().__init__()
34
+ self.fragment_names: list[str] = []
35
+ self.uses_variables = False
36
+
37
+ def enter_fragment_spread(self, node: FragmentSpreadNode, *_args: object) -> None:
38
+ self.fragment_names.append(node.name.value)
39
+
40
+ def enter_variable(self, _node: VariableNode, *_args: object) -> None:
41
+ self.uses_variables = True
42
+
43
+
44
+ def _references(node: Node, /) -> _References:
45
+ references = _References()
46
+ visit(node, references)
47
+ return references
48
+
49
+
50
+ class _SuffixVariables(Visitor):
51
+ def enter_variable(self, node: VariableNode, *_args: object) -> VariableNode:
52
+ return VariableNode(name=NameNode(value=f"{node.name.value}{_PLACEHOLDER}"))
53
+
54
+
55
+ def print_merge_template(document: DocumentNode, /) -> str:
56
+ """Print *document*, holding one operation and the fragments it uses as the server receives them, with the sigil wherever its index in a merge goes, so that merging at runtime is string replacement.
57
+
58
+ After the response name of each root field, as its alias, and after the name of each variable, in its definition and wherever it is used.
59
+ The sigil is written as a character no printed name or string can hold, then swapped in once a literal sigil in a string has been escaped, so that every sigil left is one.
60
+ A subscription, which selects a single root field, and an operation with directives of its own, which would apply to the whole merge, cannot be merged: they get no sigil.
61
+
62
+ Two things cannot keep their shape in a merge, and are inlined as inline fragments, which the server executes the same:
63
+
64
+ - a fragment spread at the root, since the fields it selects are root fields and need aliasing;
65
+ - a fragment using variables, since each operation of a merge suffixes its variables differently, while a fragment is defined once for all of them.
66
+ """
67
+ (operation,) = (
68
+ definition
69
+ for definition in document.definitions
70
+ if isinstance(definition, OperationDefinitionNode)
71
+ )
72
+
73
+ if operation.operation is OperationType.SUBSCRIPTION or operation.directives:
74
+ return print_ast(document).replace(SIGIL, _ESCAPED_SIGIL)
75
+
76
+ fragments = {
77
+ definition.name.value: definition
78
+ for definition in document.definitions
79
+ if isinstance(definition, FragmentDefinitionNode)
80
+ }
81
+
82
+ @cache
83
+ def uses_variables(fragment_name: str, /) -> bool:
84
+ references = _references(fragments[fragment_name])
85
+ return references.uses_variables or any(
86
+ map(uses_variables, references.fragment_names)
87
+ )
88
+
89
+ def rewrite(selection_set: SelectionSetNode, /, *, root: bool) -> SelectionSetNode:
90
+ selections: list[SelectionNode] = []
91
+
92
+ for selection in selection_set.selections:
93
+ match selection:
94
+ case FragmentSpreadNode() if root or uses_variables(
95
+ selection.name.value
96
+ ):
97
+ fragment = fragments[selection.name.value]
98
+ selections.append(
99
+ InlineFragmentNode(
100
+ type_condition=fragment.type_condition,
101
+ directives=selection.directives,
102
+ selection_set=rewrite(fragment.selection_set, root=root),
103
+ ),
104
+ )
105
+ case FieldNode() if root:
106
+ selections.append(
107
+ replace(
108
+ selection,
109
+ alias=NameNode(
110
+ value=f"{(selection.alias or selection.name).value}{_PLACEHOLDER}"
111
+ ),
112
+ selection_set=None
113
+ if selection.selection_set is None
114
+ else rewrite(selection.selection_set, root=False),
115
+ ),
116
+ )
117
+ case FieldNode(selection_set=SelectionSetNode() as field_selection_set):
118
+ selections.append(
119
+ replace(
120
+ selection,
121
+ selection_set=rewrite(field_selection_set, root=False),
122
+ )
123
+ )
124
+ case InlineFragmentNode():
125
+ # The fields of an inline fragment at the root are root fields too.
126
+ selections.append(
127
+ replace(
128
+ selection,
129
+ selection_set=rewrite(selection.selection_set, root=root),
130
+ )
131
+ )
132
+ case _:
133
+ selections.append(selection)
134
+
135
+ return SelectionSetNode(selections=tuple(selections))
136
+
137
+ templated = visit(
138
+ replace(operation, selection_set=rewrite(operation.selection_set, root=True)),
139
+ _SuffixVariables(),
140
+ )
141
+ text = print_ast(
142
+ DocumentNode(
143
+ definitions=(
144
+ templated,
145
+ *get_transitively_spread_fragments(templated, definitions=fragments),
146
+ )
147
+ )
148
+ )
149
+ return text.replace(SIGIL, _ESCAPED_SIGIL).replace(_PLACEHOLDER, SIGIL)
@@ -0,0 +1,80 @@
1
+ from collections.abc import Collection, Mapping, Sequence
2
+ from dataclasses import dataclass
3
+ from typing import Final, final
4
+
5
+ from graphql_codegen._generator.spelling import (
6
+ PendingSpelling,
7
+ SettledSpelling,
8
+ settle_names,
9
+ )
10
+
11
+ COMPAT: Final = PendingSpelling("_compat")
12
+ TYPING: Final = PendingSpelling("_typing")
13
+ ABC: Final = PendingSpelling("_abc")
14
+ BUILTINS: Final = PendingSpelling("_builtins")
15
+ RUNTIME: Final = PendingSpelling("_runtime")
16
+ ENUM: Final = PendingSpelling("_enum")
17
+ INPUT: Final = PendingSpelling("_input")
18
+ INJECTION: Final = PendingSpelling("_injection")
19
+ REFLECTION: Final = PendingSpelling("_reflection")
20
+ SCALAR: Final = PendingSpelling("_scalar")
21
+ OMITTED: Final = PendingSpelling("_OMITTED")
22
+ """The runtime's sentinel, imported by name: pyright only takes a sentinel as a type when it is spelled as a bare name."""
23
+
24
+
25
+ def nested_type_name(path: Sequence[str], /) -> str:
26
+ """Private, since the GraphQL way for code to name a shape is a fragment.
27
+
28
+ Prefixed by the root, so that a type checker message naming the type also names its operation or fragment.
29
+ """
30
+ return f"_{'_'.join(path)}"
31
+
32
+
33
+ @final
34
+ @dataclass(frozen=True, kw_only=True)
35
+ class OperationSpellings:
36
+ operation: SettledSpelling
37
+ variables: SettledSpelling
38
+ data: SettledSpelling
39
+
40
+
41
+ def operation_spellings(
42
+ operations: Sequence[SettledSpelling], /, *, taken: Collection[SettledSpelling]
43
+ ) -> dict[SettledSpelling, OperationSpellings]:
44
+ """The operations keep theirs, so that a type is suffixed rather than an operation: `Get`'s data type is `GetData_1` in a module also holding an operation `GetData`."""
45
+ types = settle_names(
46
+ [
47
+ f"{operation}{suffix}"
48
+ for operation in operations
49
+ for suffix in ("Variables", "Data")
50
+ ],
51
+ taken={*operations, *taken},
52
+ )
53
+ return {
54
+ operation: OperationSpellings(
55
+ operation=operation,
56
+ variables=types[f"{operation}Variables"],
57
+ data=types[f"{operation}Data"],
58
+ )
59
+ for operation in operations
60
+ }
61
+
62
+
63
+ @final
64
+ @dataclass(frozen=True, kw_only=True)
65
+ class SchemaSpellings:
66
+ type_spellings: Mapping[str, SettledSpelling]
67
+
68
+ enum_module: SettledSpelling
69
+ input_module: SettledSpelling
70
+
71
+
72
+ def schema_spellings(type_names: Collection[str], /) -> SchemaSpellings:
73
+ types = settle_names(sorted(type_names), taken=set())
74
+ # The package re-exports every type, so its modules must not be spelled like any of them.
75
+ modules = settle_names(["enum", "input"], taken=set(types.values()))
76
+ return SchemaSpellings(
77
+ type_spellings=types,
78
+ enum_module=modules["enum"],
79
+ input_module=modules["input"],
80
+ )