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,246 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Final, cast, final
|
|
4
|
+
|
|
5
|
+
from graphql import (
|
|
6
|
+
BooleanValueNode,
|
|
7
|
+
DirectiveNode,
|
|
8
|
+
FieldNode,
|
|
9
|
+
FragmentDefinitionNode,
|
|
10
|
+
FragmentSpreadNode,
|
|
11
|
+
GraphQLCompositeType,
|
|
12
|
+
GraphQLIncludeDirective,
|
|
13
|
+
GraphQLOutputType,
|
|
14
|
+
GraphQLSchema,
|
|
15
|
+
GraphQLSkipDirective,
|
|
16
|
+
InlineFragmentNode,
|
|
17
|
+
OperationDefinitionNode,
|
|
18
|
+
SelectionSetNode,
|
|
19
|
+
TypeInfo,
|
|
20
|
+
is_type_sub_type_of,
|
|
21
|
+
type_from_ast,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def inclusion(directives: Sequence[DirectiveNode], /) -> bool | None:
|
|
26
|
+
"""Whether `@skip` and `@include` include a selection, or ``None`` when a variable decides.
|
|
27
|
+
|
|
28
|
+
graphql-core's :func:`~graphql.execution.collect_fields.should_include_node` needs the variables' values, which only exist at runtime.
|
|
29
|
+
A directive without its `if` argument, which validation reports, decides nothing either.
|
|
30
|
+
"""
|
|
31
|
+
included: bool | None = True
|
|
32
|
+
|
|
33
|
+
for directive in directives:
|
|
34
|
+
name = directive.name.value
|
|
35
|
+
|
|
36
|
+
if name not in {GraphQLSkipDirective.name, GraphQLIncludeDirective.name}:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
value = next(
|
|
40
|
+
(
|
|
41
|
+
argument.value
|
|
42
|
+
for argument in directive.arguments or ()
|
|
43
|
+
if argument.name.value == "if"
|
|
44
|
+
),
|
|
45
|
+
None,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if not isinstance(value, BooleanValueNode):
|
|
49
|
+
included = None
|
|
50
|
+
elif value.value == (name == GraphQLSkipDirective.name):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
return included
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@final
|
|
57
|
+
@dataclass(frozen=True, kw_only=True)
|
|
58
|
+
class SelectedField:
|
|
59
|
+
name: str
|
|
60
|
+
alias: str | None
|
|
61
|
+
type: GraphQLOutputType
|
|
62
|
+
parent_type: GraphQLCompositeType
|
|
63
|
+
directive_names: frozenset[str]
|
|
64
|
+
description: str
|
|
65
|
+
selection_set: "SelectionSet | None"
|
|
66
|
+
skippable: bool
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def response_name(self) -> str:
|
|
70
|
+
return self.alias or self.name
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@final
|
|
74
|
+
@dataclass(frozen=True, kw_only=True)
|
|
75
|
+
class TypeCondition:
|
|
76
|
+
type: GraphQLCompositeType
|
|
77
|
+
selection_set: "SelectionSet"
|
|
78
|
+
skippable: bool
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@final
|
|
82
|
+
@dataclass(frozen=True, kw_only=True)
|
|
83
|
+
class SelectionSet:
|
|
84
|
+
"""A selection set, resolved, with the selections under a type condition kept apart.
|
|
85
|
+
|
|
86
|
+
``fields`` holds what is selected on every value, ``conditions`` what is selected only on some, and ``fragment_names`` the fragments spread here.
|
|
87
|
+
All three are in document order, which keeps emission deterministic.
|
|
88
|
+
Spreads are recorded rather than expanded, so that emission can reuse the fragment's own type as a base class.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
type: GraphQLCompositeType
|
|
92
|
+
fields: tuple[SelectedField, ...]
|
|
93
|
+
conditions: tuple[TypeCondition, ...]
|
|
94
|
+
fragment_names: tuple[str, ...]
|
|
95
|
+
description: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@final
|
|
99
|
+
class SelectionResolver:
|
|
100
|
+
"""Resolve the type of every field an operation or a fragment selects.
|
|
101
|
+
|
|
102
|
+
This is the part a generator inferring types itself easily gets wrong, on `interface` chains notably, and graphql-core supplies all of it, so nothing here infers a type:
|
|
103
|
+
|
|
104
|
+
- :class:`~graphql.TypeInfo` tracks, during a recursive descent calling its :meth:`~graphql.TypeInfo.enter` and :meth:`~graphql.TypeInfo.leave`, the definition of each field, meta fields included, and the type each selection set selects on;
|
|
105
|
+
- :func:`~graphql.is_type_sub_type_of` tells whether a fragment's type condition holds for every value of a type.
|
|
106
|
+
|
|
107
|
+
What is left is what graphql-core leaves to its caller: following fragment spreads, and `@skip` and `@include`, whose variables only have values at runtime.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
fragment_definitions: Mapping[str, FragmentDefinitionNode],
|
|
113
|
+
/,
|
|
114
|
+
*,
|
|
115
|
+
schema: GraphQLSchema,
|
|
116
|
+
) -> None:
|
|
117
|
+
self._fragment_definitions: Final = fragment_definitions
|
|
118
|
+
self._schema: Final = schema
|
|
119
|
+
|
|
120
|
+
def resolve(
|
|
121
|
+
self, definition: OperationDefinitionNode | FragmentDefinitionNode, /
|
|
122
|
+
) -> SelectionSet:
|
|
123
|
+
type_info = TypeInfo(self._schema)
|
|
124
|
+
# Entering the definition pushes the operation's root type, or the fragment's type condition.
|
|
125
|
+
type_info.enter(definition)
|
|
126
|
+
selection_set = self._resolve_selection_set(
|
|
127
|
+
definition.selection_set, type_info=type_info
|
|
128
|
+
)
|
|
129
|
+
type_info.leave(definition)
|
|
130
|
+
return selection_set
|
|
131
|
+
|
|
132
|
+
def _resolve_selection_set(
|
|
133
|
+
self, node: SelectionSetNode, /, *, type_info: TypeInfo
|
|
134
|
+
) -> SelectionSet:
|
|
135
|
+
# Entering it makes the type it selects on the parent type of what it selects.
|
|
136
|
+
type_info.enter(node)
|
|
137
|
+
parent_type = type_info.get_parent_type()
|
|
138
|
+
assert parent_type is not None, (
|
|
139
|
+
"Validation guarantees that a selection set selects on a composite type."
|
|
140
|
+
)
|
|
141
|
+
fields: list[SelectedField] = []
|
|
142
|
+
conditions: list[TypeCondition] = []
|
|
143
|
+
fragment_names: list[str] = []
|
|
144
|
+
|
|
145
|
+
for selection in node.selections:
|
|
146
|
+
# `None` rather than empty when the selection has none.
|
|
147
|
+
included = inclusion(selection.directives or ())
|
|
148
|
+
|
|
149
|
+
# Never in the response, so not in the type either.
|
|
150
|
+
if included is False:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
skippable = included is None
|
|
154
|
+
|
|
155
|
+
match selection:
|
|
156
|
+
case FieldNode():
|
|
157
|
+
fields.append(
|
|
158
|
+
self._resolve_field(
|
|
159
|
+
selection, type_info=type_info, skippable=skippable
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
case InlineFragmentNode():
|
|
163
|
+
type_info.enter(selection)
|
|
164
|
+
# Its type condition, or, without one, the enclosing type.
|
|
165
|
+
selection_set = self._resolve_selection_set(
|
|
166
|
+
selection.selection_set, type_info=type_info
|
|
167
|
+
)
|
|
168
|
+
type_info.leave(selection)
|
|
169
|
+
conditions.append(
|
|
170
|
+
TypeCondition(
|
|
171
|
+
type=selection_set.type,
|
|
172
|
+
selection_set=selection_set,
|
|
173
|
+
skippable=skippable,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
case FragmentSpreadNode():
|
|
177
|
+
# `TypeInfo` does not follow a spread: the fragment is resolved on its own, by name.
|
|
178
|
+
fragment_name = selection.name.value
|
|
179
|
+
fragment_type = cast(
|
|
180
|
+
"GraphQLCompositeType",
|
|
181
|
+
type_from_ast(
|
|
182
|
+
self._schema,
|
|
183
|
+
self._fragment_definitions[fragment_name].type_condition,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not skippable and is_type_sub_type_of(
|
|
188
|
+
self._schema, parent_type, fragment_type
|
|
189
|
+
):
|
|
190
|
+
# Its type condition holds for every value selected here, so the fragment's type stands in, as a base class.
|
|
191
|
+
fragment_names.append(fragment_name)
|
|
192
|
+
else:
|
|
193
|
+
# One on a narrower type is a branch, as an inline fragment is, and so is a skippable one, since standing in would declare its keys present.
|
|
194
|
+
conditions.append(
|
|
195
|
+
TypeCondition(
|
|
196
|
+
type=fragment_type,
|
|
197
|
+
selection_set=SelectionSet(
|
|
198
|
+
type=fragment_type,
|
|
199
|
+
fragment_names=(fragment_name,),
|
|
200
|
+
fields=(),
|
|
201
|
+
conditions=(),
|
|
202
|
+
description="",
|
|
203
|
+
),
|
|
204
|
+
skippable=skippable,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
case _: # pragma: no cover
|
|
208
|
+
raise TypeError(f"Unexpected selection: {selection}")
|
|
209
|
+
|
|
210
|
+
type_info.leave(node)
|
|
211
|
+
return SelectionSet(
|
|
212
|
+
type=parent_type,
|
|
213
|
+
fields=tuple(fields),
|
|
214
|
+
conditions=tuple(conditions),
|
|
215
|
+
fragment_names=tuple(fragment_names),
|
|
216
|
+
description=parent_type.description or "",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _resolve_field(
|
|
220
|
+
self, node: FieldNode, /, *, type_info: TypeInfo, skippable: bool
|
|
221
|
+
) -> SelectedField:
|
|
222
|
+
type_info.enter(node)
|
|
223
|
+
parent_type = type_info.get_parent_type()
|
|
224
|
+
# `__typename` included: `TypeInfo` knows the meta fields.
|
|
225
|
+
field_definition = type_info.get_field_def()
|
|
226
|
+
assert parent_type is not None and field_definition is not None, (
|
|
227
|
+
"Validation guarantees that a selected field exists."
|
|
228
|
+
)
|
|
229
|
+
selection_set = (
|
|
230
|
+
None
|
|
231
|
+
if node.selection_set is None
|
|
232
|
+
else self._resolve_selection_set(node.selection_set, type_info=type_info)
|
|
233
|
+
)
|
|
234
|
+
type_info.leave(node)
|
|
235
|
+
return SelectedField(
|
|
236
|
+
name=node.name.value,
|
|
237
|
+
alias=node.alias.value if node.alias else None,
|
|
238
|
+
type=field_definition.type,
|
|
239
|
+
parent_type=parent_type,
|
|
240
|
+
directive_names=frozenset(
|
|
241
|
+
directive.name.value for directive in node.directives or ()
|
|
242
|
+
),
|
|
243
|
+
description=field_definition.description or "",
|
|
244
|
+
selection_set=selection_set,
|
|
245
|
+
skippable=skippable,
|
|
246
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import final
|
|
4
|
+
|
|
5
|
+
from graphql import (
|
|
6
|
+
GraphQLError,
|
|
7
|
+
GraphQLInputObjectType,
|
|
8
|
+
GraphQLInterfaceType,
|
|
9
|
+
GraphQLNamedType,
|
|
10
|
+
GraphQLScalarType,
|
|
11
|
+
GraphQLSchema,
|
|
12
|
+
get_named_type,
|
|
13
|
+
is_specified_scalar_type,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@final
|
|
18
|
+
@dataclass(frozen=True, kw_only=True)
|
|
19
|
+
class StructTypes:
|
|
20
|
+
payload_field_name: str
|
|
21
|
+
|
|
22
|
+
input_type_names: Mapping[str, str]
|
|
23
|
+
"""The name of the input type each struct type's payload is shaped like, by the struct type's name."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_struct_types(
|
|
27
|
+
schema: GraphQLSchema, struct_interface_name: str, /
|
|
28
|
+
) -> StructTypes:
|
|
29
|
+
"""The interface has a single field, the payload, of a custom scalar type, since the payload is opaque on the wire.
|
|
30
|
+
|
|
31
|
+
Every object type implementing it is named after an input type followed by the interface's name: `BookFilterStruct` has the shape of `input BookFilter`, which lets one definition be both sent and received.
|
|
32
|
+
"""
|
|
33
|
+
interface = schema.type_map.get(struct_interface_name)
|
|
34
|
+
|
|
35
|
+
if not isinstance(interface, GraphQLInterfaceType):
|
|
36
|
+
# A config value naming nothing in the schema, not a mistyped one.
|
|
37
|
+
raise ValueError(f"Found no interface named `{struct_interface_name}`.") # noqa: TRY004
|
|
38
|
+
|
|
39
|
+
match list(interface.fields.items()):
|
|
40
|
+
case [(payload_field_name, payload_field)] if isinstance(
|
|
41
|
+
payload_type := get_named_type(payload_field.type), GraphQLScalarType
|
|
42
|
+
) and not is_specified_scalar_type(payload_type):
|
|
43
|
+
pass
|
|
44
|
+
case _:
|
|
45
|
+
raise GraphQLError(
|
|
46
|
+
f"Expected `{struct_interface_name}` to have a single field, the payload, of a custom scalar type.",
|
|
47
|
+
nodes=interface.ast_node,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
input_type_names: dict[str, str] = {}
|
|
51
|
+
|
|
52
|
+
for object_type in schema.get_possible_types(interface):
|
|
53
|
+
input_name = object_type.name.removesuffix(struct_interface_name)
|
|
54
|
+
|
|
55
|
+
if input_name == object_type.name or not isinstance(
|
|
56
|
+
schema.type_map.get(input_name), GraphQLInputObjectType
|
|
57
|
+
):
|
|
58
|
+
raise GraphQLError(
|
|
59
|
+
f"Expected `{object_type.name}`, which implements `{struct_interface_name}`, to be named after an input type followed by `{struct_interface_name}`.",
|
|
60
|
+
nodes=object_type.ast_node,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
input_type_names[object_type.name] = input_name
|
|
64
|
+
|
|
65
|
+
return StructTypes(
|
|
66
|
+
payload_field_name=payload_field_name, input_type_names=input_type_names
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_struct_input_name(
|
|
71
|
+
parent_type: GraphQLNamedType,
|
|
72
|
+
field_name: str,
|
|
73
|
+
/,
|
|
74
|
+
*,
|
|
75
|
+
struct_types: StructTypes | None,
|
|
76
|
+
) -> str | None:
|
|
77
|
+
"""The wire type of a struct's payload is an opaque scalar, so without this it would be :class:`object`.
|
|
78
|
+
|
|
79
|
+
The payload stays under its `value` key rather than replacing the struct, although a struct only ever has that one key.
|
|
80
|
+
Moving it up would make every response holding a struct pay for a conversion, even one whose payload otherwise needs none, and the data's type would stop mirroring the document, which selects `value`, all for little convenience.
|
|
81
|
+
|
|
82
|
+
Once the [Struct RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/Struct.md) lands, a struct field is selected without a selection set, so the wrapper disappears from the document itself.
|
|
83
|
+
"""
|
|
84
|
+
if struct_types is None or field_name != struct_types.payload_field_name:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
return struct_types.input_type_names.get(parent_type.name)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import final
|
|
5
|
+
|
|
6
|
+
from graphql_codegen._generator._ast_nodes import (
|
|
7
|
+
class_definition,
|
|
8
|
+
constant,
|
|
9
|
+
documented,
|
|
10
|
+
name,
|
|
11
|
+
qualified,
|
|
12
|
+
subscript,
|
|
13
|
+
)
|
|
14
|
+
from graphql_codegen._generator._naming import COMPAT, TYPING
|
|
15
|
+
from graphql_codegen._generator.spelling import Spelling, ast_str, is_bindable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
@dataclass(frozen=True, kw_only=True)
|
|
20
|
+
class Key:
|
|
21
|
+
name: str
|
|
22
|
+
annotation: ast.expr
|
|
23
|
+
required: bool
|
|
24
|
+
description: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _functional(
|
|
28
|
+
type_spelling: Spelling, keys: Sequence[Key], /, *, closed: bool
|
|
29
|
+
) -> ast.Assign:
|
|
30
|
+
"""The string passed as the first argument must match the variable it is assigned to, or `ty` reports `mismatched-type-name`."""
|
|
31
|
+
return ast.Assign(
|
|
32
|
+
targets=[ast.Name(id=ast_str(type_spelling), ctx=ast.Store())],
|
|
33
|
+
value=ast.Call(
|
|
34
|
+
func=qualified(COMPAT, "TypedDict"),
|
|
35
|
+
args=[
|
|
36
|
+
# Spelled like the target, placeholder included, so both get the same name.
|
|
37
|
+
constant(ast_str(type_spelling)),
|
|
38
|
+
ast.Dict(
|
|
39
|
+
keys=[constant(key.name) for key in keys],
|
|
40
|
+
values=[_annotation(key, closed=closed) for key in keys],
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
keywords=[ast.keyword(arg="closed", value=ast.Constant(value=True))]
|
|
44
|
+
if closed
|
|
45
|
+
else [],
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _is_class_attribute_safe(key: str, /, *, bare_names: frozenset[str]) -> bool:
|
|
51
|
+
"""A name Python cannot bind cannot be, a name starting with `__` would be mangled, and a public name the module references unqualified would be shadowed by the key in the class scope.
|
|
52
|
+
|
|
53
|
+
No name the generator adds can clash with a key, since :func:`~graphql_codegen._spelling.settle_module` settles none like any name the module holds.
|
|
54
|
+
"""
|
|
55
|
+
return is_bindable(key) and not key.startswith("__") and key not in bare_names
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _annotation(key: Key, /, *, closed: bool) -> ast.expr:
|
|
59
|
+
"""The key's annotation, saying whether it must be present: explicitly in a closed TypedDict, where reading what a caller must send matters, and only when it may be absent in an open one."""
|
|
60
|
+
if closed:
|
|
61
|
+
return subscript(
|
|
62
|
+
qualified(TYPING, "Required" if key.required else "NotRequired"),
|
|
63
|
+
key.annotation,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
key.annotation
|
|
68
|
+
if key.required
|
|
69
|
+
else subscript(qualified(TYPING, "NotRequired"), key.annotation)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _declaration(key: Key, /, *, closed: bool) -> list[ast.stmt]:
|
|
74
|
+
return documented(
|
|
75
|
+
ast.AnnAssign(
|
|
76
|
+
target=ast.Name(id=key.name, ctx=ast.Store()),
|
|
77
|
+
annotation=_annotation(key, closed=closed),
|
|
78
|
+
simple=1,
|
|
79
|
+
),
|
|
80
|
+
text=key.description,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def emit_closed(
|
|
85
|
+
type_spelling: Spelling,
|
|
86
|
+
keys: Sequence[Key],
|
|
87
|
+
/,
|
|
88
|
+
*,
|
|
89
|
+
description: str,
|
|
90
|
+
bare_names: frozenset[str],
|
|
91
|
+
) -> list[ast.stmt]:
|
|
92
|
+
"""Emit a closed TypedDict, as an input or variables type is, in class syntax unless a key cannot be declared in a class body, and then in functional syntax altogether, since a closed TypedDict cannot be extended with keys by a subclass."""
|
|
93
|
+
if all(_is_class_attribute_safe(key.name, bare_names=bare_names) for key in keys):
|
|
94
|
+
return [
|
|
95
|
+
class_definition(
|
|
96
|
+
type_spelling,
|
|
97
|
+
bases=[qualified(COMPAT, "TypedDict")],
|
|
98
|
+
keywords={"closed": ast.Constant(value=True)},
|
|
99
|
+
body=[
|
|
100
|
+
statement
|
|
101
|
+
for key in keys
|
|
102
|
+
for statement in _declaration(key, closed=True)
|
|
103
|
+
],
|
|
104
|
+
docstring_text=description,
|
|
105
|
+
),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
return documented(_functional(type_spelling, keys, closed=True), text=description)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def emit_data(
|
|
112
|
+
type_spelling: Spelling,
|
|
113
|
+
keys: Sequence[Key],
|
|
114
|
+
/,
|
|
115
|
+
*,
|
|
116
|
+
bases: Sequence[ast.expr],
|
|
117
|
+
closed: bool,
|
|
118
|
+
functional_base_spelling: Spelling,
|
|
119
|
+
description: str,
|
|
120
|
+
) -> list[ast.stmt]:
|
|
121
|
+
"""Emit a data type in class syntax, inheriting the keys a class body cannot declare from a functional base, so that it can still inherit fragments too.
|
|
122
|
+
|
|
123
|
+
The functional base is open, since the class extends it.
|
|
124
|
+
"""
|
|
125
|
+
# A data module references no public name unqualified.
|
|
126
|
+
safe = [
|
|
127
|
+
key
|
|
128
|
+
for key in keys
|
|
129
|
+
if _is_class_attribute_safe(key.name, bare_names=frozenset())
|
|
130
|
+
]
|
|
131
|
+
unsafe = [key for key in keys if key not in safe]
|
|
132
|
+
statements: list[ast.stmt] = []
|
|
133
|
+
all_bases = list(bases)
|
|
134
|
+
|
|
135
|
+
if unsafe:
|
|
136
|
+
statements.append(
|
|
137
|
+
_functional(functional_base_spelling, unsafe, closed=False),
|
|
138
|
+
)
|
|
139
|
+
all_bases.insert(0, name(functional_base_spelling))
|
|
140
|
+
|
|
141
|
+
statements.append(
|
|
142
|
+
class_definition(
|
|
143
|
+
type_spelling,
|
|
144
|
+
bases=all_bases or [qualified(COMPAT, "TypedDict")],
|
|
145
|
+
body=[
|
|
146
|
+
statement
|
|
147
|
+
for key in safe
|
|
148
|
+
for statement in _declaration(key, closed=False)
|
|
149
|
+
],
|
|
150
|
+
docstring_text=description,
|
|
151
|
+
keywords={"closed": ast.Constant(value=True)} if closed else {},
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
return statements
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import final
|
|
4
|
+
|
|
5
|
+
from graphql_codegen._generator.spelling import is_bindable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@final
|
|
9
|
+
@dataclass(frozen=True, kw_only=True)
|
|
10
|
+
class Builtin:
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@final
|
|
15
|
+
@dataclass(frozen=True, kw_only=True)
|
|
16
|
+
class Import:
|
|
17
|
+
level: int
|
|
18
|
+
"""Like :attr:`ast.ImportFrom.level`: 0 for an absolute path, and 2 or more for one relative to the directory the package is written to."""
|
|
19
|
+
|
|
20
|
+
module: str | None
|
|
21
|
+
name: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
type Reference = Builtin | Import
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_dotted_name(dotted_name: str, /) -> Reference:
|
|
28
|
+
"""A bare name, such as :class:`int`, is a builtin.
|
|
29
|
+
|
|
30
|
+
A dotted name is absolute, or starts with `..` to be relative to the directory the package is written to: `..scalars.decode` is `scalars.decode` next to it.
|
|
31
|
+
A dotted name starting with one dot would name a module of the generated package, which is not the project's to write in.
|
|
32
|
+
"""
|
|
33
|
+
level = len(dotted_name) - len(dotted_name.lstrip("."))
|
|
34
|
+
*modules, last = dotted_name[level:].split(".")
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
level == 1
|
|
38
|
+
or not all(is_bindable(part) for part in (*modules, last))
|
|
39
|
+
or (level == 0 and not modules and not hasattr(builtins, last))
|
|
40
|
+
):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Expected an absolute dotted name, such as `datetime.datetime`, a dotted name relative to the package's directory, such as `..scalars.decode`, or a builtin, such as `int`, but got `{dotted_name}`."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if level == 0 and not modules:
|
|
46
|
+
return Builtin(name=last)
|
|
47
|
+
|
|
48
|
+
return Import(level=level, module=".".join(modules) or None, name=last)
|