graphql-codegen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphql_codegen/__init__.py +7 -0
- graphql_codegen/__main__.py +5 -0
- graphql_codegen/_cli/__init__.py +41 -0
- graphql_codegen/_cli/_graphql_config.py +291 -0
- graphql_codegen/_cli/_introspection.py +87 -0
- graphql_codegen/_cli/_introspection_graphql.py +53 -0
- graphql_codegen/_cli/_parsing.py +67 -0
- graphql_codegen/_cli/_schema_pointer.py +108 -0
- graphql_codegen/_cli/_source.py +68 -0
- graphql_codegen/_cli/_write.py +22 -0
- graphql_codegen/_generator/__init__.py +0 -0
- graphql_codegen/_generator/_annotation.py +149 -0
- graphql_codegen/_generator/_ast_nodes.py +235 -0
- graphql_codegen/_generator/_data_type.py +532 -0
- graphql_codegen/_generator/_document.py +108 -0
- graphql_codegen/_generator/_document_module.py +54 -0
- graphql_codegen/_generator/_imports.py +166 -0
- graphql_codegen/_generator/_injector.py +267 -0
- graphql_codegen/_generator/_merge.py +149 -0
- graphql_codegen/_generator/_naming.py +80 -0
- graphql_codegen/_generator/_operation.py +223 -0
- graphql_codegen/_generator/_scalar.py +76 -0
- graphql_codegen/_generator/_schema.py +46 -0
- graphql_codegen/_generator/_schema_type.py +334 -0
- graphql_codegen/_generator/_selection.py +246 -0
- graphql_codegen/_generator/_structs.py +87 -0
- graphql_codegen/_generator/_typed_dict.py +154 -0
- graphql_codegen/_generator/dotted_name.py +48 -0
- graphql_codegen/_generator/package.py +695 -0
- graphql_codegen/_generator/spelling.py +168 -0
- graphql_codegen/_metadata.py +11 -0
- graphql_codegen/_note.py +12 -0
- graphql_codegen/config.py +42 -0
- graphql_codegen/document_sibling_module.py +58 -0
- graphql_codegen/generate.py +19 -0
- graphql_codegen/package_location.py +41 -0
- graphql_codegen/py.typed +0 -0
- graphql_codegen/runtime/__init__.py +20 -0
- graphql_codegen/runtime/_compat.py +27 -0
- graphql_codegen/runtime/_literal.py +18 -0
- graphql_codegen/runtime/_merge.py +153 -0
- graphql_codegen/runtime/_prepare.py +250 -0
- graphql_codegen/runtime/_reflection.py +376 -0
- graphql_codegen/runtime/_sigil.py +11 -0
- graphql_codegen/runtime/_transport.py +21 -0
- graphql_codegen/runtime/client.py +384 -0
- graphql_codegen/runtime/error.py +135 -0
- graphql_codegen/runtime/injection.py +107 -0
- graphql_codegen/runtime/operation.py +135 -0
- graphql_codegen/scalar.py +59 -0
- graphql_codegen-0.1.0.dist-info/METADATA +871 -0
- graphql_codegen-0.1.0.dist-info/RECORD +55 -0
- graphql_codegen-0.1.0.dist-info/WHEEL +4 -0
- graphql_codegen-0.1.0.dist-info/entry_points.txt +3 -0
- graphql_codegen-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|
+
)
|