@graphql-codegen/python-common 0.0.0-stage → 0.0.1
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 +27 -2
- package/dist/README.md +28 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/keywords.js +58 -0
- package/dist/cjs/member-naming.js +41 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/scalars.js +14 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/keywords.js +54 -0
- package/dist/esm/member-naming.js +44 -0
- package/dist/esm/scalars.js +11 -0
- package/dist/package.json +40 -0
- package/dist/typings/index.d.cts +3 -0
- package/dist/typings/index.d.ts +3 -0
- package/dist/typings/keywords.d.cts +14 -0
- package/dist/typings/keywords.d.ts +14 -0
- package/dist/typings/member-naming.d.cts +38 -0
- package/dist/typings/member-naming.d.ts +38 -0
- package/dist/typings/scalars.d.cts +6 -0
- package/dist/typings/scalars.d.ts +6 -0
- package/package.json +48 -4
- package/src/index.ts +3 -0
- package/src/keywords.ts +56 -0
- package/src/member-naming.ts +48 -0
- package/src/scalars.ts +13 -0
- package/tests/python-common.spec.ts +113 -0
- package/vitest.config.ts +12 -0
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@graphql-codegen/python-common`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared Python-specific helpers for the `@graphql-codegen/python` plugin family (`python`,
|
|
4
|
+
`python-operations`): the naming knowledge those plugins must agree on so their generated classes
|
|
5
|
+
line up exactly, kept in one place.
|
|
6
|
+
|
|
7
|
+
Modeled on `@graphql-codegen/c-sharp-common` / `@graphql-codegen/java-common`, which play the same
|
|
8
|
+
role for their respective language families.
|
|
9
|
+
|
|
10
|
+
## What's here
|
|
11
|
+
|
|
12
|
+
- **`PYTHON_KEYWORDS`** / **`PY_IDENT_RE`** / **`isValidClassAttr`** — the CPython reserved-word set
|
|
13
|
+
(incl. the soft keywords `match`/`case`) and identifier validation used to decide when a GraphQL
|
|
14
|
+
name needs escaping.
|
|
15
|
+
- **`PYTHON_SCALARS`** — the default GraphQL scalar → Python type mapping (custom scalars fall back
|
|
16
|
+
to `Any`).
|
|
17
|
+
- **`pythonFieldName`** / **`isReservedField`** — convert a GraphQL name to `snake_case` for use as
|
|
18
|
+
a Python identifier, escaping reserved keywords with a trailing `_`.
|
|
19
|
+
- **`pascalCase`** / **`constantCase`** — the class-name and document-constant casing helpers,
|
|
20
|
+
re-exported from `change-case-all`.
|
|
21
|
+
|
|
22
|
+
This package is the **single place in the family that depends on `change-case-all`**:
|
|
23
|
+
`pythonFieldName`, `pascalCase`, and `constantCase` all route through it, so every plugin gets the
|
|
24
|
+
same acronym-splitting behavior (`GetHTTPStatus` → `GetHttpStatus` / `get_http_status`) and no
|
|
25
|
+
consumer imports `change-case-all` or hand-rolls the rules itself. `python` and `python-operations`
|
|
26
|
+
get PascalCase names from `BaseVisitor.convertName` (which uses `change-case-all`); a plugin that
|
|
27
|
+
doesn't extend the base visitor can use the `pascalCase` / `constantCase` exported here to produce
|
|
28
|
+
byte-identical names.
|
package/dist/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# `@graphql-codegen/python-common`
|
|
2
|
+
|
|
3
|
+
Shared Python-specific helpers for the `@graphql-codegen/python` plugin family (`python`,
|
|
4
|
+
`python-operations`): the naming knowledge those plugins must agree on so their generated classes
|
|
5
|
+
line up exactly, kept in one place.
|
|
6
|
+
|
|
7
|
+
Modeled on `@graphql-codegen/c-sharp-common` / `@graphql-codegen/java-common`, which play the same
|
|
8
|
+
role for their respective language families.
|
|
9
|
+
|
|
10
|
+
## What's here
|
|
11
|
+
|
|
12
|
+
- **`PYTHON_KEYWORDS`** / **`PY_IDENT_RE`** / **`isValidClassAttr`** — the CPython reserved-word set
|
|
13
|
+
(incl. the soft keywords `match`/`case`) and identifier validation used to decide when a GraphQL
|
|
14
|
+
name needs escaping.
|
|
15
|
+
- **`PYTHON_SCALARS`** — the default GraphQL scalar → Python type mapping (custom scalars fall back
|
|
16
|
+
to `Any`).
|
|
17
|
+
- **`pythonFieldName`** / **`isReservedField`** — convert a GraphQL name to `snake_case` for use as
|
|
18
|
+
a Python identifier, escaping reserved keywords with a trailing `_`.
|
|
19
|
+
- **`pascalCase`** / **`constantCase`** — the class-name and document-constant casing helpers,
|
|
20
|
+
re-exported from `change-case-all`.
|
|
21
|
+
|
|
22
|
+
This package is the **single place in the family that depends on `change-case-all`**:
|
|
23
|
+
`pythonFieldName`, `pascalCase`, and `constantCase` all route through it, so every plugin gets the
|
|
24
|
+
same acronym-splitting behavior (`GetHTTPStatus` → `GetHttpStatus` / `get_http_status`) and no
|
|
25
|
+
consumer imports `change-case-all` or hand-rolls the rules itself. `python` and `python-operations`
|
|
26
|
+
get PascalCase names from `BaseVisitor.convertName` (which uses `change-case-all`); a plugin that
|
|
27
|
+
doesn't extend the base visitor can use the `pascalCase` / `constantCase` exported here to produce
|
|
28
|
+
byte-identical names.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./keywords.js"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./member-naming.js"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./scalars.js"), exports);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PY_IDENT_RE = exports.PYTHON_KEYWORDS = void 0;
|
|
4
|
+
exports.isValidClassAttr = isValidClassAttr;
|
|
5
|
+
/**
|
|
6
|
+
* Python reserved keywords (from the CPython `keyword` module, incl. the soft
|
|
7
|
+
* keywords `match`/`case`). Names matching one of these get a trailing `_`
|
|
8
|
+
* wherever they'd otherwise be used as a Python identifier.
|
|
9
|
+
*/
|
|
10
|
+
exports.PYTHON_KEYWORDS = new Set([
|
|
11
|
+
'False',
|
|
12
|
+
'None',
|
|
13
|
+
'True',
|
|
14
|
+
'and',
|
|
15
|
+
'as',
|
|
16
|
+
'assert',
|
|
17
|
+
'async',
|
|
18
|
+
'await',
|
|
19
|
+
'break',
|
|
20
|
+
'class',
|
|
21
|
+
'continue',
|
|
22
|
+
'def',
|
|
23
|
+
'del',
|
|
24
|
+
'elif',
|
|
25
|
+
'else',
|
|
26
|
+
'except',
|
|
27
|
+
'finally',
|
|
28
|
+
'for',
|
|
29
|
+
'from',
|
|
30
|
+
'global',
|
|
31
|
+
'if',
|
|
32
|
+
'import',
|
|
33
|
+
'in',
|
|
34
|
+
'is',
|
|
35
|
+
'lambda',
|
|
36
|
+
'nonlocal',
|
|
37
|
+
'not',
|
|
38
|
+
'or',
|
|
39
|
+
'pass',
|
|
40
|
+
'raise',
|
|
41
|
+
'return',
|
|
42
|
+
'try',
|
|
43
|
+
'while',
|
|
44
|
+
'with',
|
|
45
|
+
'yield',
|
|
46
|
+
'match',
|
|
47
|
+
'case',
|
|
48
|
+
]);
|
|
49
|
+
/** A valid Python identifier (matches the GraphQL Name grammar plus keywords). */
|
|
50
|
+
exports.PY_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
51
|
+
/**
|
|
52
|
+
* Whether `name` can be used as a plain Python class/dict attribute: it must
|
|
53
|
+
* be a valid identifier AND not a reserved keyword. When it cannot, the
|
|
54
|
+
* containing `TypedDict` must be emitted with functional syntax.
|
|
55
|
+
*/
|
|
56
|
+
function isValidClassAttr(name) {
|
|
57
|
+
return exports.PY_IDENT_RE.test(name) && !exports.PYTHON_KEYWORDS.has(name);
|
|
58
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pascalCase = exports.constantCase = void 0;
|
|
4
|
+
exports.pythonFieldName = pythonFieldName;
|
|
5
|
+
exports.isReservedField = isReservedField;
|
|
6
|
+
const change_case_all_1 = require("change-case-all");
|
|
7
|
+
Object.defineProperty(exports, "constantCase", { enumerable: true, get: function () { return change_case_all_1.constantCase; } });
|
|
8
|
+
Object.defineProperty(exports, "pascalCase", { enumerable: true, get: function () { return change_case_all_1.pascalCase; } });
|
|
9
|
+
const keywords_js_1 = require("./keywords.js");
|
|
10
|
+
/**
|
|
11
|
+
* This module is the single place in the Python plugin family that imports
|
|
12
|
+
* `change-case-all`. The plugins route their snake / pascal / constant casing
|
|
13
|
+
* through these exports so the generated names line up exactly and no consumer
|
|
14
|
+
* hand-rolls (or diverges on) the acronym-splitting rules.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* snake_case a GraphQL name for use as a Python identifier (field, method, or
|
|
18
|
+
* document-constant base name), appending a trailing `_` when the result
|
|
19
|
+
* collides with a reserved keyword.
|
|
20
|
+
*
|
|
21
|
+
* Uses change-case-all's `snakeCase` — the same implementation
|
|
22
|
+
* `@graphql-codegen/python`'s `pascalCase` (via `BaseVisitor.convertName`)
|
|
23
|
+
* already relies on — so acronym runs split identically on both sides, e.g.
|
|
24
|
+
* `httpURL` -> `http_url` lines up with `HttpUrl`. A hand-rolled
|
|
25
|
+
* implementation previously diverged for `pascalCase` on `GetHTTPStatus`.
|
|
26
|
+
*
|
|
27
|
+
* Each consumer previously duplicated this exact function under a locally
|
|
28
|
+
* meaningful name (`methodName`, `pyFieldName`, `snakeFieldName`); import and
|
|
29
|
+
* alias it the same way here instead of re-implementing it.
|
|
30
|
+
*/
|
|
31
|
+
function pythonFieldName(name) {
|
|
32
|
+
const snake = (0, change_case_all_1.snakeCase)(name);
|
|
33
|
+
return keywords_js_1.PYTHON_KEYWORDS.has(snake) ? `${snake}_` : snake;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether a GraphQL response key maps to a Python reserved keyword once
|
|
37
|
+
* snake-cased (before any trailing-`_` escaping is applied).
|
|
38
|
+
*/
|
|
39
|
+
function isReservedField(name) {
|
|
40
|
+
return keywords_js_1.PYTHON_KEYWORDS.has((0, change_case_all_1.snakeCase)(name));
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PYTHON_SCALARS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Default GraphQL scalar -> Python type mapping. Custom scalars fall back to
|
|
6
|
+
* `Any`.
|
|
7
|
+
*/
|
|
8
|
+
exports.PYTHON_SCALARS = {
|
|
9
|
+
ID: { input: 'str', output: 'str' },
|
|
10
|
+
String: { input: 'str', output: 'str' },
|
|
11
|
+
Int: { input: 'int', output: 'int' },
|
|
12
|
+
Float: { input: 'float', output: 'float' },
|
|
13
|
+
Boolean: { input: 'bool', output: 'bool' },
|
|
14
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python reserved keywords (from the CPython `keyword` module, incl. the soft
|
|
3
|
+
* keywords `match`/`case`). Names matching one of these get a trailing `_`
|
|
4
|
+
* wherever they'd otherwise be used as a Python identifier.
|
|
5
|
+
*/
|
|
6
|
+
export const PYTHON_KEYWORDS = new Set([
|
|
7
|
+
'False',
|
|
8
|
+
'None',
|
|
9
|
+
'True',
|
|
10
|
+
'and',
|
|
11
|
+
'as',
|
|
12
|
+
'assert',
|
|
13
|
+
'async',
|
|
14
|
+
'await',
|
|
15
|
+
'break',
|
|
16
|
+
'class',
|
|
17
|
+
'continue',
|
|
18
|
+
'def',
|
|
19
|
+
'del',
|
|
20
|
+
'elif',
|
|
21
|
+
'else',
|
|
22
|
+
'except',
|
|
23
|
+
'finally',
|
|
24
|
+
'for',
|
|
25
|
+
'from',
|
|
26
|
+
'global',
|
|
27
|
+
'if',
|
|
28
|
+
'import',
|
|
29
|
+
'in',
|
|
30
|
+
'is',
|
|
31
|
+
'lambda',
|
|
32
|
+
'nonlocal',
|
|
33
|
+
'not',
|
|
34
|
+
'or',
|
|
35
|
+
'pass',
|
|
36
|
+
'raise',
|
|
37
|
+
'return',
|
|
38
|
+
'try',
|
|
39
|
+
'while',
|
|
40
|
+
'with',
|
|
41
|
+
'yield',
|
|
42
|
+
'match',
|
|
43
|
+
'case',
|
|
44
|
+
]);
|
|
45
|
+
/** A valid Python identifier (matches the GraphQL Name grammar plus keywords). */
|
|
46
|
+
export const PY_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
47
|
+
/**
|
|
48
|
+
* Whether `name` can be used as a plain Python class/dict attribute: it must
|
|
49
|
+
* be a valid identifier AND not a reserved keyword. When it cannot, the
|
|
50
|
+
* containing `TypedDict` must be emitted with functional syntax.
|
|
51
|
+
*/
|
|
52
|
+
export function isValidClassAttr(name) {
|
|
53
|
+
return PY_IDENT_RE.test(name) && !PYTHON_KEYWORDS.has(name);
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { constantCase, pascalCase, snakeCase } from 'change-case-all';
|
|
2
|
+
import { PYTHON_KEYWORDS } from './keywords.js';
|
|
3
|
+
/**
|
|
4
|
+
* This module is the single place in the Python plugin family that imports
|
|
5
|
+
* `change-case-all`. The plugins route their snake / pascal / constant casing
|
|
6
|
+
* through these exports so the generated names line up exactly and no consumer
|
|
7
|
+
* hand-rolls (or diverges on) the acronym-splitting rules.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* snake_case a GraphQL name for use as a Python identifier (field, method, or
|
|
11
|
+
* document-constant base name), appending a trailing `_` when the result
|
|
12
|
+
* collides with a reserved keyword.
|
|
13
|
+
*
|
|
14
|
+
* Uses change-case-all's `snakeCase` — the same implementation
|
|
15
|
+
* `@graphql-codegen/python`'s `pascalCase` (via `BaseVisitor.convertName`)
|
|
16
|
+
* already relies on — so acronym runs split identically on both sides, e.g.
|
|
17
|
+
* `httpURL` -> `http_url` lines up with `HttpUrl`. A hand-rolled
|
|
18
|
+
* implementation previously diverged for `pascalCase` on `GetHTTPStatus`.
|
|
19
|
+
*
|
|
20
|
+
* Each consumer previously duplicated this exact function under a locally
|
|
21
|
+
* meaningful name (`methodName`, `pyFieldName`, `snakeFieldName`); import and
|
|
22
|
+
* alias it the same way here instead of re-implementing it.
|
|
23
|
+
*/
|
|
24
|
+
export function pythonFieldName(name) {
|
|
25
|
+
const snake = snakeCase(name);
|
|
26
|
+
return PYTHON_KEYWORDS.has(snake) ? `${snake}_` : snake;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether a GraphQL response key maps to a Python reserved keyword once
|
|
30
|
+
* snake-cased (before any trailing-`_` escaping is applied).
|
|
31
|
+
*/
|
|
32
|
+
export function isReservedField(name) {
|
|
33
|
+
return PYTHON_KEYWORDS.has(snakeCase(name));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* PascalCase a name for a generated Python class / result-type name.
|
|
37
|
+
*
|
|
38
|
+
* This is the exact `change-case-all` `pascalCase` that
|
|
39
|
+
* `@graphql-codegen/python` and `@graphql-codegen/python-operations` use via
|
|
40
|
+
* `BaseVisitor.convertName`, re-exported so a plugin that does NOT extend the
|
|
41
|
+
* base visitor produces byte-identical class names, including acronym runs,
|
|
42
|
+
* e.g. `GetHTTPStatus` -> `GetHttpStatus`.
|
|
43
|
+
*/
|
|
44
|
+
export { constantCase, pascalCase };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default GraphQL scalar -> Python type mapping. Custom scalars fall back to
|
|
3
|
+
* `Any`.
|
|
4
|
+
*/
|
|
5
|
+
export const PYTHON_SCALARS = {
|
|
6
|
+
ID: { input: 'str', output: 'str' },
|
|
7
|
+
String: { input: 'str', output: 'str' },
|
|
8
|
+
Int: { input: 'int', output: 'int' },
|
|
9
|
+
Float: { input: 'float', output: 'float' },
|
|
10
|
+
Boolean: { input: 'bool', output: 'bool' },
|
|
11
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphql-codegen/python-common",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Shared Python-specific helpers (reserved keywords, scalar mapping, identifier naming) for @graphql-codegen/python and @graphql-codegen/python-operations.",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@graphql-codegen/visitor-plugin-common": "^6.3.0",
|
|
8
|
+
"change-case-all": "1.0.15",
|
|
9
|
+
"tslib": "^2.8.1"
|
|
10
|
+
},
|
|
11
|
+
"repository": "git@github.com:dotansimha/graphql-code-generator-community.git",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">= 16.0.0"
|
|
15
|
+
},
|
|
16
|
+
"main": "cjs/index.js",
|
|
17
|
+
"module": "esm/index.js",
|
|
18
|
+
"typings": "typings/index.d.ts",
|
|
19
|
+
"typescript": {
|
|
20
|
+
"definition": "typings/index.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./typings/index.d.cts",
|
|
27
|
+
"default": "./cjs/index.js"
|
|
28
|
+
},
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./typings/index.d.ts",
|
|
31
|
+
"default": "./esm/index.js"
|
|
32
|
+
},
|
|
33
|
+
"default": {
|
|
34
|
+
"types": "./typings/index.d.ts",
|
|
35
|
+
"default": "./esm/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python reserved keywords (from the CPython `keyword` module, incl. the soft
|
|
3
|
+
* keywords `match`/`case`). Names matching one of these get a trailing `_`
|
|
4
|
+
* wherever they'd otherwise be used as a Python identifier.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PYTHON_KEYWORDS: Set<string>;
|
|
7
|
+
/** A valid Python identifier (matches the GraphQL Name grammar plus keywords). */
|
|
8
|
+
export declare const PY_IDENT_RE: RegExp;
|
|
9
|
+
/**
|
|
10
|
+
* Whether `name` can be used as a plain Python class/dict attribute: it must
|
|
11
|
+
* be a valid identifier AND not a reserved keyword. When it cannot, the
|
|
12
|
+
* containing `TypedDict` must be emitted with functional syntax.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isValidClassAttr(name: string): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python reserved keywords (from the CPython `keyword` module, incl. the soft
|
|
3
|
+
* keywords `match`/`case`). Names matching one of these get a trailing `_`
|
|
4
|
+
* wherever they'd otherwise be used as a Python identifier.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PYTHON_KEYWORDS: Set<string>;
|
|
7
|
+
/** A valid Python identifier (matches the GraphQL Name grammar plus keywords). */
|
|
8
|
+
export declare const PY_IDENT_RE: RegExp;
|
|
9
|
+
/**
|
|
10
|
+
* Whether `name` can be used as a plain Python class/dict attribute: it must
|
|
11
|
+
* be a valid identifier AND not a reserved keyword. When it cannot, the
|
|
12
|
+
* containing `TypedDict` must be emitted with functional syntax.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isValidClassAttr(name: string): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { constantCase, pascalCase } from 'change-case-all';
|
|
2
|
+
/**
|
|
3
|
+
* This module is the single place in the Python plugin family that imports
|
|
4
|
+
* `change-case-all`. The plugins route their snake / pascal / constant casing
|
|
5
|
+
* through these exports so the generated names line up exactly and no consumer
|
|
6
|
+
* hand-rolls (or diverges on) the acronym-splitting rules.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* snake_case a GraphQL name for use as a Python identifier (field, method, or
|
|
10
|
+
* document-constant base name), appending a trailing `_` when the result
|
|
11
|
+
* collides with a reserved keyword.
|
|
12
|
+
*
|
|
13
|
+
* Uses change-case-all's `snakeCase` — the same implementation
|
|
14
|
+
* `@graphql-codegen/python`'s `pascalCase` (via `BaseVisitor.convertName`)
|
|
15
|
+
* already relies on — so acronym runs split identically on both sides, e.g.
|
|
16
|
+
* `httpURL` -> `http_url` lines up with `HttpUrl`. A hand-rolled
|
|
17
|
+
* implementation previously diverged for `pascalCase` on `GetHTTPStatus`.
|
|
18
|
+
*
|
|
19
|
+
* Each consumer previously duplicated this exact function under a locally
|
|
20
|
+
* meaningful name (`methodName`, `pyFieldName`, `snakeFieldName`); import and
|
|
21
|
+
* alias it the same way here instead of re-implementing it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function pythonFieldName(name: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a GraphQL response key maps to a Python reserved keyword once
|
|
26
|
+
* snake-cased (before any trailing-`_` escaping is applied).
|
|
27
|
+
*/
|
|
28
|
+
export declare function isReservedField(name: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* PascalCase a name for a generated Python class / result-type name.
|
|
31
|
+
*
|
|
32
|
+
* This is the exact `change-case-all` `pascalCase` that
|
|
33
|
+
* `@graphql-codegen/python` and `@graphql-codegen/python-operations` use via
|
|
34
|
+
* `BaseVisitor.convertName`, re-exported so a plugin that does NOT extend the
|
|
35
|
+
* base visitor produces byte-identical class names, including acronym runs,
|
|
36
|
+
* e.g. `GetHTTPStatus` -> `GetHttpStatus`.
|
|
37
|
+
*/
|
|
38
|
+
export { constantCase, pascalCase };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { constantCase, pascalCase } from 'change-case-all';
|
|
2
|
+
/**
|
|
3
|
+
* This module is the single place in the Python plugin family that imports
|
|
4
|
+
* `change-case-all`. The plugins route their snake / pascal / constant casing
|
|
5
|
+
* through these exports so the generated names line up exactly and no consumer
|
|
6
|
+
* hand-rolls (or diverges on) the acronym-splitting rules.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* snake_case a GraphQL name for use as a Python identifier (field, method, or
|
|
10
|
+
* document-constant base name), appending a trailing `_` when the result
|
|
11
|
+
* collides with a reserved keyword.
|
|
12
|
+
*
|
|
13
|
+
* Uses change-case-all's `snakeCase` — the same implementation
|
|
14
|
+
* `@graphql-codegen/python`'s `pascalCase` (via `BaseVisitor.convertName`)
|
|
15
|
+
* already relies on — so acronym runs split identically on both sides, e.g.
|
|
16
|
+
* `httpURL` -> `http_url` lines up with `HttpUrl`. A hand-rolled
|
|
17
|
+
* implementation previously diverged for `pascalCase` on `GetHTTPStatus`.
|
|
18
|
+
*
|
|
19
|
+
* Each consumer previously duplicated this exact function under a locally
|
|
20
|
+
* meaningful name (`methodName`, `pyFieldName`, `snakeFieldName`); import and
|
|
21
|
+
* alias it the same way here instead of re-implementing it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function pythonFieldName(name: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a GraphQL response key maps to a Python reserved keyword once
|
|
26
|
+
* snake-cased (before any trailing-`_` escaping is applied).
|
|
27
|
+
*/
|
|
28
|
+
export declare function isReservedField(name: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* PascalCase a name for a generated Python class / result-type name.
|
|
31
|
+
*
|
|
32
|
+
* This is the exact `change-case-all` `pascalCase` that
|
|
33
|
+
* `@graphql-codegen/python` and `@graphql-codegen/python-operations` use via
|
|
34
|
+
* `BaseVisitor.convertName`, re-exported so a plugin that does NOT extend the
|
|
35
|
+
* base visitor produces byte-identical class names, including acronym runs,
|
|
36
|
+
* e.g. `GetHTTPStatus` -> `GetHttpStatus`.
|
|
37
|
+
*/
|
|
38
|
+
export { constantCase, pascalCase };
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphql-codegen/python-common",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shared Python-specific helpers (reserved keywords, scalar mapping, identifier naming) for @graphql-codegen/python and @graphql-codegen/python-operations.",
|
|
6
|
+
"repository": "git@github.com:dotansimha/graphql-code-generator-community.git",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">= 16.0.0"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/cjs/index.js",
|
|
12
|
+
"module": "dist/esm/index.js",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/typings/index.d.cts",
|
|
17
|
+
"default": "./dist/cjs/index.js"
|
|
18
|
+
},
|
|
19
|
+
"import": {
|
|
20
|
+
"types": "./dist/typings/index.d.ts",
|
|
21
|
+
"default": "./dist/esm/index.js"
|
|
22
|
+
},
|
|
23
|
+
"default": {
|
|
24
|
+
"types": "./dist/typings/index.d.ts",
|
|
25
|
+
"default": "./dist/esm/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"typings": "dist/typings/index.d.ts",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"lint": "eslint **/*.ts"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@graphql-codegen/visitor-plugin-common": "^6.3.0",
|
|
36
|
+
"change-case-all": "1.0.15",
|
|
37
|
+
"tslib": "^2.8.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@graphql-codegen/testing": "4.0.5"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"directory": "dist",
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"typescript": {
|
|
48
|
+
"definition": "dist/typings/index.d.ts"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
package/src/keywords.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python reserved keywords (from the CPython `keyword` module, incl. the soft
|
|
3
|
+
* keywords `match`/`case`). Names matching one of these get a trailing `_`
|
|
4
|
+
* wherever they'd otherwise be used as a Python identifier.
|
|
5
|
+
*/
|
|
6
|
+
export const PYTHON_KEYWORDS = new Set<string>([
|
|
7
|
+
'False',
|
|
8
|
+
'None',
|
|
9
|
+
'True',
|
|
10
|
+
'and',
|
|
11
|
+
'as',
|
|
12
|
+
'assert',
|
|
13
|
+
'async',
|
|
14
|
+
'await',
|
|
15
|
+
'break',
|
|
16
|
+
'class',
|
|
17
|
+
'continue',
|
|
18
|
+
'def',
|
|
19
|
+
'del',
|
|
20
|
+
'elif',
|
|
21
|
+
'else',
|
|
22
|
+
'except',
|
|
23
|
+
'finally',
|
|
24
|
+
'for',
|
|
25
|
+
'from',
|
|
26
|
+
'global',
|
|
27
|
+
'if',
|
|
28
|
+
'import',
|
|
29
|
+
'in',
|
|
30
|
+
'is',
|
|
31
|
+
'lambda',
|
|
32
|
+
'nonlocal',
|
|
33
|
+
'not',
|
|
34
|
+
'or',
|
|
35
|
+
'pass',
|
|
36
|
+
'raise',
|
|
37
|
+
'return',
|
|
38
|
+
'try',
|
|
39
|
+
'while',
|
|
40
|
+
'with',
|
|
41
|
+
'yield',
|
|
42
|
+
'match',
|
|
43
|
+
'case',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** A valid Python identifier (matches the GraphQL Name grammar plus keywords). */
|
|
47
|
+
export const PY_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether `name` can be used as a plain Python class/dict attribute: it must
|
|
51
|
+
* be a valid identifier AND not a reserved keyword. When it cannot, the
|
|
52
|
+
* containing `TypedDict` must be emitted with functional syntax.
|
|
53
|
+
*/
|
|
54
|
+
export function isValidClassAttr(name: string): boolean {
|
|
55
|
+
return PY_IDENT_RE.test(name) && !PYTHON_KEYWORDS.has(name);
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { constantCase, pascalCase, snakeCase } from 'change-case-all';
|
|
2
|
+
import { PYTHON_KEYWORDS } from './keywords.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This module is the single place in the Python plugin family that imports
|
|
6
|
+
* `change-case-all`. The plugins route their snake / pascal / constant casing
|
|
7
|
+
* through these exports so the generated names line up exactly and no consumer
|
|
8
|
+
* hand-rolls (or diverges on) the acronym-splitting rules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* snake_case a GraphQL name for use as a Python identifier (field, method, or
|
|
13
|
+
* document-constant base name), appending a trailing `_` when the result
|
|
14
|
+
* collides with a reserved keyword.
|
|
15
|
+
*
|
|
16
|
+
* Uses change-case-all's `snakeCase` — the same implementation
|
|
17
|
+
* `@graphql-codegen/python`'s `pascalCase` (via `BaseVisitor.convertName`)
|
|
18
|
+
* already relies on — so acronym runs split identically on both sides, e.g.
|
|
19
|
+
* `httpURL` -> `http_url` lines up with `HttpUrl`. A hand-rolled
|
|
20
|
+
* implementation previously diverged for `pascalCase` on `GetHTTPStatus`.
|
|
21
|
+
*
|
|
22
|
+
* Each consumer previously duplicated this exact function under a locally
|
|
23
|
+
* meaningful name (`methodName`, `pyFieldName`, `snakeFieldName`); import and
|
|
24
|
+
* alias it the same way here instead of re-implementing it.
|
|
25
|
+
*/
|
|
26
|
+
export function pythonFieldName(name: string): string {
|
|
27
|
+
const snake = snakeCase(name);
|
|
28
|
+
return PYTHON_KEYWORDS.has(snake) ? `${snake}_` : snake;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a GraphQL response key maps to a Python reserved keyword once
|
|
33
|
+
* snake-cased (before any trailing-`_` escaping is applied).
|
|
34
|
+
*/
|
|
35
|
+
export function isReservedField(name: string): boolean {
|
|
36
|
+
return PYTHON_KEYWORDS.has(snakeCase(name));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* PascalCase a name for a generated Python class / result-type name.
|
|
41
|
+
*
|
|
42
|
+
* This is the exact `change-case-all` `pascalCase` that
|
|
43
|
+
* `@graphql-codegen/python` and `@graphql-codegen/python-operations` use via
|
|
44
|
+
* `BaseVisitor.convertName`, re-exported so a plugin that does NOT extend the
|
|
45
|
+
* base visitor produces byte-identical class names, including acronym runs,
|
|
46
|
+
* e.g. `GetHTTPStatus` -> `GetHttpStatus`.
|
|
47
|
+
*/
|
|
48
|
+
export { constantCase, pascalCase };
|
package/src/scalars.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NormalizedScalarsMap } from '@graphql-codegen/visitor-plugin-common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default GraphQL scalar -> Python type mapping. Custom scalars fall back to
|
|
5
|
+
* `Any`.
|
|
6
|
+
*/
|
|
7
|
+
export const PYTHON_SCALARS: NormalizedScalarsMap = {
|
|
8
|
+
ID: { input: 'str', output: 'str' },
|
|
9
|
+
String: { input: 'str', output: 'str' },
|
|
10
|
+
Int: { input: 'int', output: 'int' },
|
|
11
|
+
Float: { input: 'float', output: 'float' },
|
|
12
|
+
Boolean: { input: 'bool', output: 'bool' },
|
|
13
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
constantCase,
|
|
4
|
+
isReservedField,
|
|
5
|
+
isValidClassAttr,
|
|
6
|
+
pascalCase,
|
|
7
|
+
PY_IDENT_RE,
|
|
8
|
+
PYTHON_KEYWORDS,
|
|
9
|
+
PYTHON_SCALARS,
|
|
10
|
+
pythonFieldName,
|
|
11
|
+
} from '../src/index.js';
|
|
12
|
+
|
|
13
|
+
describe('@graphql-codegen/python-common', () => {
|
|
14
|
+
describe('PYTHON_KEYWORDS', () => {
|
|
15
|
+
it('contains hard keywords', () => {
|
|
16
|
+
expect(PYTHON_KEYWORDS.has('class')).toBe(true);
|
|
17
|
+
expect(PYTHON_KEYWORDS.has('from')).toBe(true);
|
|
18
|
+
expect(PYTHON_KEYWORDS.has('import')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('contains the soft keywords match/case', () => {
|
|
22
|
+
expect(PYTHON_KEYWORDS.has('match')).toBe(true);
|
|
23
|
+
expect(PYTHON_KEYWORDS.has('case')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('does not flag ordinary identifiers', () => {
|
|
27
|
+
expect(PYTHON_KEYWORDS.has('id')).toBe(false);
|
|
28
|
+
expect(PYTHON_KEYWORDS.has('name')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('PY_IDENT_RE / isValidClassAttr', () => {
|
|
33
|
+
it('accepts valid identifiers that are not keywords', () => {
|
|
34
|
+
expect(PY_IDENT_RE.test('user_id')).toBe(true);
|
|
35
|
+
expect(isValidClassAttr('user_id')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects reserved keywords even though they match the identifier regex', () => {
|
|
39
|
+
expect(PY_IDENT_RE.test('class')).toBe(true);
|
|
40
|
+
expect(isValidClassAttr('class')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('rejects non-identifier strings', () => {
|
|
44
|
+
expect(isValidClassAttr('2fast')).toBe(false);
|
|
45
|
+
expect(isValidClassAttr('my-field')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('PYTHON_SCALARS', () => {
|
|
50
|
+
it('maps built-in GraphQL scalars to native Python types', () => {
|
|
51
|
+
expect(PYTHON_SCALARS.ID).toEqual({ input: 'str', output: 'str' });
|
|
52
|
+
expect(PYTHON_SCALARS.Int).toEqual({ input: 'int', output: 'int' });
|
|
53
|
+
expect(PYTHON_SCALARS.Float).toEqual({ input: 'float', output: 'float' });
|
|
54
|
+
expect(PYTHON_SCALARS.Boolean).toEqual({ input: 'bool', output: 'bool' });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('pythonFieldName', () => {
|
|
59
|
+
it('converts camelCase to snake_case', () => {
|
|
60
|
+
expect(pythonFieldName('createdAt')).toBe('created_at');
|
|
61
|
+
expect(pythonFieldName('firstName')).toBe('first_name');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('splits acronym runs the same way change-case pascalCase does', () => {
|
|
65
|
+
expect(pythonFieldName('GetHTTPStatus')).toBe('get_http_status');
|
|
66
|
+
expect(pythonFieldName('httpURL')).toBe('http_url');
|
|
67
|
+
expect(pythonFieldName('ABTest')).toBe('ab_test');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('escapes reserved keywords with a trailing underscore', () => {
|
|
71
|
+
expect(pythonFieldName('from')).toBe('from_');
|
|
72
|
+
expect(pythonFieldName('class')).toBe('class_');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('leaves non-keyword snake_case names untouched', () => {
|
|
76
|
+
expect(pythonFieldName('user_id')).toBe('user_id');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('isReservedField', () => {
|
|
81
|
+
it('is true when the snake_cased name is a reserved keyword', () => {
|
|
82
|
+
expect(isReservedField('from')).toBe(true);
|
|
83
|
+
expect(isReservedField('Class')).toBe(true); // snake_cases to "class"
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('is false when the snake_cased name merely contains a keyword as a substring', () => {
|
|
87
|
+
expect(isReservedField('className')).toBe(false); // snake_cases to "class_name"
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('is false for ordinary field names', () => {
|
|
91
|
+
expect(isReservedField('createdAt')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('pascalCase', () => {
|
|
96
|
+
it('PascalCases names for class / result types', () => {
|
|
97
|
+
expect(pascalCase('getUsers')).toBe('GetUsers');
|
|
98
|
+
expect(pascalCase('get_users')).toBe('GetUsers');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('lowercases acronym runs so class names line up with BaseVisitor.convertName', () => {
|
|
102
|
+
expect(pascalCase('GetHTTPStatus')).toBe('GetHttpStatus');
|
|
103
|
+
expect(pascalCase('httpURL')).toBe('HttpUrl');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('constantCase', () => {
|
|
108
|
+
it('CONSTANT_CASEs names for document constants', () => {
|
|
109
|
+
expect(constantCase('getUsers')).toBe('GET_USERS');
|
|
110
|
+
expect(constantCase('GetHTTPStatus')).toBe('GET_HTTP_STATUS');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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-common',
|
|
9
|
+
include: ['**/*.spec.ts'],
|
|
10
|
+
},
|
|
11
|
+
}),
|
|
12
|
+
);
|