@graphql-codegen/python 0.0.0-stage → 0.0.2-alpha-20261029093428-bb94591c48dc485d04089639f4ca0f124e339666

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.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @graphql-codegen/python
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A **schema-driven** [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) plugin that
4
+ emits **Python base types** from a `GraphQLSchema`:
5
+
6
+ - object & interface types → [pydantic](https://docs.pydantic.dev) `BaseModel` classes (snake_case
7
+ attributes; keys that differ from the GraphQL name are pinned with `Field(alias=...)` +
8
+ `model_config = ConfigDict(populate_by_name=True)`)
9
+ - input types → `TypedDict`
10
+ - enums → `class X(str, Enum)`
11
+ - scalars → native Python (`ID`/`String` → `str`, `Int` → `int`, `Float` → `float`, `Boolean` →
12
+ `bool`, custom → `Any`)
13
+
14
+ It is the base-types counterpart to `@graphql-codegen/python-operations`, analogous to how
15
+ `typescript` underpins `typescript-operations`.
16
+
17
+ ## Config
18
+
19
+ | Option | Type | Default | Description |
20
+ | ------------------ | ----------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `baseModel` | `string` | `'BaseModel'` | Base class the generated object/interface types extend. |
22
+ | `runtimeImport` | `string` | `'pydantic'` | Module the base model is imported from (`from {runtimeImport} import {baseModel}`, with `ConfigDict`/`Field` appended when aliases are used). |
23
+ | `namingConvention` | `'keep' \| 'snakeCase'` | `'snakeCase'` | How field names become attributes (`snakeCase` converts and pins renamed keys with `Field(alias=...)`). |
package/cjs/config.js ADDED
File without changes
package/cjs/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.plugin = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const plugin_helpers_1 = require("@graphql-codegen/plugin-helpers");
6
+ const visitor_js_1 = require("./visitor.js");
7
+ tslib_1.__exportStar(require("./config.js"), exports);
8
+ tslib_1.__exportStar(require("./visitor.js"), exports);
9
+ const plugin = (schema, _documents, config) => {
10
+ const astNode = (0, plugin_helpers_1.getCachedDocumentNodeFromSchema)(schema);
11
+ const visitor = new visitor_js_1.PythonVisitor(schema, config);
12
+ const visitorResult = (0, plugin_helpers_1.oldVisit)(astNode, { leave: visitor });
13
+ const content = visitorResult.definitions
14
+ .filter((def) => typeof def === 'string' && def.length > 0)
15
+ .join('\n\n\n');
16
+ return {
17
+ prepend: visitor.getImports(content),
18
+ content,
19
+ };
20
+ };
21
+ exports.plugin = plugin;
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
package/cjs/visitor.js ADDED
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PythonVisitor = exports.PYTHON_SCALARS = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const auto_bind_1 = tslib_1.__importDefault(require("auto-bind"));
6
+ const python_common_1 = require("@graphql-codegen/python-common");
7
+ Object.defineProperty(exports, "PYTHON_SCALARS", { enumerable: true, get: function () { return python_common_1.PYTHON_SCALARS; } });
8
+ const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
9
+ const INDENT = ' ';
10
+ class PythonVisitor extends visitor_plugin_common_1.BaseTypesVisitor {
11
+ constructor(schema, pluginConfig) {
12
+ // `namingConvention` here is our own field-naming option (`'keep'` |
13
+ // `'snakeCase'`) and must NOT leak into the base visitor's `namingConvention`
14
+ // (which controls TYPE-name conversion via `convertFactory`). Strip it before
15
+ // calling `super` so type names keep their default (PascalCase) conversion.
16
+ super(schema,
17
+ // Route custom scalars (no explicit mapping) to `Any` instead of `unknown`.
18
+ {
19
+ ...pluginConfig,
20
+ namingConvention: undefined,
21
+ defaultScalarType: (0, visitor_plugin_common_1.getConfigValue)(pluginConfig.defaultScalarType, 'Any'),
22
+ }, {
23
+ baseModel: (0, visitor_plugin_common_1.getConfigValue)(pluginConfig.baseModel, 'BaseModel'),
24
+ runtimeImport: (0, visitor_plugin_common_1.getConfigValue)(pluginConfig.runtimeImport, 'pydantic'),
25
+ pyNamingConvention: (0, visitor_plugin_common_1.getConfigValue)(pluginConfig.namingConvention, 'snakeCase'),
26
+ }, python_common_1.PYTHON_SCALARS);
27
+ this._usesEnum = false;
28
+ this._usesTypedDict = false;
29
+ this._usesBaseModel = false;
30
+ this._usesUnion = false;
31
+ (0, auto_bind_1.default)(this);
32
+ }
33
+ /** Python has no `export` keyword; disable the C-family export prefix. */
34
+ getExportPrefix() {
35
+ return '';
36
+ }
37
+ _getScalar(name, type) {
38
+ const mapped = this.scalars[name];
39
+ if (mapped) {
40
+ return mapped[type];
41
+ }
42
+ return 'Any';
43
+ }
44
+ // --- Nullability / list composition (emit Python instead of TS) ---
45
+ NamedType(node, key, parent, path, ancestors) {
46
+ return `Optional[${super.NamedType(node, key, parent, path, ancestors)}]`;
47
+ }
48
+ ListType(node, key, parent, path, ancestors) {
49
+ return `Optional[${super.ListType(node, key, parent, path, ancestors)}]`;
50
+ }
51
+ wrapWithListType(str) {
52
+ return `list[${str}]`;
53
+ }
54
+ NonNullType(node) {
55
+ const baseValue = super.NonNullType(node);
56
+ if (baseValue.startsWith('Optional[') && baseValue.endsWith(']')) {
57
+ return baseValue.slice('Optional['.length, -1);
58
+ }
59
+ return baseValue;
60
+ }
61
+ // --- Field rendering (name: 'annotation') ---
62
+ snakeFieldName(name) {
63
+ return (0, python_common_1.pythonFieldName)(name);
64
+ }
65
+ /**
66
+ * Field key for a `TypedDict` (input type). Input types are what the caller
67
+ * SENDS — the runtime forwards the dict to the server as-is — so they ALWAYS
68
+ * keep the wire (GraphQL) name regardless of `namingConvention`. If the name
69
+ * is a keyword / non-identifier the containing `TypedDict` is later emitted
70
+ * with functional syntax (see `buildTypedDict`).
71
+ */
72
+ dictFieldName(name) {
73
+ return name;
74
+ }
75
+ /**
76
+ * Field key for an object / interface type (a class attribute). Under `'keep'`
77
+ * a keyword / non-identifier name cannot be a class attribute and BaseModel
78
+ * cannot use functional `TypedDict` syntax, so we fall back to appending `_`
79
+ * for just that field.
80
+ */
81
+ objectFieldName(name) {
82
+ if (this.config.pyNamingConvention === 'snakeCase') {
83
+ return this.snakeFieldName(name);
84
+ }
85
+ return (0, python_common_1.isValidClassAttr)(name) ? name : `${name}_`;
86
+ }
87
+ FieldDefinition(node) {
88
+ if (this.config.onlyEnums)
89
+ return '';
90
+ const wire = node.name.value;
91
+ const attr = this.objectFieldName(wire);
92
+ // The rendered annotation (e.g. `Optional[str]`, `list[User]`). Quoted below
93
+ // so forward / recursive references resolve without ordering constraints.
94
+ const annotation = node.type;
95
+ const optional = annotation.startsWith('Optional[');
96
+ // When the Python attribute differs from the GraphQL wire key (snake_case
97
+ // conversion or a keyword escaped with `_`), pin the wire key with a pydantic
98
+ // `Field(alias=...)` so hydration maps it onto the attribute. Optional fields
99
+ // also default to `None` so a missing key is allowed.
100
+ let assignment = '';
101
+ if (attr !== wire) {
102
+ assignment = optional
103
+ ? ` = Field(default=None, alias='${wire}')`
104
+ : ` = Field(alias='${wire}')`;
105
+ }
106
+ else if (optional) {
107
+ assignment = ' = None';
108
+ }
109
+ return `${attr}: '${annotation}'${assignment}`;
110
+ }
111
+ InputValueDefinition(node) {
112
+ if (this.config.onlyEnums)
113
+ return '';
114
+ return `${this.dictFieldName(node.name.value)}: '${node.type}'`;
115
+ }
116
+ // --- Class / block builder ---
117
+ buildClass(name, base, fields) {
118
+ const header = `class ${name}(${base}):`;
119
+ const body = fields.filter(Boolean);
120
+ if (body.length === 0) {
121
+ return `${header}\n${INDENT}pass`;
122
+ }
123
+ return [header, ...body.map(f => INDENT + f)].join('\n');
124
+ }
125
+ /**
126
+ * Build a base-model subclass. Fields are class-level annotations; if any field
127
+ * pins a wire key with `Field(alias=...)`, a
128
+ * `model_config = ConfigDict(populate_by_name=True)` line is prepended so the
129
+ * class can be constructed by attribute name as well as by alias.
130
+ */
131
+ buildBaseModelClass(name, fields) {
132
+ const header = `class ${name}(${this.config.baseModel}):`;
133
+ const body = fields.filter(Boolean);
134
+ if (body.length === 0) {
135
+ return `${header}\n${INDENT}pass`;
136
+ }
137
+ const lines = [header];
138
+ if (body.some(f => f.includes('alias='))) {
139
+ lines.push(`${INDENT}model_config = ConfigDict(populate_by_name=True)`);
140
+ }
141
+ lines.push(...body.map(f => INDENT + f));
142
+ return lines.join('\n');
143
+ }
144
+ /**
145
+ * Split a rendered `key: 'annotation'` field back into its parts. The key is
146
+ * always a GraphQL Name (no `: '`) and the annotation is always single-quoted,
147
+ * so splitting on the first `: '` is unambiguous.
148
+ */
149
+ splitField(rendered) {
150
+ const sep = rendered.indexOf(": '");
151
+ const key = rendered.slice(0, sep);
152
+ const annotation = rendered.slice(sep + 3, -1);
153
+ return { key, annotation };
154
+ }
155
+ /**
156
+ * Build a `TypedDict`. If every key is a valid Python attribute, the readable
157
+ * class syntax is used; otherwise (a key is a keyword / non-identifier under
158
+ * `'keep'`) the functional `TypedDict('Name', { 'key': 'annotation' })` syntax
159
+ * is emitted so the offending keys remain valid.
160
+ */
161
+ buildTypedDict(name, fields) {
162
+ const body = fields.filter(Boolean);
163
+ const parsed = body.map(f => this.splitField(f));
164
+ const needsFunctional = parsed.some(p => !(0, python_common_1.isValidClassAttr)(p.key));
165
+ if (!needsFunctional) {
166
+ return this.buildClass(name, 'TypedDict', body);
167
+ }
168
+ const entries = parsed.map(p => `${INDENT}'${p.key}': '${p.annotation}',`);
169
+ return [`${name} = TypedDict('${name}', {`, ...entries, '})'].join('\n');
170
+ }
171
+ ObjectTypeDefinition(node) {
172
+ if (this.config.onlyOperationTypes || this.config.onlyEnums)
173
+ return '';
174
+ if (node.name.value.startsWith('__'))
175
+ return '';
176
+ this._usesBaseModel = true;
177
+ return this.buildBaseModelClass(this.convertName(node), node.fields);
178
+ }
179
+ InterfaceTypeDefinition(node) {
180
+ if (this.config.onlyOperationTypes || this.config.onlyEnums)
181
+ return '';
182
+ if (node.name.value.startsWith('__'))
183
+ return '';
184
+ this._usesBaseModel = true;
185
+ return this.buildBaseModelClass(this.convertName(node), node.fields);
186
+ }
187
+ InputObjectTypeDefinition(node) {
188
+ if (this.config.onlyEnums)
189
+ return '';
190
+ if (node.name.value.startsWith('__'))
191
+ return '';
192
+ this._usesTypedDict = true;
193
+ // Under `'keep'`, a keyword / non-identifier field key forces functional
194
+ // `TypedDict` syntax; otherwise readable class syntax is used.
195
+ return this.buildTypedDict(this.convertName(node), node.fields);
196
+ }
197
+ EnumTypeDefinition(node) {
198
+ var _a;
199
+ if (node.name.value.startsWith('__'))
200
+ return '';
201
+ this._usesEnum = true;
202
+ const name = this.convertName(node, {
203
+ useTypesPrefix: this.config.enumPrefix,
204
+ useTypesSuffix: this.config.enumSuffix,
205
+ });
206
+ const members = ((_a = node.values) !== null && _a !== void 0 ? _a : []).map(value => `${INDENT}${value.name.value} = '${value.name.value}'`);
207
+ const body = members.length > 0 ? members : [`${INDENT}pass`];
208
+ return [`class ${name}(str, Enum):`, ...body].join('\n');
209
+ }
210
+ /** Scalars are handled inline via the scalar map; no standalone class. */
211
+ ScalarTypeDefinition() {
212
+ return '';
213
+ }
214
+ /**
215
+ * `union Foo = A | B` -> `Foo = Union['A', 'B']`.
216
+ *
217
+ * Must be overridden: `BaseTypesVisitor`'s implementation emits TypeScript
218
+ * (`export type Foo = A | B;`), and it dereferences the node description
219
+ * unguarded, so an undocumented union crashes the generator outright. A schema
220
+ * merely *containing* a union is enough to hit this, even when no operation
221
+ * selects one.
222
+ *
223
+ * Member names come from `parent[key]` — the original, un-visited node — the
224
+ * same way `typescript`'s visitor reads them. By `leave` time `node.types`
225
+ * has already been rendered into annotations (`Optional[Foo]`) and would
226
+ * otherwise be mangled. Members are quoted so a union declared before its
227
+ * members still resolves, matching how every other annotation in this plugin
228
+ * is emitted.
229
+ *
230
+ * Note this only makes a union *declaration* valid Python. Actually selecting
231
+ * union members in an operation needs inline-fragment support, which
232
+ * `python-operations` does not implement yet (it throws).
233
+ */
234
+ UnionTypeDefinition(node, key, parent) {
235
+ var _a;
236
+ if (this.config.onlyOperationTypes || this.config.onlyEnums)
237
+ return '';
238
+ if (node.name.value.startsWith('__'))
239
+ return '';
240
+ const originalNode = parent[key];
241
+ const members = ((_a = originalNode.types) !== null && _a !== void 0 ? _a : []).map(t => `'${this.convertName(t)}'`);
242
+ // A member-less union is legal to declare but has no inhabitant.
243
+ if (members.length === 0) {
244
+ return `${this.convertName(node)} = Any`;
245
+ }
246
+ this._usesUnion = true;
247
+ return `${this.convertName(node)} = Union[${members.join(', ')}]`;
248
+ }
249
+ /**
250
+ * Build the `prepend` import lines, emitting only what the generated
251
+ * `content` actually references.
252
+ */
253
+ getImports(content) {
254
+ const imports = [];
255
+ if (this._usesEnum) {
256
+ imports.push('from enum import Enum');
257
+ }
258
+ const typingParts = [];
259
+ if (content.includes('Optional[')) {
260
+ typingParts.push('Optional');
261
+ }
262
+ if (this._usesTypedDict) {
263
+ typingParts.push('TypedDict');
264
+ }
265
+ if (this._usesUnion) {
266
+ typingParts.push('Union');
267
+ }
268
+ if (/\bAny\b/.test(content)) {
269
+ typingParts.push('Any');
270
+ }
271
+ if (typingParts.length > 0) {
272
+ imports.push(`from typing import ${typingParts.join(', ')}`);
273
+ }
274
+ if (this._usesBaseModel) {
275
+ // `ConfigDict` / `Field` are pydantic names; emit them from the same module
276
+ // as the base model, and only when the generated content uses them.
277
+ const parts = [this.config.baseModel];
278
+ if (content.includes('ConfigDict(')) {
279
+ parts.push('ConfigDict');
280
+ }
281
+ if (content.includes('Field(')) {
282
+ parts.push('Field');
283
+ }
284
+ imports.push(`from ${this.config.runtimeImport} import ${parts.join(', ')}`);
285
+ }
286
+ return imports;
287
+ }
288
+ }
289
+ exports.PythonVisitor = PythonVisitor;
package/esm/config.js ADDED
File without changes
package/esm/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import { getCachedDocumentNodeFromSchema, oldVisit, } from '@graphql-codegen/plugin-helpers';
2
+ import { PythonVisitor } from './visitor.js';
3
+ export * from './config.js';
4
+ export * from './visitor.js';
5
+ export const plugin = (schema, _documents, config) => {
6
+ const astNode = getCachedDocumentNodeFromSchema(schema);
7
+ const visitor = new PythonVisitor(schema, config);
8
+ const visitorResult = oldVisit(astNode, { leave: visitor });
9
+ const content = visitorResult.definitions
10
+ .filter((def) => typeof def === 'string' && def.length > 0)
11
+ .join('\n\n\n');
12
+ return {
13
+ prepend: visitor.getImports(content),
14
+ content,
15
+ };
16
+ };
package/esm/visitor.js ADDED
@@ -0,0 +1,284 @@
1
+ import autoBind from 'auto-bind';
2
+ import { isValidClassAttr, PYTHON_SCALARS, pythonFieldName } from '@graphql-codegen/python-common';
3
+ import { BaseTypesVisitor, getConfigValue, } from '@graphql-codegen/visitor-plugin-common';
4
+ export { PYTHON_SCALARS };
5
+ const INDENT = ' ';
6
+ export class PythonVisitor extends BaseTypesVisitor {
7
+ constructor(schema, pluginConfig) {
8
+ // `namingConvention` here is our own field-naming option (`'keep'` |
9
+ // `'snakeCase'`) and must NOT leak into the base visitor's `namingConvention`
10
+ // (which controls TYPE-name conversion via `convertFactory`). Strip it before
11
+ // calling `super` so type names keep their default (PascalCase) conversion.
12
+ super(schema,
13
+ // Route custom scalars (no explicit mapping) to `Any` instead of `unknown`.
14
+ {
15
+ ...pluginConfig,
16
+ namingConvention: undefined,
17
+ defaultScalarType: getConfigValue(pluginConfig.defaultScalarType, 'Any'),
18
+ }, {
19
+ baseModel: getConfigValue(pluginConfig.baseModel, 'BaseModel'),
20
+ runtimeImport: getConfigValue(pluginConfig.runtimeImport, 'pydantic'),
21
+ pyNamingConvention: getConfigValue(pluginConfig.namingConvention, 'snakeCase'),
22
+ }, PYTHON_SCALARS);
23
+ this._usesEnum = false;
24
+ this._usesTypedDict = false;
25
+ this._usesBaseModel = false;
26
+ this._usesUnion = false;
27
+ autoBind(this);
28
+ }
29
+ /** Python has no `export` keyword; disable the C-family export prefix. */
30
+ getExportPrefix() {
31
+ return '';
32
+ }
33
+ _getScalar(name, type) {
34
+ const mapped = this.scalars[name];
35
+ if (mapped) {
36
+ return mapped[type];
37
+ }
38
+ return 'Any';
39
+ }
40
+ // --- Nullability / list composition (emit Python instead of TS) ---
41
+ NamedType(node, key, parent, path, ancestors) {
42
+ return `Optional[${super.NamedType(node, key, parent, path, ancestors)}]`;
43
+ }
44
+ ListType(node, key, parent, path, ancestors) {
45
+ return `Optional[${super.ListType(node, key, parent, path, ancestors)}]`;
46
+ }
47
+ wrapWithListType(str) {
48
+ return `list[${str}]`;
49
+ }
50
+ NonNullType(node) {
51
+ const baseValue = super.NonNullType(node);
52
+ if (baseValue.startsWith('Optional[') && baseValue.endsWith(']')) {
53
+ return baseValue.slice('Optional['.length, -1);
54
+ }
55
+ return baseValue;
56
+ }
57
+ // --- Field rendering (name: 'annotation') ---
58
+ snakeFieldName(name) {
59
+ return pythonFieldName(name);
60
+ }
61
+ /**
62
+ * Field key for a `TypedDict` (input type). Input types are what the caller
63
+ * SENDS — the runtime forwards the dict to the server as-is — so they ALWAYS
64
+ * keep the wire (GraphQL) name regardless of `namingConvention`. If the name
65
+ * is a keyword / non-identifier the containing `TypedDict` is later emitted
66
+ * with functional syntax (see `buildTypedDict`).
67
+ */
68
+ dictFieldName(name) {
69
+ return name;
70
+ }
71
+ /**
72
+ * Field key for an object / interface type (a class attribute). Under `'keep'`
73
+ * a keyword / non-identifier name cannot be a class attribute and BaseModel
74
+ * cannot use functional `TypedDict` syntax, so we fall back to appending `_`
75
+ * for just that field.
76
+ */
77
+ objectFieldName(name) {
78
+ if (this.config.pyNamingConvention === 'snakeCase') {
79
+ return this.snakeFieldName(name);
80
+ }
81
+ return isValidClassAttr(name) ? name : `${name}_`;
82
+ }
83
+ FieldDefinition(node) {
84
+ if (this.config.onlyEnums)
85
+ return '';
86
+ const wire = node.name.value;
87
+ const attr = this.objectFieldName(wire);
88
+ // The rendered annotation (e.g. `Optional[str]`, `list[User]`). Quoted below
89
+ // so forward / recursive references resolve without ordering constraints.
90
+ const annotation = node.type;
91
+ const optional = annotation.startsWith('Optional[');
92
+ // When the Python attribute differs from the GraphQL wire key (snake_case
93
+ // conversion or a keyword escaped with `_`), pin the wire key with a pydantic
94
+ // `Field(alias=...)` so hydration maps it onto the attribute. Optional fields
95
+ // also default to `None` so a missing key is allowed.
96
+ let assignment = '';
97
+ if (attr !== wire) {
98
+ assignment = optional
99
+ ? ` = Field(default=None, alias='${wire}')`
100
+ : ` = Field(alias='${wire}')`;
101
+ }
102
+ else if (optional) {
103
+ assignment = ' = None';
104
+ }
105
+ return `${attr}: '${annotation}'${assignment}`;
106
+ }
107
+ InputValueDefinition(node) {
108
+ if (this.config.onlyEnums)
109
+ return '';
110
+ return `${this.dictFieldName(node.name.value)}: '${node.type}'`;
111
+ }
112
+ // --- Class / block builder ---
113
+ buildClass(name, base, fields) {
114
+ const header = `class ${name}(${base}):`;
115
+ const body = fields.filter(Boolean);
116
+ if (body.length === 0) {
117
+ return `${header}\n${INDENT}pass`;
118
+ }
119
+ return [header, ...body.map(f => INDENT + f)].join('\n');
120
+ }
121
+ /**
122
+ * Build a base-model subclass. Fields are class-level annotations; if any field
123
+ * pins a wire key with `Field(alias=...)`, a
124
+ * `model_config = ConfigDict(populate_by_name=True)` line is prepended so the
125
+ * class can be constructed by attribute name as well as by alias.
126
+ */
127
+ buildBaseModelClass(name, fields) {
128
+ const header = `class ${name}(${this.config.baseModel}):`;
129
+ const body = fields.filter(Boolean);
130
+ if (body.length === 0) {
131
+ return `${header}\n${INDENT}pass`;
132
+ }
133
+ const lines = [header];
134
+ if (body.some(f => f.includes('alias='))) {
135
+ lines.push(`${INDENT}model_config = ConfigDict(populate_by_name=True)`);
136
+ }
137
+ lines.push(...body.map(f => INDENT + f));
138
+ return lines.join('\n');
139
+ }
140
+ /**
141
+ * Split a rendered `key: 'annotation'` field back into its parts. The key is
142
+ * always a GraphQL Name (no `: '`) and the annotation is always single-quoted,
143
+ * so splitting on the first `: '` is unambiguous.
144
+ */
145
+ splitField(rendered) {
146
+ const sep = rendered.indexOf(": '");
147
+ const key = rendered.slice(0, sep);
148
+ const annotation = rendered.slice(sep + 3, -1);
149
+ return { key, annotation };
150
+ }
151
+ /**
152
+ * Build a `TypedDict`. If every key is a valid Python attribute, the readable
153
+ * class syntax is used; otherwise (a key is a keyword / non-identifier under
154
+ * `'keep'`) the functional `TypedDict('Name', { 'key': 'annotation' })` syntax
155
+ * is emitted so the offending keys remain valid.
156
+ */
157
+ buildTypedDict(name, fields) {
158
+ const body = fields.filter(Boolean);
159
+ const parsed = body.map(f => this.splitField(f));
160
+ const needsFunctional = parsed.some(p => !isValidClassAttr(p.key));
161
+ if (!needsFunctional) {
162
+ return this.buildClass(name, 'TypedDict', body);
163
+ }
164
+ const entries = parsed.map(p => `${INDENT}'${p.key}': '${p.annotation}',`);
165
+ return [`${name} = TypedDict('${name}', {`, ...entries, '})'].join('\n');
166
+ }
167
+ ObjectTypeDefinition(node) {
168
+ if (this.config.onlyOperationTypes || this.config.onlyEnums)
169
+ return '';
170
+ if (node.name.value.startsWith('__'))
171
+ return '';
172
+ this._usesBaseModel = true;
173
+ return this.buildBaseModelClass(this.convertName(node), node.fields);
174
+ }
175
+ InterfaceTypeDefinition(node) {
176
+ if (this.config.onlyOperationTypes || this.config.onlyEnums)
177
+ return '';
178
+ if (node.name.value.startsWith('__'))
179
+ return '';
180
+ this._usesBaseModel = true;
181
+ return this.buildBaseModelClass(this.convertName(node), node.fields);
182
+ }
183
+ InputObjectTypeDefinition(node) {
184
+ if (this.config.onlyEnums)
185
+ return '';
186
+ if (node.name.value.startsWith('__'))
187
+ return '';
188
+ this._usesTypedDict = true;
189
+ // Under `'keep'`, a keyword / non-identifier field key forces functional
190
+ // `TypedDict` syntax; otherwise readable class syntax is used.
191
+ return this.buildTypedDict(this.convertName(node), node.fields);
192
+ }
193
+ EnumTypeDefinition(node) {
194
+ var _a;
195
+ if (node.name.value.startsWith('__'))
196
+ return '';
197
+ this._usesEnum = true;
198
+ const name = this.convertName(node, {
199
+ useTypesPrefix: this.config.enumPrefix,
200
+ useTypesSuffix: this.config.enumSuffix,
201
+ });
202
+ const members = ((_a = node.values) !== null && _a !== void 0 ? _a : []).map(value => `${INDENT}${value.name.value} = '${value.name.value}'`);
203
+ const body = members.length > 0 ? members : [`${INDENT}pass`];
204
+ return [`class ${name}(str, Enum):`, ...body].join('\n');
205
+ }
206
+ /** Scalars are handled inline via the scalar map; no standalone class. */
207
+ ScalarTypeDefinition() {
208
+ return '';
209
+ }
210
+ /**
211
+ * `union Foo = A | B` -> `Foo = Union['A', 'B']`.
212
+ *
213
+ * Must be overridden: `BaseTypesVisitor`'s implementation emits TypeScript
214
+ * (`export type Foo = A | B;`), and it dereferences the node description
215
+ * unguarded, so an undocumented union crashes the generator outright. A schema
216
+ * merely *containing* a union is enough to hit this, even when no operation
217
+ * selects one.
218
+ *
219
+ * Member names come from `parent[key]` — the original, un-visited node — the
220
+ * same way `typescript`'s visitor reads them. By `leave` time `node.types`
221
+ * has already been rendered into annotations (`Optional[Foo]`) and would
222
+ * otherwise be mangled. Members are quoted so a union declared before its
223
+ * members still resolves, matching how every other annotation in this plugin
224
+ * is emitted.
225
+ *
226
+ * Note this only makes a union *declaration* valid Python. Actually selecting
227
+ * union members in an operation needs inline-fragment support, which
228
+ * `python-operations` does not implement yet (it throws).
229
+ */
230
+ UnionTypeDefinition(node, key, parent) {
231
+ var _a;
232
+ if (this.config.onlyOperationTypes || this.config.onlyEnums)
233
+ return '';
234
+ if (node.name.value.startsWith('__'))
235
+ return '';
236
+ const originalNode = parent[key];
237
+ const members = ((_a = originalNode.types) !== null && _a !== void 0 ? _a : []).map(t => `'${this.convertName(t)}'`);
238
+ // A member-less union is legal to declare but has no inhabitant.
239
+ if (members.length === 0) {
240
+ return `${this.convertName(node)} = Any`;
241
+ }
242
+ this._usesUnion = true;
243
+ return `${this.convertName(node)} = Union[${members.join(', ')}]`;
244
+ }
245
+ /**
246
+ * Build the `prepend` import lines, emitting only what the generated
247
+ * `content` actually references.
248
+ */
249
+ getImports(content) {
250
+ const imports = [];
251
+ if (this._usesEnum) {
252
+ imports.push('from enum import Enum');
253
+ }
254
+ const typingParts = [];
255
+ if (content.includes('Optional[')) {
256
+ typingParts.push('Optional');
257
+ }
258
+ if (this._usesTypedDict) {
259
+ typingParts.push('TypedDict');
260
+ }
261
+ if (this._usesUnion) {
262
+ typingParts.push('Union');
263
+ }
264
+ if (/\bAny\b/.test(content)) {
265
+ typingParts.push('Any');
266
+ }
267
+ if (typingParts.length > 0) {
268
+ imports.push(`from typing import ${typingParts.join(', ')}`);
269
+ }
270
+ if (this._usesBaseModel) {
271
+ // `ConfigDict` / `Field` are pydantic names; emit them from the same module
272
+ // as the base model, and only when the generated content uses them.
273
+ const parts = [this.config.baseModel];
274
+ if (content.includes('ConfigDict(')) {
275
+ parts.push('ConfigDict');
276
+ }
277
+ if (content.includes('Field(')) {
278
+ parts.push('Field');
279
+ }
280
+ imports.push(`from ${this.config.runtimeImport} import ${parts.join(', ')}`);
281
+ }
282
+ return imports;
283
+ }
284
+ }
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@graphql-codegen/python",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.2-alpha-20261029093428-bb94591c48dc485d04089639f4ca0f124e339666",
4
+ "description": "GraphQL Code Generator plugin that emits Python types",
5
+ "sideEffects": false,
6
+ "peerDependencies": {
7
+ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
8
+ },
9
+ "dependencies": {
10
+ "@graphql-codegen/plugin-helpers": "^6.3.0",
11
+ "@graphql-codegen/python-common": "0.0.2-alpha-20261029093428-bb94591c48dc485d04089639f4ca0f124e339666",
12
+ "@graphql-codegen/visitor-plugin-common": "^6.3.0",
13
+ "auto-bind": "~4.0.0",
14
+ "tslib": "^2.8.1"
15
+ },
16
+ "repository": "git@github.com:dotansimha/graphql-code-generator-community.git",
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">= 16.0.0"
20
+ },
21
+ "main": "cjs/index.js",
22
+ "module": "esm/index.js",
23
+ "typings": "typings/index.d.ts",
24
+ "typescript": {
25
+ "definition": "typings/index.d.ts"
26
+ },
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "require": {
31
+ "types": "./typings/index.d.cts",
32
+ "default": "./cjs/index.js"
33
+ },
34
+ "import": {
35
+ "types": "./typings/index.d.ts",
36
+ "default": "./esm/index.js"
37
+ },
38
+ "default": {
39
+ "types": "./typings/index.d.ts",
40
+ "default": "./esm/index.js"
41
+ }
42
+ },
43
+ "./package.json": "./package.json"
44
+ }
6
45
  }
