@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 CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # `@graphql-codegen/python-common`
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
+ 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,3 @@
1
+ export * from './keywords.js';
2
+ export * from './member-naming.js';
3
+ export * from './scalars.js';
@@ -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,3 @@
1
+ export * from './keywords.cjs';
2
+ export * from './member-naming.cjs';
3
+ export * from './scalars.cjs';
@@ -0,0 +1,3 @@
1
+ export * from './keywords.js';
2
+ export * from './member-naming.js';
3
+ export * from './scalars.js';
@@ -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 };
@@ -0,0 +1,6 @@
1
+ import type { NormalizedScalarsMap } from '@graphql-codegen/visitor-plugin-common';
2
+ /**
3
+ * Default GraphQL scalar -> Python type mapping. Custom scalars fall back to
4
+ * `Any`.
5
+ */
6
+ export declare const PYTHON_SCALARS: NormalizedScalarsMap;
@@ -0,0 +1,6 @@
1
+ import type { NormalizedScalarsMap } from '@graphql-codegen/visitor-plugin-common';
2
+ /**
3
+ * Default GraphQL scalar -> Python type mapping. Custom scalars fall back to
4
+ * `Any`.
5
+ */
6
+ export declare const PYTHON_SCALARS: NormalizedScalarsMap;
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@graphql-codegen/python-common",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -0,0 +1,3 @@
1
+ export * from './keywords.js';
2
+ export * from './member-naming.js';
3
+ export * from './scalars.js';
@@ -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
+ });
@@ -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
+ );