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,223 @@
1
+ import ast
2
+ from collections.abc import Collection, Mapping, Sequence, Set as AbstractSet
3
+ from dataclasses import replace
4
+ from typing import Final
5
+
6
+ from graphql import (
7
+ REMOVE,
8
+ DirectiveNode,
9
+ DocumentNode,
10
+ ExecutableDefinitionNode,
11
+ FragmentDefinitionNode,
12
+ GraphQLSchema,
13
+ OperationDefinitionNode,
14
+ StringValueNode,
15
+ VariableDefinitionNode,
16
+ Visitor,
17
+ is_non_null_type,
18
+ type_from_ast,
19
+ visit,
20
+ )
21
+
22
+ from graphql_codegen._generator._annotation import AnnotationBuilder
23
+ from graphql_codegen._generator._ast_nodes import (
24
+ constant,
25
+ documented,
26
+ name,
27
+ qualified,
28
+ subscript,
29
+ )
30
+ from graphql_codegen._generator._naming import (
31
+ BUILTINS,
32
+ RUNTIME,
33
+ TYPING,
34
+ OperationSpellings,
35
+ )
36
+ from graphql_codegen._generator._typed_dict import Key, emit_closed
37
+
38
+
39
+ def description_text(description: StringValueNode | None, /) -> str:
40
+ return "" if description is None else description.value
41
+
42
+
43
+ def _undescribed[Node: ExecutableDefinitionNode | VariableDefinitionNode](
44
+ node: Node, /
45
+ ) -> Node | None:
46
+ if node.description is None:
47
+ return None
48
+
49
+ return replace(node, description=None)
50
+
51
+
52
+ class _StripForServer(Visitor):
53
+ """Remove what the server does not need: this library's directives, and descriptions.
54
+
55
+ The spec says descriptions must not affect execution, so on the wire they would only cost bytes, and a server whose parser predates them would reject the whole request.
56
+ They become docstrings instead.
57
+
58
+ Block strings are printed as ordinary ones, with the same value, so that no token spans lines, since the runtime slices a merge template at blank lines, and so that a sigil in a string can be escaped, which a block string cannot do.
59
+ """
60
+
61
+ def __init__(self, directive_names: AbstractSet[str], /) -> None:
62
+ super().__init__()
63
+ self._directive_names: Final = directive_names
64
+
65
+ def enter_directive(self, node: DirectiveNode, *_args: object) -> object:
66
+ return REMOVE if node.name.value in self._directive_names else None
67
+
68
+ def enter_string_value(self, node: StringValueNode, *_args: object) -> object:
69
+ if not node.block:
70
+ return None
71
+
72
+ return replace(node, block=False)
73
+
74
+ def enter_operation_definition(
75
+ self, node: OperationDefinitionNode, *_args: object
76
+ ) -> object:
77
+ return _undescribed(node)
78
+
79
+ def enter_fragment_definition(
80
+ self, node: FragmentDefinitionNode, *_args: object
81
+ ) -> object:
82
+ return _undescribed(node)
83
+
84
+ def enter_variable_definition(
85
+ self, node: VariableDefinitionNode, *_args: object
86
+ ) -> object:
87
+ return _undescribed(node)
88
+
89
+
90
+ def strip_for_server(
91
+ operation: OperationDefinitionNode,
92
+ /,
93
+ *,
94
+ fragments: Sequence[FragmentDefinitionNode],
95
+ client_directive_names: AbstractSet[str],
96
+ ) -> DocumentNode:
97
+ document = DocumentNode(definitions=(operation, *fragments))
98
+ result = visit(document, _StripForServer(client_directive_names))
99
+ assert isinstance(result, DocumentNode)
100
+ return result
101
+
102
+
103
+ def emit_variables_type(
104
+ operation: OperationDefinitionNode,
105
+ /,
106
+ *,
107
+ spellings: OperationSpellings,
108
+ schema: GraphQLSchema,
109
+ annotations: AnnotationBuilder,
110
+ injector_names: AbstractSet[str],
111
+ ) -> list[ast.stmt]:
112
+ """A variable is :data:`typing.Required` only when it is non-null *and* has no default, for the same reason as an input field: a default makes it optional for the client, and the document carries the default so Python never needs to."""
113
+ keys: list[Key] = []
114
+
115
+ for definition in operation.variable_definitions or ():
116
+ variable_name = definition.variable.name.value
117
+
118
+ if variable_name in injector_names:
119
+ # Injected: the client inserts it, so a caller must not be able to.
120
+ continue
121
+
122
+ variable_type = type_from_ast(schema, definition.type)
123
+ assert variable_type is not None, (
124
+ "Validation guarantees that a variable's type exists."
125
+ )
126
+ keys.append(
127
+ Key(
128
+ name=variable_name,
129
+ annotation=annotations.build(
130
+ variable_type,
131
+ position="input",
132
+ object_annotation=None,
133
+ bare_inputs=False,
134
+ ),
135
+ required=is_non_null_type(variable_type)
136
+ and definition.default_value is None,
137
+ description=description_text(definition.description),
138
+ ),
139
+ )
140
+
141
+ return emit_closed(
142
+ spellings.variables,
143
+ keys,
144
+ description="",
145
+ bare_names=frozenset(),
146
+ )
147
+
148
+
149
+ def _frozenset(elements: Sequence[ast.expr], /) -> ast.expr:
150
+ return ast.Call(
151
+ func=qualified(BUILTINS, "frozenset"),
152
+ args=[ast.Set(elts=list(elements))],
153
+ keywords=[],
154
+ )
155
+
156
+
157
+ def emit_operation_constant(
158
+ operation: OperationDefinitionNode,
159
+ /,
160
+ *,
161
+ graphql_name: str,
162
+ spellings: OperationSpellings,
163
+ document: str,
164
+ injections: Mapping[str, Collection[tuple[str, ...]]],
165
+ ) -> list[ast.stmt]:
166
+ """The operation's description is the constant's docstring, since the constant is what callers reach for.
167
+
168
+ It is annotated although its type parameters could be inferred from the call: pyrefly widens an inferred `Literal['mutation']` to :class:`str` in a list comprehension passed to the clients' overloads, which then match none.
169
+ The document is inlined rather than given a constant of its own: a triple quoted literal reads as GraphQL wherever it sits.
170
+ """
171
+ # In the order of the class's fields, which is the order things happen in.
172
+ keywords = [
173
+ ast.keyword(arg="operation_type", value=constant(operation.operation.value)),
174
+ # The GraphQL name, which is what the server knows the operation by.
175
+ ast.keyword(arg="name", value=constant(graphql_name)),
176
+ ast.keyword(arg="document", value=constant(document)),
177
+ # What crosses the wire is read off the variables and data types at runtime: they are the only description of it.
178
+ ast.keyword(arg="variables_type", value=name(spellings.variables)),
179
+ ]
180
+
181
+ if injections:
182
+ keywords.append(
183
+ ast.keyword(
184
+ arg="injections",
185
+ value=ast.Dict(
186
+ keys=[constant(injector_name) for injector_name in injections],
187
+ values=[
188
+ _frozenset(
189
+ [
190
+ ast.Tuple(
191
+ elts=[constant(part) for part in target],
192
+ ctx=ast.Load(),
193
+ )
194
+ for target in targets
195
+ ],
196
+ )
197
+ for targets in injections.values()
198
+ ],
199
+ ),
200
+ ),
201
+ )
202
+
203
+ keywords.append(ast.keyword(arg="data_type", value=name(spellings.data)))
204
+
205
+ # Not annotated `Final`: pyright would then reject the package re-exporting it from the module named like it.
206
+ return documented(
207
+ ast.AnnAssign(
208
+ target=ast.Name(id=spellings.operation, ctx=ast.Store()),
209
+ annotation=subscript(
210
+ qualified(RUNTIME, "Operation"),
211
+ subscript(
212
+ qualified(TYPING, "Literal"), constant(operation.operation.value)
213
+ ),
214
+ name(spellings.variables),
215
+ name(spellings.data),
216
+ ),
217
+ value=ast.Call(
218
+ func=qualified(RUNTIME, "Operation"), args=[], keywords=keywords
219
+ ),
220
+ simple=1,
221
+ ),
222
+ text=description_text(operation.description),
223
+ )
@@ -0,0 +1,76 @@
1
+ import ast
2
+ from collections.abc import Mapping
3
+ from typing import assert_never
4
+
5
+ from graphql import GraphQLScalarType, GraphQLSchema
6
+
7
+ from graphql_codegen._generator._ast_nodes import name, qualified, subscript, type_alias
8
+ from graphql_codegen._generator._naming import BUILTINS, REFLECTION, TYPING
9
+ from graphql_codegen._generator.dotted_name import Builtin, Import, Reference
10
+ from graphql_codegen._generator.spelling import (
11
+ PendingSpelling,
12
+ SettledSpelling,
13
+ ast_str,
14
+ )
15
+ from graphql_codegen.scalar import Scalar
16
+
17
+
18
+ def check_scalars(scalars: Mapping[str, Scalar], /, *, schema: GraphQLSchema) -> None:
19
+ for scalar_name in sorted(scalars):
20
+ if not isinstance(schema.type_map.get(scalar_name), GraphQLScalarType):
21
+ # A config value naming nothing in the schema, not a mistyped one.
22
+ raise ValueError(f"Expected `{scalar_name}` to be a scalar of the schema.") # noqa: TRY004
23
+
24
+
25
+ def emit_scalars(
26
+ scalars: Mapping[str, Scalar], /, *, spellings: Mapping[str, SettledSpelling]
27
+ ) -> list[ast.stmt]:
28
+ imported: dict[Import, PendingSpelling] = {}
29
+
30
+ def reference(path: Reference, /) -> ast.expr:
31
+ match path:
32
+ case Builtin(name=builtin_name):
33
+ return qualified(BUILTINS, builtin_name)
34
+ case Import(name=imported_name):
35
+ return name(
36
+ imported.setdefault(path, PendingSpelling(f"_{imported_name}"))
37
+ )
38
+ case _ as never:
39
+ assert_never(never)
40
+
41
+ body: list[ast.stmt] = []
42
+
43
+ for scalar_name, scalar in sorted(scalars.items()):
44
+ if scalar.codec is None:
45
+ value = reference(scalar._type_reference)
46
+ else:
47
+ value = subscript(
48
+ qualified(TYPING, "Annotated"),
49
+ reference(scalar._type_reference),
50
+ ast.Call(
51
+ func=qualified(REFLECTION, "Codec"),
52
+ args=[],
53
+ keywords=[
54
+ ast.keyword(
55
+ arg="decode",
56
+ value=reference(scalar.codec._decode_reference),
57
+ ),
58
+ ast.keyword(
59
+ arg="encode",
60
+ value=reference(scalar.codec._encode_reference),
61
+ ),
62
+ ],
63
+ ),
64
+ )
65
+
66
+ body.append(type_alias(spellings[scalar_name], value))
67
+
68
+ imports = [
69
+ ast.ImportFrom(
70
+ module=import_.module,
71
+ names=[ast.alias(name=import_.name, asname=ast_str(spelling))],
72
+ level=import_.level,
73
+ )
74
+ for import_, spelling in imported.items()
75
+ ]
76
+ return [*imports, *body]
@@ -0,0 +1,46 @@
1
+ from graphql import (
2
+ DirectiveLocation,
3
+ GraphQLDirective,
4
+ GraphQLNonNull,
5
+ GraphQLNullableType,
6
+ GraphQLSchema,
7
+ validate_schema,
8
+ )
9
+
10
+
11
+ def prepare_schema(
12
+ schema: GraphQLSchema, /, *, non_null_directive_name: str | None
13
+ ) -> GraphQLSchema:
14
+ """Declares the client directive, if any, so that documents using it validate."""
15
+ if non_null_directive_name is not None:
16
+ schema = _declare_directive(schema, name=non_null_directive_name)
17
+
18
+ # The schema operations are validated against, whose result graphql-core's `validate()` then reuses.
19
+ if errors := validate_schema(schema):
20
+ raise ExceptionGroup("The schema is invalid", errors)
21
+
22
+ return schema
23
+
24
+
25
+ def _declare_directive(schema: GraphQLSchema, /, *, name: str) -> GraphQLSchema:
26
+ if schema.get_directive(name) is not None:
27
+ raise ValueError(
28
+ f"Expected the schema to declare no `@{name}`, the name of the client directive."
29
+ )
30
+
31
+ directive = GraphQLDirective(
32
+ name,
33
+ locations=[DirectiveLocation.FIELD],
34
+ description="Assert that this field is not null, even though the schema allows it to be.",
35
+ )
36
+
37
+ # A new dictionary on each call, so setting a key of it leaves *schema* alone.
38
+ kwargs = schema.to_kwargs()
39
+ kwargs["directives"] = (*schema.directives, directive)
40
+ return GraphQLSchema(**kwargs)
41
+
42
+
43
+ def as_non_null[Nullable: GraphQLNullableType](
44
+ type_: Nullable | GraphQLNonNull[Nullable], /
45
+ ) -> GraphQLNonNull[Nullable]:
46
+ return type_ if isinstance(type_, GraphQLNonNull) else GraphQLNonNull(type_)
@@ -0,0 +1,334 @@
1
+ import ast
2
+ from collections.abc import Mapping, Set as AbstractSet
3
+
4
+ from graphql import (
5
+ DocumentNode,
6
+ FieldNode,
7
+ GraphQLEnumType,
8
+ GraphQLInputField,
9
+ GraphQLInputObjectType,
10
+ GraphQLNamedType,
11
+ GraphQLSchema,
12
+ TypeInfo,
13
+ TypeInfoVisitor,
14
+ Undefined,
15
+ VariableDefinitionNode,
16
+ Visitor,
17
+ get_named_type,
18
+ is_non_null_type,
19
+ type_from_ast,
20
+ visit,
21
+ )
22
+
23
+ from graphql_codegen._generator._annotation import AnnotationBuilder
24
+ from graphql_codegen._generator._ast_nodes import (
25
+ constant,
26
+ documented,
27
+ module,
28
+ name,
29
+ qualified,
30
+ subscript,
31
+ type_alias,
32
+ union,
33
+ )
34
+ from graphql_codegen._generator._naming import TYPING, SchemaSpellings
35
+ from graphql_codegen._generator._schema import as_non_null
36
+ from graphql_codegen._generator._structs import StructTypes, get_struct_input_name
37
+ from graphql_codegen._generator._typed_dict import Key, emit_closed
38
+ from graphql_codegen._generator.spelling import (
39
+ PendingSpelling,
40
+ SettledSpelling,
41
+ ast_str,
42
+ )
43
+
44
+
45
+ def _is_required(field: GraphQLInputField, /) -> bool:
46
+ """In GraphQL a non-null input field with a default is optional for the client: the `!` constrains the value, not whether one must be sent.
47
+
48
+ Reading `!` alone would force callers to pass values the server would happily supply.
49
+ """
50
+ # graphql-core holds a default given as a Python value in `default_value`, and one given as a GraphQL literal, as SDL gives it, in `default`.
51
+ return (
52
+ is_non_null_type(field.type)
53
+ and field.default_value is Undefined
54
+ and field.default is None
55
+ )
56
+
57
+
58
+ def _reached[Type: GraphQLNamedType](
59
+ schema: GraphQLSchema, kind: type[Type], /, *, schema_spellings: SchemaSpellings
60
+ ) -> list[Type]:
61
+ return [
62
+ type_
63
+ for type_ in schema.type_map.values()
64
+ if isinstance(type_, kind) and type_.name in schema_spellings.type_spellings
65
+ ]
66
+
67
+
68
+ def get_reached_type_names(
69
+ document: DocumentNode,
70
+ /,
71
+ *,
72
+ schema: GraphQLSchema,
73
+ struct_types: StructTypes | None,
74
+ injector_names: AbstractSet[str],
75
+ ) -> frozenset[str]:
76
+ """Only the enums and input types *document* sends or receives, so that the generated code grows with the documents rather than with the schema.
77
+
78
+ An input type is reached through a variable, and reaches the types of its fields in turn, a `@oneOf` input's members included.
79
+ An enum is reached like an input type, or when a field of its type is selected.
80
+ The input type of a struct payload is reached when the payload is selected, since the payload has its shape.
81
+ An injected field reaches nothing, since it is removed from its input type.
82
+ """
83
+ pending: list[GraphQLNamedType] = []
84
+ type_info = TypeInfo(schema)
85
+
86
+ class _Visitor(Visitor):
87
+ def enter_variable_definition(
88
+ self, node: VariableDefinitionNode, *_args: object
89
+ ) -> None:
90
+ variable_type = type_from_ast(schema, node.type)
91
+ assert variable_type is not None, (
92
+ "Validation guarantees that a variable's type exists."
93
+ )
94
+ pending.append(get_named_type(variable_type))
95
+
96
+ def enter_field(self, node: FieldNode, *_args: object) -> None:
97
+ field_type = type_info.get_type()
98
+ parent_type = type_info.get_parent_type()
99
+ assert field_type is not None and parent_type is not None, (
100
+ "Validation guarantees that a selected field exists."
101
+ )
102
+
103
+ pending.append(get_named_type(field_type))
104
+ struct_input_name = get_struct_input_name(
105
+ parent_type,
106
+ node.name.value,
107
+ struct_types=struct_types,
108
+ )
109
+
110
+ if struct_input_name is not None:
111
+ pending.append(schema.type_map[struct_input_name])
112
+
113
+ visit(document, TypeInfoVisitor(type_info, _Visitor()))
114
+
115
+ reached: set[str] = set()
116
+
117
+ while pending:
118
+ named_type = pending.pop()
119
+
120
+ if (
121
+ named_type.name in reached
122
+ or named_type.name.startswith("__")
123
+ or not isinstance(named_type, GraphQLEnumType | GraphQLInputObjectType)
124
+ ):
125
+ continue
126
+
127
+ reached.add(named_type.name)
128
+
129
+ if isinstance(named_type, GraphQLInputObjectType):
130
+ pending.extend(
131
+ get_named_type(field.type)
132
+ for field_name, field in named_type.fields.items()
133
+ if field_name not in injector_names
134
+ )
135
+
136
+ return frozenset(reached)
137
+
138
+
139
+ def emit_enums(
140
+ schema: GraphQLSchema, /, *, schema_spellings: SchemaSpellings
141
+ ) -> list[ast.stmt]:
142
+ """Closed, with no `"%future added value"` sentinel and no per-enum openness config, which costs far less here than in a validating generator.
143
+
144
+ A validating client turns a member it has not heard of into a hard failure, so a server adding one breaks every older client.
145
+ Nothing is validated here, so an unknown member reaches the caller as the string the server sent, and the `Literal` is guidance: a `match` may keep a catch-all arm, which really runs, or `assert_never`, which a type checker enforces.
146
+ """
147
+ return [
148
+ statement
149
+ for enum_type in _reached(
150
+ schema, GraphQLEnumType, schema_spellings=schema_spellings
151
+ )
152
+ for statement in documented(
153
+ type_alias(
154
+ schema_spellings.type_spellings[enum_type.name],
155
+ subscript(
156
+ qualified(TYPING, "Literal"),
157
+ *(constant(member) for member in enum_type.values),
158
+ ),
159
+ ),
160
+ text=enum_type.description or "",
161
+ )
162
+ ]
163
+
164
+
165
+ def _emit_one_of(
166
+ input_type: GraphQLInputObjectType,
167
+ /,
168
+ *,
169
+ fields: Mapping[str, GraphQLInputField],
170
+ annotations: AnnotationBuilder,
171
+ spelling: SettledSpelling,
172
+ bare_names: frozenset[str],
173
+ ) -> list[ast.stmt]:
174
+ """`closed=True` makes a type checker reject a value with two keys, the only enforcement there is, since nothing is validated at runtime.
175
+
176
+ `total=` could never express it: it governs whether *declared* keys are required, never whether *undeclared* ones are allowed.
177
+ """
178
+ body: list[ast.stmt] = []
179
+ member_spellings: list[PendingSpelling] = []
180
+
181
+ for field_name, field in fields.items():
182
+ member_spelling = PendingSpelling(f"_{spelling}_{field_name}")
183
+ member_spellings.append(member_spelling)
184
+ body.extend(
185
+ emit_closed(
186
+ member_spelling,
187
+ [
188
+ Key(
189
+ name=field_name,
190
+ # The spec declares every member nullable but rejects a null one, so the member that is set can never be `None`.
191
+ annotation=annotations.build(
192
+ as_non_null(field.type),
193
+ position="input",
194
+ bare_inputs=True,
195
+ object_annotation=None,
196
+ ),
197
+ description=field.description or "",
198
+ required=True,
199
+ ),
200
+ ],
201
+ description="",
202
+ bare_names=bare_names,
203
+ ),
204
+ )
205
+
206
+ assert member_spellings, f"`{input_type.name}` is `@oneOf` but has no field."
207
+ body.extend(
208
+ documented(
209
+ type_alias(
210
+ spelling,
211
+ union(*(name(member_spelling) for member_spelling in member_spellings)),
212
+ ),
213
+ text=input_type.description or "",
214
+ ),
215
+ )
216
+ return body
217
+
218
+
219
+ def emit_inputs(
220
+ schema: GraphQLSchema,
221
+ /,
222
+ *,
223
+ annotations: AnnotationBuilder,
224
+ schema_spellings: SchemaSpellings,
225
+ injector_names: AbstractSet[str],
226
+ ) -> list[ast.stmt]:
227
+ """Input types are not a DAG: the schema has reference cycles, all passing through the nullable members of a `@oneOf` input, which the spec permits since only *non-null* cycles are unsatisfiable.
228
+
229
+ One module with postponed annotations lets them name each other freely.
230
+ """
231
+ input_types = _reached(
232
+ schema, GraphQLInputObjectType, schema_spellings=schema_spellings
233
+ )
234
+ bare_names = frozenset(
235
+ schema_spellings.type_spellings[input_type.name] for input_type in input_types
236
+ )
237
+ body: list[ast.stmt] = []
238
+
239
+ for input_type in input_types:
240
+ fields = {
241
+ field_name: field
242
+ for field_name, field in input_type.fields.items()
243
+ # An injected field is set by the client, so a caller must not be able to.
244
+ if field_name not in injector_names
245
+ }
246
+
247
+ if input_type.is_one_of:
248
+ body.extend(
249
+ _emit_one_of(
250
+ input_type,
251
+ fields=fields,
252
+ annotations=annotations,
253
+ spelling=schema_spellings.type_spellings[input_type.name],
254
+ bare_names=bare_names,
255
+ ),
256
+ )
257
+ continue
258
+
259
+ body.extend(
260
+ emit_closed(
261
+ schema_spellings.type_spellings[input_type.name],
262
+ [
263
+ Key(
264
+ name=field_name,
265
+ annotation=annotations.build(
266
+ field.type,
267
+ position="input",
268
+ bare_inputs=True,
269
+ object_annotation=None,
270
+ ),
271
+ required=_is_required(field),
272
+ description=field.description or "",
273
+ )
274
+ for field_name, field in fields.items()
275
+ ],
276
+ description=input_type.description or "",
277
+ bare_names=bare_names,
278
+ ),
279
+ )
280
+
281
+ return [*_defer_functional_references(body, input_names=bare_names), *body]
282
+
283
+
284
+ def _defer_functional_references(
285
+ body: list[ast.stmt], /, *, input_names: frozenset[SettledSpelling]
286
+ ) -> list[ast.stmt]:
287
+ """Postponed evaluation only covers annotations: the values of a functional TypedDict are an ordinary dict, evaluated when the statement runs, so an input it names that is defined later, or the type itself, would not exist yet.
288
+
289
+ Each such name is referenced through a `type` alias instead, whose value is evaluated lazily.
290
+ """
291
+ aliases: dict[SettledSpelling, PendingSpelling] = {}
292
+
293
+ for statement in body:
294
+ if not (
295
+ isinstance(statement, ast.Assign) and isinstance(statement.value, ast.Call)
296
+ ):
297
+ continue
298
+
299
+ for node in ast.walk(statement.value):
300
+ if isinstance(node, ast.Name) and node.id in input_names:
301
+ alias = aliases.setdefault(
302
+ SettledSpelling(node.id), PendingSpelling(f"_{node.id}_Ref")
303
+ )
304
+ node.id = ast_str(alias)
305
+
306
+ return [
307
+ type_alias(alias, name(input_name)) for input_name, alias in aliases.items()
308
+ ]
309
+
310
+
311
+ def emit_schema_package(
312
+ schema: GraphQLSchema, /, *, schema_spellings: SchemaSpellings
313
+ ) -> ast.Module:
314
+ """Re-export every enum and input type of the package, which share GraphQL's single type name space, by a star import of each module.
315
+
316
+ It leaves out names starting with an underscore: every helper alias and nested type, and any GraphQL type named that way, which the typing spec keeps private anyway and which its public module still holds.
317
+
318
+ """
319
+ exports = {
320
+ schema_spellings.enum_module: _reached(
321
+ schema, GraphQLEnumType, schema_spellings=schema_spellings
322
+ ),
323
+ schema_spellings.input_module: _reached(
324
+ schema, GraphQLInputObjectType, schema_spellings=schema_spellings
325
+ ),
326
+ }
327
+ return module(
328
+ [
329
+ ast.ImportFrom(module=module_name, names=[ast.alias(name="*")], level=1)
330
+ for module_name, types in exports.items()
331
+ # An empty `from ... import` is a syntax error.
332
+ if types
333
+ ],
334
+ )