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