@graphql-codegen/python 0.0.1 → 0.0.2
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/package.json +26 -36
- package/dist/README.md +0 -23
- package/dist/package.json +0 -45
- package/src/config.ts +0 -111
- package/src/index.ts +0 -32
- package/src/visitor.ts +0 -346
- package/tests/__snapshots__/python.spec.ts.snap +0 -37
- package/tests/python.spec.ts +0 -286
- package/vitest.config.ts +0 -12
- /package/{dist/cjs → cjs}/config.js +0 -0
- /package/{dist/cjs → cjs}/index.js +0 -0
- /package/{dist/cjs → cjs}/package.json +0 -0
- /package/{dist/cjs → cjs}/visitor.js +0 -0
- /package/{dist/esm → esm}/config.js +0 -0
- /package/{dist/esm → esm}/index.js +0 -0
- /package/{dist/esm → esm}/visitor.js +0 -0
- /package/{dist/typings → typings}/config.d.cts +0 -0
- /package/{dist/typings → typings}/config.d.ts +0 -0
- /package/{dist/typings → typings}/index.d.cts +0 -0
- /package/{dist/typings → typings}/index.d.ts +0 -0
- /package/{dist/typings → typings}/visitor.d.cts +0 -0
- /package/{dist/typings → typings}/visitor.d.ts +0 -0
package/package.json
CHANGED
|
@@ -1,55 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphql-codegen/python",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "0.0.2",
|
|
5
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",
|
|
12
|
+
"@graphql-codegen/visitor-plugin-common": "^6.3.0",
|
|
13
|
+
"auto-bind": "~4.0.0",
|
|
14
|
+
"tslib": "^2.8.1"
|
|
15
|
+
},
|
|
6
16
|
"repository": "git@github.com:dotansimha/graphql-code-generator-community.git",
|
|
7
17
|
"license": "MIT",
|
|
8
18
|
"engines": {
|
|
9
19
|
"node": ">= 16.0.0"
|
|
10
20
|
},
|
|
11
|
-
"main": "
|
|
12
|
-
"module": "
|
|
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",
|
|
13
28
|
"exports": {
|
|
14
29
|
".": {
|
|
15
30
|
"require": {
|
|
16
|
-
"types": "./
|
|
17
|
-
"default": "./
|
|
31
|
+
"types": "./typings/index.d.cts",
|
|
32
|
+
"default": "./cjs/index.js"
|
|
18
33
|
},
|
|
19
34
|
"import": {
|
|
20
|
-
"types": "./
|
|
21
|
-
"default": "./
|
|
35
|
+
"types": "./typings/index.d.ts",
|
|
36
|
+
"default": "./esm/index.js"
|
|
22
37
|
},
|
|
23
38
|
"default": {
|
|
24
|
-
"types": "./
|
|
25
|
-
"default": "./
|
|
39
|
+
"types": "./typings/index.d.ts",
|
|
40
|
+
"default": "./esm/index.js"
|
|
26
41
|
}
|
|
27
42
|
},
|
|
28
43
|
"./package.json": "./package.json"
|
|
29
|
-
},
|
|
30
|
-
"typings": "dist/typings/index.d.ts",
|
|
31
|
-
"scripts": {
|
|
32
|
-
"lint": "eslint **/*.ts"
|
|
33
|
-
},
|
|
34
|
-
"peerDependencies": {
|
|
35
|
-
"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"
|
|
36
|
-
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"@graphql-codegen/plugin-helpers": "^6.3.0",
|
|
39
|
-
"@graphql-codegen/python-common": "0.0.1",
|
|
40
|
-
"@graphql-codegen/visitor-plugin-common": "^6.3.0",
|
|
41
|
-
"auto-bind": "~4.0.0",
|
|
42
|
-
"tslib": "^2.8.1"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@graphql-codegen/testing": "4.0.5"
|
|
46
|
-
},
|
|
47
|
-
"publishConfig": {
|
|
48
|
-
"directory": "dist",
|
|
49
|
-
"access": "public"
|
|
50
|
-
},
|
|
51
|
-
"sideEffects": false,
|
|
52
|
-
"typescript": {
|
|
53
|
-
"definition": "dist/typings/index.d.ts"
|
|
54
44
|
}
|
|
55
|
-
}
|
|
45
|
+
}
|
package/dist/README.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# @graphql-codegen/python
|
|
2
|
-
|
|
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/dist/package.json
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@graphql-codegen/python",
|
|
3
|
-
"version": "0.0.1",
|
|
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.1",
|
|
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
|
-
}
|
|
45
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { RawTypesConfig } from '@graphql-codegen/visitor-plugin-common';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @description This plugin generates Python types based on your `GraphQLSchema`.
|
|
5
|
-
*
|
|
6
|
-
* It generates idiomatic Python for your entire schema:
|
|
7
|
-
* - object & interface types become [pydantic](https://docs.pydantic.dev)
|
|
8
|
-
* `BaseModel` classes with snake_case attributes; each field whose Python name
|
|
9
|
-
* differs from the GraphQL key is pinned with `Field(alias=...)` so pydantic
|
|
10
|
-
* hydrates the wire (camelCase) key onto the attribute,
|
|
11
|
-
* - input types become `TypedDict`s keyed by the WIRE (GraphQL) name — inputs
|
|
12
|
-
* are sent to the server as-is, so their keys are never snake-cased,
|
|
13
|
-
* - enums become `str, Enum` classes,
|
|
14
|
-
* - scalars are mapped to native Python types.
|
|
15
|
-
*
|
|
16
|
-
* By default object / interface field names are converted to snake_case (see
|
|
17
|
-
* `namingConvention`). Every annotation is quoted so forward / recursive
|
|
18
|
-
* references resolve without ordering constraints.
|
|
19
|
-
*/
|
|
20
|
-
export interface PythonPluginConfig extends RawTypesConfig {
|
|
21
|
-
/**
|
|
22
|
-
* @description The base class that generated object and interface types extend.
|
|
23
|
-
* @default "BaseModel"
|
|
24
|
-
*
|
|
25
|
-
* @exampleMarkdown
|
|
26
|
-
* ```ts filename="codegen.ts"
|
|
27
|
-
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
28
|
-
*
|
|
29
|
-
* const config: CodegenConfig = {
|
|
30
|
-
* generates: {
|
|
31
|
-
* 'schema.py': {
|
|
32
|
-
* plugins: ['@graphql-codegen/python'],
|
|
33
|
-
* config: {
|
|
34
|
-
* baseModel: 'BaseModel',
|
|
35
|
-
* },
|
|
36
|
-
* },
|
|
37
|
-
* },
|
|
38
|
-
* };
|
|
39
|
-
* export default config;
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
baseModel?: string;
|
|
43
|
-
/**
|
|
44
|
-
* @description The module the base model is imported from. Emitted as
|
|
45
|
-
* `from {runtimeImport} import {baseModel}` (with `ConfigDict` / `Field`
|
|
46
|
-
* appended when the generated types use aliases). Defaults to `pydantic`; if
|
|
47
|
-
* you override it, point it at a module that re-exports a pydantic-compatible
|
|
48
|
-
* `BaseModel`, `ConfigDict`, and `Field`.
|
|
49
|
-
* @default "pydantic"
|
|
50
|
-
*
|
|
51
|
-
* @exampleMarkdown
|
|
52
|
-
* ```ts filename="codegen.ts"
|
|
53
|
-
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
54
|
-
*
|
|
55
|
-
* const config: CodegenConfig = {
|
|
56
|
-
* generates: {
|
|
57
|
-
* 'schema.py': {
|
|
58
|
-
* plugins: ['@graphql-codegen/python'],
|
|
59
|
-
* config: {
|
|
60
|
-
* runtimeImport: 'pydantic',
|
|
61
|
-
* },
|
|
62
|
-
* },
|
|
63
|
-
* },
|
|
64
|
-
* };
|
|
65
|
-
* export default config;
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
runtimeImport?: string;
|
|
69
|
-
/**
|
|
70
|
-
* @description Controls how GraphQL OBJECT / INTERFACE field names are turned
|
|
71
|
-
* into Python class attributes. Input-type keys are unaffected — they always
|
|
72
|
-
* keep the wire (GraphQL) name because the runtime forwards the input dict to
|
|
73
|
-
* the server as-is.
|
|
74
|
-
*
|
|
75
|
-
* - `'snakeCase'` (default): field names are converted from camelCase to
|
|
76
|
-
* snake_case and Python reserved keywords are suffixed with `_`. This is the
|
|
77
|
-
* idiomatic-Python choice; each renamed field is pinned with `Field(alias=...)`
|
|
78
|
-
* (e.g. `from` -> `from_` with `alias='from'`) so pydantic hydrates the
|
|
79
|
-
* original wire key onto the attribute.
|
|
80
|
-
* - `'keep'`: object / interface field names are used verbatim; a keyword /
|
|
81
|
-
* non-identifier field falls back to a trailing `_` for just that field.
|
|
82
|
-
*
|
|
83
|
-
* @default "snakeCase"
|
|
84
|
-
*
|
|
85
|
-
* @exampleMarkdown
|
|
86
|
-
* ```ts filename="codegen.ts"
|
|
87
|
-
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
88
|
-
*
|
|
89
|
-
* const config: CodegenConfig = {
|
|
90
|
-
* generates: {
|
|
91
|
-
* 'schema.py': {
|
|
92
|
-
* plugins: ['@graphql-codegen/python'],
|
|
93
|
-
* config: {
|
|
94
|
-
* namingConvention: 'snakeCase',
|
|
95
|
-
* },
|
|
96
|
-
* },
|
|
97
|
-
* },
|
|
98
|
-
* };
|
|
99
|
-
* export default config;
|
|
100
|
-
* ```
|
|
101
|
-
*/
|
|
102
|
-
namingConvention?: 'keep' | 'snakeCase';
|
|
103
|
-
/**
|
|
104
|
-
* @description Does not add a `__typename` field to the generated types.
|
|
105
|
-
* For the Python types plugin `__typename` is not emitted by default, so this
|
|
106
|
-
* option is accepted for parity but has no additional effect unless you opt in
|
|
107
|
-
* elsewhere.
|
|
108
|
-
* @default true
|
|
109
|
-
*/
|
|
110
|
-
skipTypename?: boolean;
|
|
111
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { GraphQLSchema } from 'graphql';
|
|
2
|
-
import {
|
|
3
|
-
getCachedDocumentNodeFromSchema,
|
|
4
|
-
oldVisit,
|
|
5
|
-
PluginFunction,
|
|
6
|
-
Types,
|
|
7
|
-
} from '@graphql-codegen/plugin-helpers';
|
|
8
|
-
import { PythonPluginConfig } from './config.js';
|
|
9
|
-
import { PythonVisitor } from './visitor.js';
|
|
10
|
-
|
|
11
|
-
export * from './config.js';
|
|
12
|
-
export * from './visitor.js';
|
|
13
|
-
|
|
14
|
-
export const plugin: PluginFunction<PythonPluginConfig, Types.ComplexPluginOutput> = (
|
|
15
|
-
schema: GraphQLSchema,
|
|
16
|
-
_documents: Types.DocumentFile[],
|
|
17
|
-
config: PythonPluginConfig,
|
|
18
|
-
) => {
|
|
19
|
-
const astNode = getCachedDocumentNodeFromSchema(schema);
|
|
20
|
-
const visitor = new PythonVisitor(schema, config);
|
|
21
|
-
|
|
22
|
-
const visitorResult = oldVisit(astNode, { leave: visitor });
|
|
23
|
-
|
|
24
|
-
const content = (visitorResult.definitions as unknown[])
|
|
25
|
-
.filter((def): def is string => typeof def === 'string' && def.length > 0)
|
|
26
|
-
.join('\n\n\n');
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
prepend: visitor.getImports(content),
|
|
30
|
-
content,
|
|
31
|
-
};
|
|
32
|
-
};
|
package/src/visitor.ts
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import autoBind from 'auto-bind';
|
|
2
|
-
import {
|
|
3
|
-
EnumTypeDefinitionNode,
|
|
4
|
-
FieldDefinitionNode,
|
|
5
|
-
GraphQLSchema,
|
|
6
|
-
InputObjectTypeDefinitionNode,
|
|
7
|
-
InputValueDefinitionNode,
|
|
8
|
-
InterfaceTypeDefinitionNode,
|
|
9
|
-
ListTypeNode,
|
|
10
|
-
NamedTypeNode,
|
|
11
|
-
NonNullTypeNode,
|
|
12
|
-
ObjectTypeDefinitionNode,
|
|
13
|
-
UnionTypeDefinitionNode,
|
|
14
|
-
} from 'graphql';
|
|
15
|
-
import { isValidClassAttr, PYTHON_SCALARS, pythonFieldName } from '@graphql-codegen/python-common';
|
|
16
|
-
import {
|
|
17
|
-
BaseTypesVisitor,
|
|
18
|
-
getConfigValue,
|
|
19
|
-
ParsedTypesConfig,
|
|
20
|
-
} from '@graphql-codegen/visitor-plugin-common';
|
|
21
|
-
import { PythonPluginConfig } from './config.js';
|
|
22
|
-
|
|
23
|
-
export { PYTHON_SCALARS };
|
|
24
|
-
|
|
25
|
-
const INDENT = ' ';
|
|
26
|
-
|
|
27
|
-
export type PythonNamingConvention = 'keep' | 'snakeCase';
|
|
28
|
-
|
|
29
|
-
export interface PythonPluginParsedConfig extends ParsedTypesConfig {
|
|
30
|
-
baseModel: string;
|
|
31
|
-
runtimeImport: string;
|
|
32
|
-
pyNamingConvention: PythonNamingConvention;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class PythonVisitor<
|
|
36
|
-
TRawConfig extends PythonPluginConfig = PythonPluginConfig,
|
|
37
|
-
TParsedConfig extends PythonPluginParsedConfig = PythonPluginParsedConfig,
|
|
38
|
-
> extends BaseTypesVisitor<TRawConfig, TParsedConfig> {
|
|
39
|
-
private _usesEnum = false;
|
|
40
|
-
private _usesTypedDict = false;
|
|
41
|
-
private _usesBaseModel = false;
|
|
42
|
-
private _usesUnion = false;
|
|
43
|
-
|
|
44
|
-
constructor(schema: GraphQLSchema, pluginConfig: TRawConfig) {
|
|
45
|
-
// `namingConvention` here is our own field-naming option (`'keep'` |
|
|
46
|
-
// `'snakeCase'`) and must NOT leak into the base visitor's `namingConvention`
|
|
47
|
-
// (which controls TYPE-name conversion via `convertFactory`). Strip it before
|
|
48
|
-
// calling `super` so type names keep their default (PascalCase) conversion.
|
|
49
|
-
super(
|
|
50
|
-
schema,
|
|
51
|
-
// Route custom scalars (no explicit mapping) to `Any` instead of `unknown`.
|
|
52
|
-
{
|
|
53
|
-
...pluginConfig,
|
|
54
|
-
namingConvention: undefined,
|
|
55
|
-
defaultScalarType: getConfigValue(pluginConfig.defaultScalarType, 'Any'),
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
baseModel: getConfigValue(pluginConfig.baseModel, 'BaseModel'),
|
|
59
|
-
runtimeImport: getConfigValue(pluginConfig.runtimeImport, 'pydantic'),
|
|
60
|
-
pyNamingConvention: getConfigValue(pluginConfig.namingConvention, 'snakeCase'),
|
|
61
|
-
} as TParsedConfig,
|
|
62
|
-
PYTHON_SCALARS,
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
autoBind(this);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Python has no `export` keyword; disable the C-family export prefix. */
|
|
69
|
-
protected getExportPrefix(): string {
|
|
70
|
-
return '';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
protected _getScalar(name: string, type: 'input' | 'output'): string {
|
|
74
|
-
const mapped = this.scalars[name];
|
|
75
|
-
if (mapped) {
|
|
76
|
-
return mapped[type];
|
|
77
|
-
}
|
|
78
|
-
return 'Any';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// --- Nullability / list composition (emit Python instead of TS) ---
|
|
82
|
-
|
|
83
|
-
NamedType(node: NamedTypeNode, key: any, parent: any, path: any, ancestors: any): string {
|
|
84
|
-
return `Optional[${super.NamedType(node, key, parent, path, ancestors)}]`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
ListType(node: ListTypeNode, key: any, parent: any, path: any, ancestors: any): string {
|
|
88
|
-
return `Optional[${super.ListType(node, key, parent, path, ancestors)}]`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
protected wrapWithListType(str: string): string {
|
|
92
|
-
return `list[${str}]`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
NonNullType(node: NonNullTypeNode): string {
|
|
96
|
-
const baseValue = super.NonNullType(node);
|
|
97
|
-
if (baseValue.startsWith('Optional[') && baseValue.endsWith(']')) {
|
|
98
|
-
return baseValue.slice('Optional['.length, -1);
|
|
99
|
-
}
|
|
100
|
-
return baseValue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// --- Field rendering (name: 'annotation') ---
|
|
104
|
-
|
|
105
|
-
private snakeFieldName(name: string): string {
|
|
106
|
-
return pythonFieldName(name);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Field key for a `TypedDict` (input type). Input types are what the caller
|
|
111
|
-
* SENDS — the runtime forwards the dict to the server as-is — so they ALWAYS
|
|
112
|
-
* keep the wire (GraphQL) name regardless of `namingConvention`. If the name
|
|
113
|
-
* is a keyword / non-identifier the containing `TypedDict` is later emitted
|
|
114
|
-
* with functional syntax (see `buildTypedDict`).
|
|
115
|
-
*/
|
|
116
|
-
private dictFieldName(name: string): string {
|
|
117
|
-
return name;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Field key for an object / interface type (a class attribute). Under `'keep'`
|
|
122
|
-
* a keyword / non-identifier name cannot be a class attribute and BaseModel
|
|
123
|
-
* cannot use functional `TypedDict` syntax, so we fall back to appending `_`
|
|
124
|
-
* for just that field.
|
|
125
|
-
*/
|
|
126
|
-
private objectFieldName(name: string): string {
|
|
127
|
-
if (this.config.pyNamingConvention === 'snakeCase') {
|
|
128
|
-
return this.snakeFieldName(name);
|
|
129
|
-
}
|
|
130
|
-
return isValidClassAttr(name) ? name : `${name}_`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
FieldDefinition(node: FieldDefinitionNode): string {
|
|
134
|
-
if (this.config.onlyEnums) return '';
|
|
135
|
-
const wire = node.name.value;
|
|
136
|
-
const attr = this.objectFieldName(wire);
|
|
137
|
-
// The rendered annotation (e.g. `Optional[str]`, `list[User]`). Quoted below
|
|
138
|
-
// so forward / recursive references resolve without ordering constraints.
|
|
139
|
-
const annotation = node.type as unknown as string;
|
|
140
|
-
const optional = annotation.startsWith('Optional[');
|
|
141
|
-
// When the Python attribute differs from the GraphQL wire key (snake_case
|
|
142
|
-
// conversion or a keyword escaped with `_`), pin the wire key with a pydantic
|
|
143
|
-
// `Field(alias=...)` so hydration maps it onto the attribute. Optional fields
|
|
144
|
-
// also default to `None` so a missing key is allowed.
|
|
145
|
-
let assignment = '';
|
|
146
|
-
if (attr !== wire) {
|
|
147
|
-
assignment = optional
|
|
148
|
-
? ` = Field(default=None, alias='${wire}')`
|
|
149
|
-
: ` = Field(alias='${wire}')`;
|
|
150
|
-
} else if (optional) {
|
|
151
|
-
assignment = ' = None';
|
|
152
|
-
}
|
|
153
|
-
return `${attr}: '${annotation}'${assignment}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
InputValueDefinition(node: InputValueDefinitionNode): string {
|
|
157
|
-
if (this.config.onlyEnums) return '';
|
|
158
|
-
return `${this.dictFieldName(node.name.value)}: '${node.type as unknown as string}'`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// --- Class / block builder ---
|
|
162
|
-
|
|
163
|
-
private buildClass(name: string, base: string, fields: readonly string[]): string {
|
|
164
|
-
const header = `class ${name}(${base}):`;
|
|
165
|
-
const body = fields.filter(Boolean);
|
|
166
|
-
if (body.length === 0) {
|
|
167
|
-
return `${header}\n${INDENT}pass`;
|
|
168
|
-
}
|
|
169
|
-
return [header, ...body.map(f => INDENT + f)].join('\n');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Build a base-model subclass. Fields are class-level annotations; if any field
|
|
174
|
-
* pins a wire key with `Field(alias=...)`, a
|
|
175
|
-
* `model_config = ConfigDict(populate_by_name=True)` line is prepended so the
|
|
176
|
-
* class can be constructed by attribute name as well as by alias.
|
|
177
|
-
*/
|
|
178
|
-
private buildBaseModelClass(name: string, fields: readonly string[]): string {
|
|
179
|
-
const header = `class ${name}(${this.config.baseModel}):`;
|
|
180
|
-
const body = fields.filter(Boolean);
|
|
181
|
-
if (body.length === 0) {
|
|
182
|
-
return `${header}\n${INDENT}pass`;
|
|
183
|
-
}
|
|
184
|
-
const lines = [header];
|
|
185
|
-
if (body.some(f => f.includes('alias='))) {
|
|
186
|
-
lines.push(`${INDENT}model_config = ConfigDict(populate_by_name=True)`);
|
|
187
|
-
}
|
|
188
|
-
lines.push(...body.map(f => INDENT + f));
|
|
189
|
-
return lines.join('\n');
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Split a rendered `key: 'annotation'` field back into its parts. The key is
|
|
194
|
-
* always a GraphQL Name (no `: '`) and the annotation is always single-quoted,
|
|
195
|
-
* so splitting on the first `: '` is unambiguous.
|
|
196
|
-
*/
|
|
197
|
-
private splitField(rendered: string): { key: string; annotation: string } {
|
|
198
|
-
const sep = rendered.indexOf(": '");
|
|
199
|
-
const key = rendered.slice(0, sep);
|
|
200
|
-
const annotation = rendered.slice(sep + 3, -1);
|
|
201
|
-
return { key, annotation };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Build a `TypedDict`. If every key is a valid Python attribute, the readable
|
|
206
|
-
* class syntax is used; otherwise (a key is a keyword / non-identifier under
|
|
207
|
-
* `'keep'`) the functional `TypedDict('Name', { 'key': 'annotation' })` syntax
|
|
208
|
-
* is emitted so the offending keys remain valid.
|
|
209
|
-
*/
|
|
210
|
-
private buildTypedDict(name: string, fields: readonly string[]): string {
|
|
211
|
-
const body = fields.filter(Boolean);
|
|
212
|
-
const parsed = body.map(f => this.splitField(f));
|
|
213
|
-
const needsFunctional = parsed.some(p => !isValidClassAttr(p.key));
|
|
214
|
-
if (!needsFunctional) {
|
|
215
|
-
return this.buildClass(name, 'TypedDict', body);
|
|
216
|
-
}
|
|
217
|
-
const entries = parsed.map(p => `${INDENT}'${p.key}': '${p.annotation}',`);
|
|
218
|
-
return [`${name} = TypedDict('${name}', {`, ...entries, '})'].join('\n');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string {
|
|
222
|
-
if (this.config.onlyOperationTypes || this.config.onlyEnums) return '';
|
|
223
|
-
if (node.name.value.startsWith('__')) return '';
|
|
224
|
-
this._usesBaseModel = true;
|
|
225
|
-
return this.buildBaseModelClass(this.convertName(node), node.fields as unknown as string[]);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode): string {
|
|
229
|
-
if (this.config.onlyOperationTypes || this.config.onlyEnums) return '';
|
|
230
|
-
if (node.name.value.startsWith('__')) return '';
|
|
231
|
-
this._usesBaseModel = true;
|
|
232
|
-
return this.buildBaseModelClass(this.convertName(node), node.fields as unknown as string[]);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string {
|
|
236
|
-
if (this.config.onlyEnums) return '';
|
|
237
|
-
if (node.name.value.startsWith('__')) return '';
|
|
238
|
-
this._usesTypedDict = true;
|
|
239
|
-
// Under `'keep'`, a keyword / non-identifier field key forces functional
|
|
240
|
-
// `TypedDict` syntax; otherwise readable class syntax is used.
|
|
241
|
-
return this.buildTypedDict(this.convertName(node), node.fields as unknown as string[]);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
EnumTypeDefinition(node: EnumTypeDefinitionNode): string {
|
|
245
|
-
if (node.name.value.startsWith('__')) return '';
|
|
246
|
-
this._usesEnum = true;
|
|
247
|
-
const name = this.convertName(node, {
|
|
248
|
-
useTypesPrefix: this.config.enumPrefix,
|
|
249
|
-
useTypesSuffix: this.config.enumSuffix,
|
|
250
|
-
});
|
|
251
|
-
const members = (node.values ?? []).map(
|
|
252
|
-
value => `${INDENT}${value.name.value} = '${value.name.value}'`,
|
|
253
|
-
);
|
|
254
|
-
const body = members.length > 0 ? members : [`${INDENT}pass`];
|
|
255
|
-
return [`class ${name}(str, Enum):`, ...body].join('\n');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Scalars are handled inline via the scalar map; no standalone class. */
|
|
259
|
-
ScalarTypeDefinition(): string {
|
|
260
|
-
return '';
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* `union Foo = A | B` -> `Foo = Union['A', 'B']`.
|
|
265
|
-
*
|
|
266
|
-
* Must be overridden: `BaseTypesVisitor`'s implementation emits TypeScript
|
|
267
|
-
* (`export type Foo = A | B;`), and it dereferences the node description
|
|
268
|
-
* unguarded, so an undocumented union crashes the generator outright. A schema
|
|
269
|
-
* merely *containing* a union is enough to hit this, even when no operation
|
|
270
|
-
* selects one.
|
|
271
|
-
*
|
|
272
|
-
* Member names come from `parent[key]` — the original, un-visited node — the
|
|
273
|
-
* same way `typescript`'s visitor reads them. By `leave` time `node.types`
|
|
274
|
-
* has already been rendered into annotations (`Optional[Foo]`) and would
|
|
275
|
-
* otherwise be mangled. Members are quoted so a union declared before its
|
|
276
|
-
* members still resolves, matching how every other annotation in this plugin
|
|
277
|
-
* is emitted.
|
|
278
|
-
*
|
|
279
|
-
* Note this only makes a union *declaration* valid Python. Actually selecting
|
|
280
|
-
* union members in an operation needs inline-fragment support, which
|
|
281
|
-
* `python-operations` does not implement yet (it throws).
|
|
282
|
-
*/
|
|
283
|
-
UnionTypeDefinition(
|
|
284
|
-
node: UnionTypeDefinitionNode,
|
|
285
|
-
key: string | number | undefined,
|
|
286
|
-
parent: any,
|
|
287
|
-
): string {
|
|
288
|
-
if (this.config.onlyOperationTypes || this.config.onlyEnums) return '';
|
|
289
|
-
if (node.name.value.startsWith('__')) return '';
|
|
290
|
-
|
|
291
|
-
const originalNode = parent[key] as UnionTypeDefinitionNode;
|
|
292
|
-
const members = (originalNode.types ?? []).map(t => `'${this.convertName(t)}'`);
|
|
293
|
-
|
|
294
|
-
// A member-less union is legal to declare but has no inhabitant.
|
|
295
|
-
if (members.length === 0) {
|
|
296
|
-
return `${this.convertName(node)} = Any`;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
this._usesUnion = true;
|
|
300
|
-
return `${this.convertName(node)} = Union[${members.join(', ')}]`;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Build the `prepend` import lines, emitting only what the generated
|
|
305
|
-
* `content` actually references.
|
|
306
|
-
*/
|
|
307
|
-
getImports(content: string): string[] {
|
|
308
|
-
const imports: string[] = [];
|
|
309
|
-
|
|
310
|
-
if (this._usesEnum) {
|
|
311
|
-
imports.push('from enum import Enum');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const typingParts: string[] = [];
|
|
315
|
-
if (content.includes('Optional[')) {
|
|
316
|
-
typingParts.push('Optional');
|
|
317
|
-
}
|
|
318
|
-
if (this._usesTypedDict) {
|
|
319
|
-
typingParts.push('TypedDict');
|
|
320
|
-
}
|
|
321
|
-
if (this._usesUnion) {
|
|
322
|
-
typingParts.push('Union');
|
|
323
|
-
}
|
|
324
|
-
if (/\bAny\b/.test(content)) {
|
|
325
|
-
typingParts.push('Any');
|
|
326
|
-
}
|
|
327
|
-
if (typingParts.length > 0) {
|
|
328
|
-
imports.push(`from typing import ${typingParts.join(', ')}`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (this._usesBaseModel) {
|
|
332
|
-
// `ConfigDict` / `Field` are pydantic names; emit them from the same module
|
|
333
|
-
// as the base model, and only when the generated content uses them.
|
|
334
|
-
const parts = [this.config.baseModel];
|
|
335
|
-
if (content.includes('ConfigDict(')) {
|
|
336
|
-
parts.push('ConfigDict');
|
|
337
|
-
}
|
|
338
|
-
if (content.includes('Field(')) {
|
|
339
|
-
parts.push('Field');
|
|
340
|
-
}
|
|
341
|
-
imports.push(`from ${this.config.runtimeImport} import ${parts.join(', ')}`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return imports;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`@graphql-codegen/python > matches the full schema snapshot 1`] = `
|
|
4
|
-
"from enum import Enum
|
|
5
|
-
from typing import Optional, TypedDict, Any
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Role(str, Enum):
|
|
10
|
-
ADMIN = 'ADMIN'
|
|
11
|
-
USER = 'USER'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Node(BaseModel):
|
|
15
|
-
id: 'str'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class User(BaseModel):
|
|
19
|
-
model_config = ConfigDict(populate_by_name=True)
|
|
20
|
-
id: 'str'
|
|
21
|
-
name: 'str'
|
|
22
|
-
email: 'Optional[str]' = None
|
|
23
|
-
role: 'Role'
|
|
24
|
-
friends: 'list[User]'
|
|
25
|
-
created_at: 'Optional[Any]' = Field(default=None, alias='createdAt')
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
UserFilter = TypedDict('UserFilter', {
|
|
29
|
-
'role': 'Optional[Role]',
|
|
30
|
-
'from': 'Optional[str]',
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class Query(BaseModel):
|
|
35
|
-
users: 'list[User]'
|
|
36
|
-
"
|
|
37
|
-
`;
|
package/tests/python.spec.ts
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { buildSchema } from 'graphql';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import type { PythonPluginConfig } from '../src/config.js';
|
|
4
|
-
import { plugin } from '../src/index.js';
|
|
5
|
-
|
|
6
|
-
const FULL_SCHEMA = /* GraphQL */ `
|
|
7
|
-
scalar DateTime
|
|
8
|
-
enum Role {
|
|
9
|
-
ADMIN
|
|
10
|
-
USER
|
|
11
|
-
}
|
|
12
|
-
interface Node {
|
|
13
|
-
id: ID!
|
|
14
|
-
}
|
|
15
|
-
type User implements Node {
|
|
16
|
-
id: ID!
|
|
17
|
-
name: String!
|
|
18
|
-
email: String
|
|
19
|
-
role: Role!
|
|
20
|
-
friends: [User!]!
|
|
21
|
-
createdAt: DateTime
|
|
22
|
-
}
|
|
23
|
-
input UserFilter {
|
|
24
|
-
role: Role
|
|
25
|
-
from: String
|
|
26
|
-
}
|
|
27
|
-
type Query {
|
|
28
|
-
users(filter: UserFilter): [User!]!
|
|
29
|
-
}
|
|
30
|
-
`;
|
|
31
|
-
|
|
32
|
-
function run(schemaSDL: string, config: PythonPluginConfig = {}) {
|
|
33
|
-
const schema = buildSchema(schemaSDL);
|
|
34
|
-
const result = plugin(schema, [], config) as { prepend: string[]; content: string };
|
|
35
|
-
return result;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function full(schemaSDL: string, config: PythonPluginConfig = {}): string {
|
|
39
|
-
const { prepend, content } = run(schemaSDL, config);
|
|
40
|
-
return `${prepend.join('\n')}\n\n\n${content}\n`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe('@graphql-codegen/python', () => {
|
|
44
|
-
it('maps built-in scalars to native Python types and strips Optional on non-null', () => {
|
|
45
|
-
const { content } = run(`
|
|
46
|
-
type Q { a: ID! b: String! c: Int! d: Float! e: Boolean! }
|
|
47
|
-
`);
|
|
48
|
-
expect(content).toContain(`a: 'str'`);
|
|
49
|
-
expect(content).toContain(`b: 'str'`);
|
|
50
|
-
expect(content).toContain(`c: 'int'`);
|
|
51
|
-
expect(content).toContain(`d: 'float'`);
|
|
52
|
-
expect(content).toContain(`e: 'bool'`);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('renders enums as (str, Enum) classes', () => {
|
|
56
|
-
const { content, prepend } = run(`enum Role { ADMIN USER } type Q { r: Role! }`);
|
|
57
|
-
expect(content).toContain('class Role(str, Enum):');
|
|
58
|
-
expect(content).toContain(` ADMIN = 'ADMIN'`);
|
|
59
|
-
expect(content).toContain(` USER = 'USER'`);
|
|
60
|
-
expect(prepend).toContain('from enum import Enum');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('renders object types as BaseModel classes', () => {
|
|
64
|
-
const { content, prepend } = run(`type User { id: ID! name: String! }`);
|
|
65
|
-
expect(content).toContain('class User(BaseModel):');
|
|
66
|
-
expect(content).toContain(` id: 'str'`);
|
|
67
|
-
expect(content).toContain(` name: 'str'`);
|
|
68
|
-
expect(prepend).toContain('from pydantic import BaseModel');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('renders interfaces as BaseModel classes', () => {
|
|
72
|
-
const { content } = run(`interface Node { id: ID! }`);
|
|
73
|
-
expect(content).toContain('class Node(BaseModel):');
|
|
74
|
-
expect(content).toContain(` id: 'str'`);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('renders input types as TypedDict', () => {
|
|
78
|
-
const { content, prepend } = run(`input UserFilter { role: String }`);
|
|
79
|
-
expect(content).toContain('class UserFilter(TypedDict):');
|
|
80
|
-
expect(prepend).toContain('from typing import Optional, TypedDict');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('maps custom scalars to Any', () => {
|
|
84
|
-
const { content, prepend } = run(`scalar DateTime type Q { at: DateTime }`);
|
|
85
|
-
expect(content).toContain(`at: 'Optional[Any]'`);
|
|
86
|
-
// no standalone class for the scalar
|
|
87
|
-
expect(content).not.toContain('class DateTime');
|
|
88
|
-
expect(prepend.some(l => l.includes('Any'))).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('honors config.scalars overrides for custom scalars', () => {
|
|
92
|
-
const { content } = run(`scalar DateTime type Q { at: DateTime! }`, {
|
|
93
|
-
scalars: { DateTime: 'datetime' },
|
|
94
|
-
});
|
|
95
|
-
expect(content).toContain(`at: 'datetime'`);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('converts object field names to snake_case by default (namingConvention: snakeCase)', () => {
|
|
99
|
-
const { content } = run(`type User { createdAt: String firstName: String! }`);
|
|
100
|
-
expect(content).toContain(`created_at: 'Optional[str]'`);
|
|
101
|
-
expect(content).toContain(`first_name: 'str'`);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('emits a functional TypedDict when an input key is a reserved keyword (inputs keep wire names)', () => {
|
|
105
|
-
const { content, prepend } = run(`input F { from: String role: String }`);
|
|
106
|
-
// `from` is a Python keyword -> whole input type uses functional syntax.
|
|
107
|
-
expect(content).toContain(`F = TypedDict('F', {`);
|
|
108
|
-
expect(content).toContain(` 'from': 'Optional[str]',`);
|
|
109
|
-
expect(content).toContain(` 'role': 'Optional[str]',`);
|
|
110
|
-
expect(content).not.toContain(`class F(TypedDict):`);
|
|
111
|
-
expect(prepend).toContain('from typing import Optional, TypedDict');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('keeps input keys as wire names (never snake-cased), class syntax when valid', () => {
|
|
115
|
-
const { content } = run(`input UserFilter { role: String createdAt: String }`);
|
|
116
|
-
expect(content).toContain(`class UserFilter(TypedDict):`);
|
|
117
|
-
expect(content).toContain(` role: 'Optional[str]'`);
|
|
118
|
-
// Input keys keep the WIRE name (the runtime sends the dict as-is).
|
|
119
|
-
expect(content).toContain(` createdAt: 'Optional[str]'`);
|
|
120
|
-
expect(content).not.toContain(`TypedDict('UserFilter'`);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('input keys keep wire names even under namingConvention: snakeCase', () => {
|
|
124
|
-
const { content } = run(`input F { from: String createdAt: String }`, {
|
|
125
|
-
namingConvention: 'snakeCase',
|
|
126
|
-
});
|
|
127
|
-
expect(content).toContain(`F = TypedDict('F', {`);
|
|
128
|
-
expect(content).toContain(` 'from': 'Optional[str]',`);
|
|
129
|
-
expect(content).toContain(` 'createdAt': 'Optional[str]',`);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('escapes keyword fields on object types and pins the wire key with Field(alias=...)', () => {
|
|
133
|
-
// Object types extend a pydantic BaseModel; a keyword field is escaped with
|
|
134
|
-
// `_` and `Field(alias=...)` pins the original wire key so pydantic hydrates
|
|
135
|
-
// it onto the escaped attribute. `model_config` enables population by name.
|
|
136
|
-
const { content, prepend } = run(
|
|
137
|
-
`type Message { from: String! createdAt: String! class: Int }`,
|
|
138
|
-
);
|
|
139
|
-
expect(content).toContain(`class Message(BaseModel):`);
|
|
140
|
-
expect(content).toContain(` model_config = ConfigDict(populate_by_name=True)`);
|
|
141
|
-
expect(content).toContain(` from_: 'str' = Field(alias='from')`);
|
|
142
|
-
expect(content).toContain(` created_at: 'str' = Field(alias='createdAt')`);
|
|
143
|
-
expect(content).toContain(` class_: 'Optional[int]' = Field(default=None, alias='class')`);
|
|
144
|
-
expect(content).not.toContain(`def __init__`);
|
|
145
|
-
expect(prepend).toContain('from pydantic import BaseModel, ConfigDict, Field');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('namingConvention: keep leaves object field names verbatim (keyword -> _ + alias)', () => {
|
|
149
|
-
const { content } = run(`type User { createdAt: String firstName: String! from: String }`, {
|
|
150
|
-
namingConvention: 'keep',
|
|
151
|
-
});
|
|
152
|
-
expect(content).toContain(`createdAt: 'Optional[str]' = None`);
|
|
153
|
-
expect(content).toContain(`firstName: 'str'`);
|
|
154
|
-
expect(content).toContain(`from_: 'Optional[str]' = Field(default=None, alias='from')`);
|
|
155
|
-
expect(content).not.toContain(`def __init__`);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('composes nullability and lists correctly', () => {
|
|
159
|
-
const { content } = run(`
|
|
160
|
-
type User { id: ID! }
|
|
161
|
-
type Q {
|
|
162
|
-
nn: String!
|
|
163
|
-
opt: String
|
|
164
|
-
reqList: [User!]!
|
|
165
|
-
nullListNullItem: [User]
|
|
166
|
-
}
|
|
167
|
-
`);
|
|
168
|
-
expect(content).toContain(`nn: 'str'`);
|
|
169
|
-
expect(content).toContain(`opt: 'Optional[str]'`);
|
|
170
|
-
expect(content).toContain(`req_list: 'list[User]'`);
|
|
171
|
-
expect(content).toContain(`null_list_null_item: 'Optional[list[Optional[User]]]'`);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('verifies onlyOperationTypes emits enums + inputs + scalars and skips object types', () => {
|
|
175
|
-
const { content } = run(
|
|
176
|
-
`scalar DateTime
|
|
177
|
-
enum Role { ADMIN USER }
|
|
178
|
-
input UserFilter { from: String role: Role }
|
|
179
|
-
type Message { from: String! createdAt: DateTime! }
|
|
180
|
-
type Query { m: Message }`,
|
|
181
|
-
{ onlyOperationTypes: true },
|
|
182
|
-
);
|
|
183
|
-
// enums + input types present...
|
|
184
|
-
expect(content).toContain(`class Role(str, Enum):`);
|
|
185
|
-
expect(content).toContain(`UserFilter = TypedDict('UserFilter', {`);
|
|
186
|
-
// ...object types skipped.
|
|
187
|
-
expect(content).not.toContain(`class Message`);
|
|
188
|
-
expect(content).not.toContain(`class Query`);
|
|
189
|
-
expect(content).not.toContain('BaseModel');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('supports recursive types via quoted forward references', () => {
|
|
193
|
-
const { content } = run(`type User { id: ID! friends: [User!]! }`);
|
|
194
|
-
expect(content).toContain('class User(BaseModel):');
|
|
195
|
-
expect(content).toContain(`friends: 'list[User]'`);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('honors baseModel and runtimeImport config', () => {
|
|
199
|
-
const { content, prepend } = run(`type User { id: ID! }`, {
|
|
200
|
-
baseModel: 'PydModel',
|
|
201
|
-
runtimeImport: 'myapp.models',
|
|
202
|
-
});
|
|
203
|
-
expect(content).toContain('class User(PydModel):');
|
|
204
|
-
expect(prepend).toContain('from myapp.models import PydModel');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('only emits imports that are actually used', () => {
|
|
208
|
-
// No enum, no input, no Any -> only Optional + BaseModel
|
|
209
|
-
const { prepend } = run(`type User { name: String }`);
|
|
210
|
-
expect(prepend).not.toContain('from enum import Enum');
|
|
211
|
-
expect(prepend).toContain('from typing import Optional');
|
|
212
|
-
expect(prepend).toContain('from pydantic import BaseModel');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('does not emit introspection types', () => {
|
|
216
|
-
const { content } = run(FULL_SCHEMA);
|
|
217
|
-
expect(content).not.toContain('__Schema');
|
|
218
|
-
expect(content).not.toContain('__Type');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe('unions', () => {
|
|
222
|
-
const UNION_SCHEMA = /* GraphQL */ `
|
|
223
|
-
type A {
|
|
224
|
-
a: String
|
|
225
|
-
}
|
|
226
|
-
type B {
|
|
227
|
-
b: Int
|
|
228
|
-
}
|
|
229
|
-
union AB = A | B
|
|
230
|
-
type Query {
|
|
231
|
-
thing: AB
|
|
232
|
-
}
|
|
233
|
-
`;
|
|
234
|
-
|
|
235
|
-
it('emits a typing.Union alias with quoted members', () => {
|
|
236
|
-
const { content, prepend } = run(UNION_SCHEMA);
|
|
237
|
-
expect(content).toContain(`Ab = Union['A', 'B']`);
|
|
238
|
-
expect(prepend.some(l => /from typing import .*\bUnion\b/.test(l))).toBe(true);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('never emits TypeScript for a union', () => {
|
|
242
|
-
// The inherited BaseTypesVisitor implementation emits `export type Ab = A | B;`
|
|
243
|
-
// plus a `/** ... */` comment — neither is valid Python.
|
|
244
|
-
const { content } = run(UNION_SCHEMA);
|
|
245
|
-
expect(content).not.toContain('export type');
|
|
246
|
-
expect(content).not.toContain('/**');
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('does not crash on a union without a description', () => {
|
|
250
|
-
// The inherited implementation dereferences the description unguarded.
|
|
251
|
-
expect(() => run(UNION_SCHEMA)).not.toThrow();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('handles a documented union the same way', () => {
|
|
255
|
-
const { content } = run(/* GraphQL */ `
|
|
256
|
-
type A {
|
|
257
|
-
a: String
|
|
258
|
-
}
|
|
259
|
-
type B {
|
|
260
|
-
b: Int
|
|
261
|
-
}
|
|
262
|
-
"Documented."
|
|
263
|
-
union AB = A | B
|
|
264
|
-
type Query {
|
|
265
|
-
thing: AB
|
|
266
|
-
}
|
|
267
|
-
`);
|
|
268
|
-
expect(content).toContain(`Ab = Union['A', 'B']`);
|
|
269
|
-
expect(content).not.toContain('export type');
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('is skipped under onlyOperationTypes / onlyEnums', () => {
|
|
273
|
-
expect(run(UNION_SCHEMA, { onlyOperationTypes: true }).content).not.toContain('Ab = Union');
|
|
274
|
-
expect(run(UNION_SCHEMA, { onlyEnums: true }).content).not.toContain('Ab = Union');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('references the union from a field that selects it', () => {
|
|
278
|
-
const { content } = run(UNION_SCHEMA);
|
|
279
|
-
expect(content).toContain(`thing: 'Optional[Ab]'`);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('matches the full schema snapshot', () => {
|
|
284
|
-
expect(full(FULL_SCHEMA)).toMatchSnapshot();
|
|
285
|
-
});
|
|
286
|
-
});
|
package/vitest.config.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { defineProject, mergeConfig } from 'vitest/config';
|
|
2
|
-
import { sharedConfig } from '../../../../vitest.config.js';
|
|
3
|
-
|
|
4
|
-
export default mergeConfig(
|
|
5
|
-
sharedConfig,
|
|
6
|
-
defineProject({
|
|
7
|
-
test: {
|
|
8
|
-
name: 'python',
|
|
9
|
-
include: ['**/*.spec.ts'],
|
|
10
|
-
},
|
|
11
|
-
}),
|
|
12
|
-
);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|