@@ -0,0 +1,110 @@
1
+ import { RawTypesConfig } from '@graphql-codegen/visitor-plugin-common';
2
+ /**
3
+ * @description This plugin generates Python types based on your `GraphQLSchema`.
4
+ *
5
+ * It generates idiomatic Python for your entire schema:
6
+ * - object & interface types become [pydantic](https://docs.pydantic.dev)
7
+ * `BaseModel` classes with snake_case attributes; each field whose Python name
8
+ * differs from the GraphQL key is pinned with `Field(alias=...)` so pydantic
9
+ * hydrates the wire (camelCase) key onto the attribute,
10
+ * - input types become `TypedDict`s keyed by the WIRE (GraphQL) name — inputs
11
+ * are sent to the server as-is, so their keys are never snake-cased,
12
+ * - enums become `str, Enum` classes,
13
+ * - scalars are mapped to native Python types.
14
+ *
15
+ * By default object / interface field names are converted to snake_case (see
16
+ * `namingConvention`). Every annotation is quoted so forward / recursive
17
+ * references resolve without ordering constraints.
18
+ */
19
+ export interface PythonPluginConfig extends RawTypesConfig {
20
+ /**
21
+ * @description The base class that generated object and interface types extend.
22
+ * @default "BaseModel"
23
+ *
24
+ * @exampleMarkdown
25
+ * ```ts filename="codegen.ts"
26
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
27
+ *
28
+ * const config: CodegenConfig = {
29
+ * generates: {
30
+ * 'schema.py': {
31
+ * plugins: ['@graphql-codegen/python'],
32
+ * config: {
33
+ * baseModel: 'BaseModel',
34
+ * },
35
+ * },
36
+ * },
37
+ * };
38
+ * export default config;
39
+ * ```
40
+ */
41
+ baseModel?: string;
42
+ /**
43
+ * @description The module the base model is imported from. Emitted as
44
+ * `from {runtimeImport} import {baseModel}` (with `ConfigDict` / `Field`
45
+ * appended when the generated types use aliases). Defaults to `pydantic`; if
46
+ * you override it, point it at a module that re-exports a pydantic-compatible
47
+ * `BaseModel`, `ConfigDict`, and `Field`.
48
+ * @default "pydantic"
49
+ *
50
+ * @exampleMarkdown
51
+ * ```ts filename="codegen.ts"
52
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
53
+ *
54
+ * const config: CodegenConfig = {
55
+ * generates: {
56
+ * 'schema.py': {
57
+ * plugins: ['@graphql-codegen/python'],
58
+ * config: {
59
+ * runtimeImport: 'pydantic',
60
+ * },
61
+ * },
62
+ * },
63
+ * };
64
+ * export default config;
65
+ * ```
66
+ */
67
+ runtimeImport?: string;
68
+ /**
69
+ * @description Controls how GraphQL OBJECT / INTERFACE field names are turned
70
+ * into Python class attributes. Input-type keys are unaffected — they always
71
+ * keep the wire (GraphQL) name because the runtime forwards the input dict to
72
+ * the server as-is.
73
+ *
74
+ * - `'snakeCase'` (default): field names are converted from camelCase to
75
+ * snake_case and Python reserved keywords are suffixed with `_`. This is the
76
+ * idiomatic-Python choice; each renamed field is pinned with `Field(alias=...)`
77
+ * (e.g. `from` -> `from_` with `alias='from'`) so pydantic hydrates the
78
+ * original wire key onto the attribute.
79
+ * - `'keep'`: object / interface field names are used verbatim; a keyword /
80
+ * non-identifier field falls back to a trailing `_` for just that field.
81
+ *
82
+ * @default "snakeCase"
83
+ *
84
+ * @exampleMarkdown
85
+ * ```ts filename="codegen.ts"
86
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
87
+ *
88
+ * const config: CodegenConfig = {
89
+ * generates: {
90
+ * 'schema.py': {
91
+ * plugins: ['@graphql-codegen/python'],
92
+ * config: {
93
+ * namingConvention: 'snakeCase',
94
+ * },
95
+ * },
96
+ * },
97
+ * };
98
+ * export default config;
99
+ * ```
100
+ */
101
+ namingConvention?: 'keep' | 'snakeCase';
102
+ /**
103
+ * @description Does not add a `__typename` field to the generated types.
104
+ * For the Python types plugin `__typename` is not emitted by default, so this
105
+ * option is accepted for parity but has no additional effect unless you opt in
106
+ * elsewhere.
107
+ * @default true
108
+ */
109
+ skipTypename?: boolean;
110
+ }
@@ -0,0 +1,110 @@
1
+ import { RawTypesConfig } from '@graphql-codegen/visitor-plugin-common';
2
+ /**
3
+ * @description This plugin generates Python types based on your `GraphQLSchema`.
4
+ *
5
+ * It generates idiomatic Python for your entire schema:
6
+ * - object & interface types become [pydantic](https://docs.pydantic.dev)
7
+ * `BaseModel` classes with snake_case attributes; each field whose Python name
8
+ * differs from the GraphQL key is pinned with `Field(alias=...)` so pydantic
9
+ * hydrates the wire (camelCase) key onto the attribute,
10
+ * - input types become `TypedDict`s keyed by the WIRE (GraphQL) name — inputs
11
+ * are sent to the server as-is, so their keys are never snake-cased,
12
+ * - enums become `str, Enum` classes,
13
+ * - scalars are mapped to native Python types.
14
+ *
15
+ * By default object / interface field names are converted to snake_case (see
16
+ * `namingConvention`). Every annotation is quoted so forward / recursive
17
+ * references resolve without ordering constraints.
18
+ */
19
+ export interface PythonPluginConfig extends RawTypesConfig {
20
+ /**
21
+ * @description The base class that generated object and interface types extend.
22
+ * @default "BaseModel"
23
+ *
24
+ * @exampleMarkdown
25
+ * ```ts filename="codegen.ts"
26
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
27
+ *
28
+ * const config: CodegenConfig = {
29
+ * generates: {
30
+ * 'schema.py': {
31
+ * plugins: ['@graphql-codegen/python'],
32
+ * config: {
33
+ * baseModel: 'BaseModel',
34
+ * },
35
+ * },
36
+ * },
37
+ * };
38
+ * export default config;
39
+ * ```
40
+ */
41
+ baseModel?: string;
42
+ /**
43
+ * @description The module the base model is imported from. Emitted as
44
+ * `from {runtimeImport} import {baseModel}` (with `ConfigDict` / `Field`
45
+ * appended when the generated types use aliases). Defaults to `pydantic`; if
46
+ * you override it, point it at a module that re-exports a pydantic-compatible
47
+ * `BaseModel`, `ConfigDict`, and `Field`.
48
+ * @default "pydantic"
49
+ *
50
+ * @exampleMarkdown
51
+ * ```ts filename="codegen.ts"
52
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
53
+ *
54
+ * const config: CodegenConfig = {
55
+ * generates: {
56
+ * 'schema.py': {
57
+ * plugins: ['@graphql-codegen/python'],
58
+ * config: {
59
+ * runtimeImport: 'pydantic',
60
+ * },
61
+ * },
62
+ * },
63
+ * };
64
+ * export default config;
65
+ * ```
66
+ */
67
+ runtimeImport?: string;
68
+ /**
69
+ * @description Controls how GraphQL OBJECT / INTERFACE field names are turned
70
+ * into Python class attributes. Input-type keys are unaffected — they always
71
+ * keep the wire (GraphQL) name because the runtime forwards the input dict to
72
+ * the server as-is.
73
+ *
74
+ * - `'snakeCase'` (default): field names are converted from camelCase to
75
+ * snake_case and Python reserved keywords are suffixed with `_`. This is the
76
+ * idiomatic-Python choice; each renamed field is pinned with `Field(alias=...)`
77
+ * (e.g. `from` -> `from_` with `alias='from'`) so pydantic hydrates the
78
+ * original wire key onto the attribute.
79
+ * - `'keep'`: object / interface field names are used verbatim; a keyword /
80
+ * non-identifier field falls back to a trailing `_` for just that field.
81
+ *
82
+ * @default "snakeCase"
83
+ *
84
+ * @exampleMarkdown
85
+ * ```ts filename="codegen.ts"
86
+ * import type { CodegenConfig } from '@graphql-codegen/cli';
87
+ *
88
+ * const config: CodegenConfig = {
89
+ * generates: {
90
+ * 'schema.py': {
91
+ * plugins: ['@graphql-codegen/python'],
92
+ * config: {
93
+ * namingConvention: 'snakeCase',
94
+ * },
95
+ * },
96
+ * },
97
+ * };
98
+ * export default config;
99
+ * ```
100
+ */
101
+ namingConvention?: 'keep' | 'snakeCase';
102
+ /**
103
+ * @description Does not add a `__typename` field to the generated types.
104
+ * For the Python types plugin `__typename` is not emitted by default, so this
105
+ * option is accepted for parity but has no additional effect unless you opt in
106
+ * elsewhere.
107
+ * @default true
108
+ */
109
+ skipTypename?: boolean;
110
+ }
@@ -0,0 +1,5 @@
1
+ import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
2
+ import { PythonPluginConfig } from './config.cjs';
3
+ export * from './config.cjs';
4
+ export * from './visitor.cjs';
5
+ export declare const plugin: PluginFunction<PythonPluginConfig, Types.ComplexPluginOutput>;
@@ -0,0 +1,5 @@
1
+ import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
2
+ import { PythonPluginConfig } from './config.js';
3
+ export * from './config.js';
4
+ export * from './visitor.js';
5
+ export declare const plugin: PluginFunction<PythonPluginConfig, Types.ComplexPluginOutput>;
@@ -0,0 +1,96 @@
1
+ import { EnumTypeDefinitionNode, FieldDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, InterfaceTypeDefinitionNode, ListTypeNode, NamedTypeNode, NonNullTypeNode, ObjectTypeDefinitionNode, UnionTypeDefinitionNode } from 'graphql';
2
+ import { PYTHON_SCALARS } from '@graphql-codegen/python-common';
3
+ import { BaseTypesVisitor, ParsedTypesConfig } from '@graphql-codegen/visitor-plugin-common';
4
+ import { PythonPluginConfig } from './config.cjs';
5
+ export { PYTHON_SCALARS };
6
+ export type PythonNamingConvention = 'keep' | 'snakeCase';
7
+ export interface PythonPluginParsedConfig extends ParsedTypesConfig {
8
+ baseModel: string;
9
+ runtimeImport: string;
10
+ pyNamingConvention: PythonNamingConvention;
11
+ }
12
+ export declare class PythonVisitor<TRawConfig extends PythonPluginConfig = PythonPluginConfig, TParsedConfig extends PythonPluginParsedConfig = PythonPluginParsedConfig> extends BaseTypesVisitor<TRawConfig, TParsedConfig> {
13
+ private _usesEnum;
14
+ private _usesTypedDict;
15
+ private _usesBaseModel;
16
+ private _usesUnion;
17
+ constructor(schema: GraphQLSchema, pluginConfig: TRawConfig);
18
+ /** Python has no `export` keyword; disable the C-family export prefix. */
19
+ protected getExportPrefix(): string;
20
+ protected _getScalar(name: string, type: 'input' | 'output'): string;
21
+ NamedType(node: NamedTypeNode, key: any, parent: any, path: any, ancestors: any): string;
22
+ ListType(node: ListTypeNode, key: any, parent: any, path: any, ancestors: any): string;
23
+ protected wrapWithListType(str: string): string;
24
+ NonNullType(node: NonNullTypeNode): string;
25
+ private snakeFieldName;
26
+ /**
27
+ * Field key for a `TypedDict` (input type). Input types are what the caller
28
+ * SENDS — the runtime forwards the dict to the server as-is — so they ALWAYS
29
+ * keep the wire (GraphQL) name regardless of `namingConvention`. If the name
30
+ * is a keyword / non-identifier the containing `TypedDict` is later emitted
31
+ * with functional syntax (see `buildTypedDict`).
32
+ */
33
+ private dictFieldName;
34
+ /**
35
+ * Field key for an object / interface type (a class attribute). Under `'keep'`
36
+ * a keyword / non-identifier name cannot be a class attribute and BaseModel
37
+ * cannot use functional `TypedDict` syntax, so we fall back to appending `_`
38
+ * for just that field.
39
+ */
40
+ private objectFieldName;
41
+ FieldDefinition(node: FieldDefinitionNode): string;
42
+ InputValueDefinition(node: InputValueDefinitionNode): string;
43
+ private buildClass;
44
+ /**
45
+ * Build a base-model subclass. Fields are class-level annotations; if any field
46
+ * pins a wire key with `Field(alias=...)`, a
47
+ * `model_config = ConfigDict(populate_by_name=True)` line is prepended so the
48
+ * class can be constructed by attribute name as well as by alias.
49
+ */
50
+ private buildBaseModelClass;
51
+ /**
52
+ * Split a rendered `key: 'annotation'` field back into its parts. The key is
53
+ * always a GraphQL Name (no `: '`) and the annotation is always single-quoted,
54
+ * so splitting on the first `: '` is unambiguous.
55
+ */
56
+ private splitField;
57
+ /**
58
+ * Build a `TypedDict`. If every key is a valid Python attribute, the readable
59
+ * class syntax is used; otherwise (a key is a keyword / non-identifier under
60
+ * `'keep'`) the functional `TypedDict('Name', { 'key': 'annotation' })` syntax
61
+ * is emitted so the offending keys remain valid.
62
+ */
63
+ private buildTypedDict;
64
+ ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string;
65
+ InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode): string;
66
+ InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string;
67
+ EnumTypeDefinition(node: EnumTypeDefinitionNode): string;
68
+ /** Scalars are handled inline via the scalar map; no standalone class. */
69
+ ScalarTypeDefinition(): string;
70
+ /**
71
+ * `union Foo = A | B` -> `Foo = Union['A', 'B']`.
72
+ *
73
+ * Must be overridden: `BaseTypesVisitor`'s implementation emits TypeScript
74
+ * (`export type Foo = A | B;`), and it dereferences the node description
75
+ * unguarded, so an undocumented union crashes the generator outright. A schema
76
+ * merely *containing* a union is enough to hit this, even when no operation
77
+ * selects one.
78
+ *
79
+ * Member names come from `parent[key]` — the original, un-visited node — the
80
+ * same way `typescript`'s visitor reads them. By `leave` time `node.types`
81
+ * has already been rendered into annotations (`Optional[Foo]`) and would
82
+ * otherwise be mangled. Members are quoted so a union declared before its
83
+ * members still resolves, matching how every other annotation in this plugin
84
+ * is emitted.
85
+ *
86
+ * Note this only makes a union *declaration* valid Python. Actually selecting
87
+ * union members in an operation needs inline-fragment support, which
88
+ * `python-operations` does not implement yet (it throws).
89
+ */
90
+ UnionTypeDefinition(node: UnionTypeDefinitionNode, key: string | number | undefined, parent: any): string;
91
+ /**
92
+ * Build the `prepend` import lines, emitting only what the generated
93
+ * `content` actually references.
94
+ */
95
+ getImports(content: string): string[];
96
+ }
@@ -0,0 +1,96 @@
1
+ import { EnumTypeDefinitionNode, FieldDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, InterfaceTypeDefinitionNode, ListTypeNode, NamedTypeNode, NonNullTypeNode, ObjectTypeDefinitionNode, UnionTypeDefinitionNode } from 'graphql';
2
+ import { PYTHON_SCALARS } from '@graphql-codegen/python-common';
3
+ import { BaseTypesVisitor, ParsedTypesConfig } from '@graphql-codegen/visitor-plugin-common';
4
+ import { PythonPluginConfig } from './config.js';
5
+ export { PYTHON_SCALARS };
6
+ export type PythonNamingConvention = 'keep' | 'snakeCase';
7
+ export interface PythonPluginParsedConfig extends ParsedTypesConfig {
8
+ baseModel: string;
9
+ runtimeImport: string;
10
+ pyNamingConvention: PythonNamingConvention;
11
+ }
12
+ export declare class PythonVisitor<TRawConfig extends PythonPluginConfig = PythonPluginConfig, TParsedConfig extends PythonPluginParsedConfig = PythonPluginParsedConfig> extends BaseTypesVisitor<TRawConfig, TParsedConfig> {
13
+ private _usesEnum;
14
+ private _usesTypedDict;
15
+ private _usesBaseModel;
16
+ private _usesUnion;
17
+ constructor(schema: GraphQLSchema, pluginConfig: TRawConfig);
18
+ /** Python has no `export` keyword; disable the C-family export prefix. */
19
+ protected getExportPrefix(): string;
20
+ protected _getScalar(name: string, type: 'input' | 'output'): string;
21
+ NamedType(node: NamedTypeNode, key: any, parent: any, path: any, ancestors: any): string;
22
+ ListType(node: ListTypeNode, key: any, parent: any, path: any, ancestors: any): string;
23
+ protected wrapWithListType(str: string): string;
24
+ NonNullType(node: NonNullTypeNode): string;
25
+ private snakeFieldName;
26
+ /**
27
+ * Field key for a `TypedDict` (input type). Input types are what the caller
28
+ * SENDS — the runtime forwards the dict to the server as-is — so they ALWAYS
29
+ * keep the wire (GraphQL) name regardless of `namingConvention`. If the name
30
+ * is a keyword / non-identifier the containing `TypedDict` is later emitted
31
+ * with functional syntax (see `buildTypedDict`).
32
+ */
33
+ private dictFieldName;
34
+ /**
35
+ * Field key for an object / interface type (a class attribute). Under `'keep'`
36
+ * a keyword / non-identifier name cannot be a class attribute and BaseModel
37
+ * cannot use functional `TypedDict` syntax, so we fall back to appending `_`
38
+ * for just that field.
39
+ */
40
+ private objectFieldName;
41
+ FieldDefinition(node: FieldDefinitionNode): string;
42
+ InputValueDefinition(node: InputValueDefinitionNode): string;
43
+ private buildClass;
44
+ /**
45
+ * Build a base-model subclass. Fields are class-level annotations; if any field
46
+ * pins a wire key with `Field(alias=...)`, a
47
+ * `model_config = ConfigDict(populate_by_name=True)` line is prepended so the
48
+ * class can be constructed by attribute name as well as by alias.
49
+ */
50
+ private buildBaseModelClass;
51
+ /**
52
+ * Split a rendered `key: 'annotation'` field back into its parts. The key is
53
+ * always a GraphQL Name (no `: '`) and the annotation is always single-quoted,
54
+ * so splitting on the first `: '` is unambiguous.
55
+ */
56
+ private splitField;
57
+ /**
58
+ * Build a `TypedDict`. If every key is a valid Python attribute, the readable
59
+ * class syntax is used; otherwise (a key is a keyword / non-identifier under
60
+ * `'keep'`) the functional `TypedDict('Name', { 'key': 'annotation' })` syntax
61
+ * is emitted so the offending keys remain valid.
62
+ */
63
+ private buildTypedDict;
64
+ ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string;
65
+ InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode): string;
66
+ InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string;
67
+ EnumTypeDefinition(node: EnumTypeDefinitionNode): string;
68
+ /** Scalars are handled inline via the scalar map; no standalone class. */
69
+ ScalarTypeDefinition(): string;
70
+ /**
71
+ * `union Foo = A | B` -> `Foo = Union['A', 'B']`.
72
+ *
73
+ * Must be overridden: `BaseTypesVisitor`'s implementation emits TypeScript
74
+ * (`export type Foo = A | B;`), and it dereferences the node description
75
+ * unguarded, so an undocumented union crashes the generator outright. A schema
76
+ * merely *containing* a union is enough to hit this, even when no operation
77
+ * selects one.
78
+ *
79
+ * Member names come from `parent[key]` — the original, un-visited node — the
80
+ * same way `typescript`'s visitor reads them. By `leave` time `node.types`
81
+ * has already been rendered into annotations (`Optional[Foo]`) and would
82
+ * otherwise be mangled. Members are quoted so a union declared before its
83
+ * members still resolves, matching how every other annotation in this plugin
84
+ * is emitted.
85
+ *
86
+ * Note this only makes a union *declaration* valid Python. Actually selecting
87
+ * union members in an operation needs inline-fragment support, which
88
+ * `python-operations` does not implement yet (it throws).
89
+ */
90
+ UnionTypeDefinition(node: UnionTypeDefinitionNode, key: string | number | undefined, parent: any): string;
91
+ /**
92
+ * Build the `prepend` import lines, emitting only what the generated
93
+ * `content` actually references.
94
+ */
95
+ getImports(content: string): string[];
96
+ }