@typespec/http-client-python 0.35.0 → 0.36.0
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 +6 -0
- package/dist/emitter/code-model.d.ts +1 -1
- package/dist/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/emitter.d.ts +2 -2
- package/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +23 -2
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/external-process.d.ts +1 -1
- package/dist/emitter/external-process.d.ts.map +1 -1
- package/dist/emitter/external-process.js.map +1 -1
- package/dist/emitter/http.d.ts +2 -2
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +1 -1
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/index.d.ts +2 -1
- package/dist/emitter/index.d.ts.map +1 -1
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/lib.d.ts +3 -2
- package/dist/emitter/lib.d.ts.map +1 -1
- package/dist/emitter/lib.js +6 -1
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/node-runner.browser.d.ts +1 -1
- package/dist/emitter/node-runner.browser.d.ts.map +1 -1
- package/dist/emitter/node-runner.browser.js.map +1 -1
- package/dist/emitter/node-runner.d.ts +3 -3
- package/dist/emitter/node-runner.d.ts.map +1 -1
- package/dist/emitter/node-runner.js.map +1 -1
- package/dist/emitter/system-requirements.d.ts.map +1 -1
- package/dist/emitter/system-requirements.js.map +1 -1
- package/dist/emitter/types.d.ts +4 -4
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +15 -3
- package/dist/emitter/types.js.map +1 -1
- package/dist/emitter/utils.d.ts +2 -2
- package/dist/emitter/utils.d.ts.map +1 -1
- package/dist/emitter/utils.js +1 -1
- package/dist/emitter/utils.js.map +1 -1
- package/emitter/src/code-model.ts +4 -2
- package/emitter/src/emitter.ts +30 -6
- package/emitter/src/external-process.ts +2 -1
- package/emitter/src/http.ts +5 -5
- package/emitter/src/index.ts +2 -1
- package/emitter/src/lib.ts +11 -6
- package/emitter/src/node-runner.browser.ts +2 -1
- package/emitter/src/node-runner.ts +6 -3
- package/emitter/src/system-requirements.ts +2 -1
- package/emitter/src/types.ts +23 -8
- package/emitter/src/utils.ts +5 -4
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/browser-pyodide.test.ts +25 -0
- package/emitter/test/types.test.ts +72 -0
- package/eng/scripts/ci/dev_requirements.txt +1 -1
- package/eng/scripts/ci/regenerate-common.ts +29 -100
- package/eng/scripts/ci/regenerate.ts +3 -4
- package/eng/scripts/ci/run-tests.ts +2 -1
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/eng/scripts/setup/system-requirements.ts +2 -1
- package/generator/build/lib/pygen/__init__.py +24 -5
- package/generator/build/lib/pygen/codegen/models/__init__.py +3 -1
- package/generator/build/lib/pygen/codegen/models/code_model.py +7 -1
- package/generator/build/lib/pygen/codegen/models/enum_type.py +7 -7
- package/generator/build/lib/pygen/codegen/models/list_type.py +1 -0
- package/generator/build/lib/pygen/codegen/models/model_type.py +1 -3
- package/generator/build/lib/pygen/codegen/models/operation.py +7 -2
- package/generator/build/lib/pygen/codegen/models/parameter.py +3 -1
- package/generator/build/lib/pygen/codegen/models/utils.py +14 -0
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +6 -6
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +28 -14
- package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +13 -6
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +21 -1
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +9 -3
- package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -3
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +5 -12
- package/generator/build/lib/pygen/codegen/templates/validation.py.jinja2 +1 -1
- package/generator/build/lib/pygen/preprocess/__init__.py +68 -17
- package/generator/build/lib/pygen/utils.py +15 -0
- package/generator/component-detection-pip-report.json +7 -7
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/__init__.py +24 -5
- package/generator/pygen/codegen/models/__init__.py +3 -1
- package/generator/pygen/codegen/models/code_model.py +7 -1
- package/generator/pygen/codegen/models/enum_type.py +7 -7
- package/generator/pygen/codegen/models/list_type.py +1 -0
- package/generator/pygen/codegen/models/model_type.py +1 -3
- package/generator/pygen/codegen/models/operation.py +7 -2
- package/generator/pygen/codegen/models/parameter.py +3 -1
- package/generator/pygen/codegen/models/utils.py +14 -0
- package/generator/pygen/codegen/serializers/__init__.py +6 -6
- package/generator/pygen/codegen/serializers/builder_serializer.py +28 -14
- package/generator/pygen/codegen/serializers/client_serializer.py +13 -6
- package/generator/pygen/codegen/serializers/general_serializer.py +21 -1
- package/generator/pygen/codegen/serializers/model_serializer.py +9 -3
- package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -3
- package/generator/pygen/codegen/serializers/types_serializer.py +5 -12
- package/generator/pygen/codegen/templates/validation.py.jinja2 +1 -1
- package/generator/pygen/preprocess/__init__.py +68 -17
- package/generator/pygen/utils.py +15 -0
- package/generator/pygen.egg-info/PKG-INFO +1 -1
- package/generator/pygen.egg-info/requires.txt +1 -1
- package/generator/setup.py +1 -1
- package/package.json +37 -35
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { strictEqual } from "assert";
|
|
2
|
+
import { afterEach, describe, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const loadPyodide = vi.hoisted(() => vi.fn());
|
|
5
|
+
|
|
6
|
+
vi.mock("../src/pyodide-loader.js", () => ({ loadPyodide }));
|
|
7
|
+
|
|
8
|
+
describe("typespec-python: browser pyodide bootstrap", () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
delete (globalThis as any).window;
|
|
11
|
+
loadPyodide.mockReset();
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Hosts like the TypeSpec playground import every available emitter up front. Booting Pyodide at
|
|
16
|
+
// module scope downloaded a full CPython WebAssembly runtime on every page load, which pushed the
|
|
17
|
+
// page past the per-tab memory budget on mobile browsers and prevented it from loading.
|
|
18
|
+
it("does not boot pyodide when the emitter is imported in a browser", async () => {
|
|
19
|
+
(globalThis as any).window = globalThis;
|
|
20
|
+
|
|
21
|
+
await import("../src/emitter.js");
|
|
22
|
+
|
|
23
|
+
strictEqual(loadPyodide.mock.calls.length, 0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SdkBuiltInType } from "@azure-tools/typespec-client-generator-core";
|
|
2
|
+
import { deepStrictEqual } from "assert";
|
|
3
|
+
import { describe, it } from "vitest";
|
|
4
|
+
import type { PythonSdkContext } from "../src/lib.js";
|
|
5
|
+
import { getType } from "../src/types.js";
|
|
6
|
+
|
|
7
|
+
function createBuiltInType(
|
|
8
|
+
kind: SdkBuiltInType["kind"],
|
|
9
|
+
encode?: string,
|
|
10
|
+
wireType?: SdkBuiltInType,
|
|
11
|
+
): SdkBuiltInType {
|
|
12
|
+
return {
|
|
13
|
+
kind,
|
|
14
|
+
name: kind,
|
|
15
|
+
crossLanguageDefinitionId: `TypeSpec.${kind}`,
|
|
16
|
+
decorators: [],
|
|
17
|
+
encode,
|
|
18
|
+
wireType,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function emitBuiltInType(type: SdkBuiltInType): Record<string, any> {
|
|
23
|
+
const context = {
|
|
24
|
+
__simpleTypesMap: new Map(),
|
|
25
|
+
} as PythonSdkContext;
|
|
26
|
+
return getType(context, type);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("typespec-python: built-in types", () => {
|
|
30
|
+
it("preserves boolean encoded as string", () => {
|
|
31
|
+
deepStrictEqual(
|
|
32
|
+
emitBuiltInType(createBuiltInType("boolean", "string", createBuiltInType("string"))),
|
|
33
|
+
{
|
|
34
|
+
type: "boolean",
|
|
35
|
+
encode: "string",
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it.each(["safeint", "uint32", "uint8"] as const)(
|
|
41
|
+
"preserves %s encoded as string as an integer",
|
|
42
|
+
(kind) => {
|
|
43
|
+
deepStrictEqual(
|
|
44
|
+
emitBuiltInType(createBuiltInType(kind, "string", createBuiltInType("string"))),
|
|
45
|
+
{
|
|
46
|
+
type: "integer",
|
|
47
|
+
encode: "string",
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
it.each(["base64", "base64url"] as const)("preserves bytes encoded as %s", (encode) => {
|
|
54
|
+
deepStrictEqual(
|
|
55
|
+
emitBuiltInType(createBuiltInType("bytes", encode, createBuiltInType("string"))),
|
|
56
|
+
{
|
|
57
|
+
type: "bytes",
|
|
58
|
+
encode,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("falls back to the wire type for a custom encoding", () => {
|
|
64
|
+
deepStrictEqual(
|
|
65
|
+
emitBuiltInType(createBuiltInType("string", "abc", createBuiltInType("int32"))),
|
|
66
|
+
{
|
|
67
|
+
type: "integer",
|
|
68
|
+
encode: "abc",
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
import { compile, NodeHost } from "@typespec/compiler";
|
|
17
17
|
import { execSync } from "child_process";
|
|
18
18
|
import { existsSync, rmSync } from "fs";
|
|
19
|
-
import { access,
|
|
20
|
-
import { tmpdir } from "os";
|
|
19
|
+
import { access, mkdir, readdir, writeFile } from "fs/promises";
|
|
21
20
|
import { dirname, join, relative, resolve } from "path";
|
|
22
21
|
import pc from "picocolors";
|
|
23
22
|
|
|
@@ -70,7 +69,11 @@ export interface BuildTaskGroupsOptions {
|
|
|
70
69
|
|
|
71
70
|
// ---- Public constants ----
|
|
72
71
|
|
|
73
|
-
export const SKIP_SPECS: string[] = [
|
|
72
|
+
export const SKIP_SPECS: string[] = [
|
|
73
|
+
"type/file",
|
|
74
|
+
// LroPaging.postPagingLroWithBody generates begin_post_paging_lro_with_body with pylint R0915 (52/50).
|
|
75
|
+
"azure/resource-manager/operation-templates",
|
|
76
|
+
];
|
|
74
77
|
|
|
75
78
|
export const SpecialFlags: Record<string, Record<string, any>> = {
|
|
76
79
|
azure: {
|
|
@@ -321,7 +324,7 @@ export const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<str
|
|
|
321
324
|
{
|
|
322
325
|
"package-name": "typetest-model-usage-typeddictonly",
|
|
323
326
|
namespace: "typetest.model.usage.typeddictonly",
|
|
324
|
-
"models-mode": "
|
|
327
|
+
"models-mode": "none",
|
|
325
328
|
},
|
|
326
329
|
],
|
|
327
330
|
"type/model/visibility": [
|
|
@@ -655,6 +658,28 @@ export async function runParallel(
|
|
|
655
658
|
return results;
|
|
656
659
|
}
|
|
657
660
|
|
|
661
|
+
/**
|
|
662
|
+
* Removes every generated file under `tests/generated`, keeping only the
|
|
663
|
+
* hand-authored files that are tracked in git. `git clean -xdf` deletes ignored
|
|
664
|
+
* and untracked files but never tracked ones, so `.gitignore` stays the single
|
|
665
|
+
* source of truth for which fixtures survive regeneration (each exception there
|
|
666
|
+
* documents why it cannot be regenerated).
|
|
667
|
+
*/
|
|
668
|
+
export async function cleanGeneratedCode(generatedFolder: string): Promise<void> {
|
|
669
|
+
const repoDir = resolve(generatedFolder, "..");
|
|
670
|
+
const testsGeneratedDir = resolve(repoDir, "tests/generated");
|
|
671
|
+
if (!existsSync(testsGeneratedDir)) return;
|
|
672
|
+
|
|
673
|
+
console.log(pc.cyan(`\n${"=".repeat(60)}`));
|
|
674
|
+
console.log(pc.cyan(`Cleaning generated test packages`));
|
|
675
|
+
console.log(pc.cyan(`${"=".repeat(60)}\n`));
|
|
676
|
+
|
|
677
|
+
execSync(`git clean -x -d -f -q -- "${testsGeneratedDir}"`, {
|
|
678
|
+
cwd: repoDir,
|
|
679
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
658
683
|
/**
|
|
659
684
|
* Pre-create the marker files that the test harness expects to find before
|
|
660
685
|
* regeneration so it can verify they're cleared/preserved correctly.
|
|
@@ -698,99 +723,3 @@ export async function preprocess(flavor: string, generatedFolder: string): Promi
|
|
|
698
723
|
}),
|
|
699
724
|
);
|
|
700
725
|
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Resets the `tests/generated/{azure,unbranded}` baseline by sparse-checking-out
|
|
704
|
-
* `eng/tools/azure-sdk-tools/emitter/generated` from the Azure/azure-sdk-for-python repo, then
|
|
705
|
-
* deleting a couple of fully-generated package folders so regeneration has to
|
|
706
|
-
* recreate them from scratch (smoke test of full-emit path).
|
|
707
|
-
*
|
|
708
|
-
* `generatedFolder` is the per-repo `generator/` directory; baseline lands at
|
|
709
|
-
* `<generatedFolder>/../tests/generated`.
|
|
710
|
-
*/
|
|
711
|
-
export async function prepareBaselineOfGeneratedCode(generatedFolder: string): Promise<void> {
|
|
712
|
-
const repoUrl = "https://github.com/Azure/azure-sdk-for-python.git";
|
|
713
|
-
const branch = "typespec-python-generated-tests";
|
|
714
|
-
const sourceSubdir = "eng/tools/azure-sdk-tools/emitter/generated";
|
|
715
|
-
const testsGeneratedDir = resolve(generatedFolder, "../tests/generated");
|
|
716
|
-
|
|
717
|
-
console.log(pc.cyan(`\n${"=".repeat(60)}`));
|
|
718
|
-
console.log(pc.cyan(`Resetting baseline from ${repoUrl} (${branch}/${sourceSubdir})`));
|
|
719
|
-
console.log(pc.cyan(`${"=".repeat(60)}\n`));
|
|
720
|
-
|
|
721
|
-
// Wipe tests/generated
|
|
722
|
-
if (existsSync(testsGeneratedDir)) {
|
|
723
|
-
console.log(pc.dim(`Removing ${testsGeneratedDir}`));
|
|
724
|
-
rmSync(testsGeneratedDir, { recursive: true, force: true });
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Sparse-checkout the baseline folder into a temp directory
|
|
728
|
-
const tempDir = await mkdtemp(join(tmpdir(), "azsdk-baseline-"));
|
|
729
|
-
try {
|
|
730
|
-
console.log(pc.dim(`Cloning into ${tempDir}`));
|
|
731
|
-
const run = (cmd: string) =>
|
|
732
|
-
execSync(cmd, { cwd: tempDir, stdio: ["ignore", "ignore", "inherit"] });
|
|
733
|
-
|
|
734
|
-
run(`git init`);
|
|
735
|
-
run(`git config core.longpaths true`);
|
|
736
|
-
run(`git remote add origin ${repoUrl}`);
|
|
737
|
-
run(`git config core.sparseCheckout true`);
|
|
738
|
-
run(`git sparse-checkout init --cone`);
|
|
739
|
-
run(`git sparse-checkout set ${sourceSubdir}`);
|
|
740
|
-
run(`git fetch --depth 1 origin ${branch}`);
|
|
741
|
-
run(`git checkout FETCH_HEAD`);
|
|
742
|
-
|
|
743
|
-
// we don't copy whole generated folder, just the specific subfolders needed for tests
|
|
744
|
-
// to verify correct preservation/deletion of files and folders during regeneration,
|
|
745
|
-
// to avoid accidentally including any manually edited code that might be in the repo
|
|
746
|
-
// and cause confusion when it doesn't get updated during regeneration
|
|
747
|
-
const legacyCodePathNeededForTests = [
|
|
748
|
-
"azure/authentication-api-key",
|
|
749
|
-
"unbranded/authentication-api-key",
|
|
750
|
-
"azure/authentication-union",
|
|
751
|
-
"azure/generation-subdir",
|
|
752
|
-
"azure/generation-subdir2",
|
|
753
|
-
"unbranded/generation-subdir",
|
|
754
|
-
"unbranded/generation-subdir2",
|
|
755
|
-
"azure/azure-client-generator-core-alternate-type",
|
|
756
|
-
];
|
|
757
|
-
|
|
758
|
-
const sourceRoot = join(tempDir, ...sourceSubdir.split("/"));
|
|
759
|
-
for (const subPath of legacyCodePathNeededForTests) {
|
|
760
|
-
const segments = subPath.split("/");
|
|
761
|
-
const src = join(sourceRoot, ...segments);
|
|
762
|
-
const dest = join(testsGeneratedDir, ...segments);
|
|
763
|
-
if (!existsSync(src)) {
|
|
764
|
-
console.warn(pc.yellow(`Baseline folder not found: ${src}`));
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
console.log(pc.dim(`Copying ${subPath} -> ${dest}`));
|
|
768
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
769
|
-
await cp(src, dest, { recursive: true });
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
console.log(pc.green(`Baseline reset complete.\n`));
|
|
773
|
-
} finally {
|
|
774
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Smoke test the full-emit path: delete a couple of fully-generated package
|
|
778
|
-
// folders and every README.md so regeneration has to recreate them.
|
|
779
|
-
const deleteIfExists = (path: string) => {
|
|
780
|
-
if (!existsSync(path)) return;
|
|
781
|
-
console.log(pc.dim(`Deleting ${path}`));
|
|
782
|
-
rmSync(path, { recursive: true, force: true });
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
deleteIfExists(join(testsGeneratedDir, "azure", "authentication-http-custom"));
|
|
786
|
-
deleteIfExists(join(testsGeneratedDir, "unbranded", "encode-array"));
|
|
787
|
-
|
|
788
|
-
if (existsSync(testsGeneratedDir)) {
|
|
789
|
-
const entries = await readdir(testsGeneratedDir, { recursive: true, withFileTypes: true });
|
|
790
|
-
for (const entry of entries) {
|
|
791
|
-
if (entry.isFile() && entry.name === "README.md") {
|
|
792
|
-
deleteIfExists(join(entry.parentPath, entry.name));
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
@@ -20,13 +20,12 @@ import pc from "picocolors";
|
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
import { parseArgs } from "util";
|
|
22
22
|
|
|
23
|
+
import type { RegenerateContext, RegenerateFlags } from "./regenerate-common.js";
|
|
23
24
|
import {
|
|
24
25
|
buildTaskGroups,
|
|
26
|
+
cleanGeneratedCode,
|
|
25
27
|
getSubdirectories,
|
|
26
|
-
prepareBaselineOfGeneratedCode,
|
|
27
28
|
preprocess,
|
|
28
|
-
RegenerateContext,
|
|
29
|
-
RegenerateFlags,
|
|
30
29
|
runParallel,
|
|
31
30
|
} from "./regenerate-common.js";
|
|
32
31
|
|
|
@@ -256,7 +255,7 @@ async function main() {
|
|
|
256
255
|
const startTime = performance.now();
|
|
257
256
|
let success: boolean;
|
|
258
257
|
|
|
259
|
-
await
|
|
258
|
+
await cleanGeneratedCode(GENERATED_FOLDER);
|
|
260
259
|
|
|
261
260
|
if (flavor) {
|
|
262
261
|
success = await regenerateFlavor(flavor, name, debug, jobs);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
import { ChildProcess
|
|
2
|
+
import type { ChildProcess } from "child_process";
|
|
3
|
+
import { spawn } from "child_process";
|
|
3
4
|
import fs from "fs";
|
|
4
5
|
import { cpus } from "os";
|
|
5
6
|
import { dirname, join } from "path";
|
|
Binary file
|
|
Binary file
|
|
@@ -12,7 +12,7 @@ from abc import ABC, abstractmethod
|
|
|
12
12
|
from typing import Any, Iterator, Optional, Union
|
|
13
13
|
|
|
14
14
|
import yaml
|
|
15
|
-
from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE
|
|
15
|
+
from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE, is_typeddict_only
|
|
16
16
|
|
|
17
17
|
from ._version import VERSION
|
|
18
18
|
|
|
@@ -36,6 +36,7 @@ class OptionsDict(MutableMapping):
|
|
|
36
36
|
"low-level-client": False,
|
|
37
37
|
"no-async": False,
|
|
38
38
|
"no-namespace-folders": False,
|
|
39
|
+
"generate-typeddict": True,
|
|
39
40
|
"polymorphic-examples": 5,
|
|
40
41
|
"validate-versioning": True,
|
|
41
42
|
"version-tolerant": True,
|
|
@@ -45,10 +46,20 @@ class OptionsDict(MutableMapping):
|
|
|
45
46
|
|
|
46
47
|
def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
|
|
47
48
|
self._data = options.copy() if options else {}
|
|
49
|
+
# 'models-mode: typeddict' is deprecated. Represent it internally as 'none' models-mode with
|
|
50
|
+
# TypedDict generation enabled, so the rest of the codebase reasons in terms of models-mode
|
|
51
|
+
# plus the generate-typeddict flag.
|
|
52
|
+
if self._data.get("models-mode") == "typeddict":
|
|
53
|
+
self._data["generate-typeddict"] = True
|
|
48
54
|
for key in list(self._data):
|
|
49
55
|
self._data[key] = self._validate_and_transform(key, self._data[key])
|
|
50
56
|
self._validate_combinations()
|
|
51
57
|
|
|
58
|
+
@property
|
|
59
|
+
def generate_typeddict_only(self) -> bool:
|
|
60
|
+
"""Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
|
|
61
|
+
return is_typeddict_only(self)
|
|
62
|
+
|
|
52
63
|
def __getitem__(self, key: str) -> Any: # pylint: disable=too-many-return-statements
|
|
53
64
|
if key == "head-as-boolean" and self.get("azure-arm"):
|
|
54
65
|
# override to always true if azure-arm is set
|
|
@@ -167,14 +178,22 @@ class OptionsDict(MutableMapping):
|
|
|
167
178
|
if key == "builders-visibility" and value not in ["public", "hidden", "embedded"]:
|
|
168
179
|
raise ValueError("The value of --builders-visibility must be either 'public', 'hidden', or 'embedded'")
|
|
169
180
|
|
|
181
|
+
if key == "models-mode" and value == "typeddict":
|
|
182
|
+
# Deprecated: keep accepting it for back-compat but store it as 'none' (falsy) with
|
|
183
|
+
# TypedDict generation enabled (see OptionsDict.__init__ and generate_typeddict_only).
|
|
184
|
+
_LOGGER.warning(
|
|
185
|
+
"'models-mode: typeddict' is deprecated. Use 'models-mode: none' with "
|
|
186
|
+
"'generate-typeddict: true' (the default) instead."
|
|
187
|
+
)
|
|
188
|
+
value = False
|
|
189
|
+
|
|
170
190
|
if key == "models-mode" and value == "none":
|
|
171
191
|
value = False # switch to falsy value for easier code writing
|
|
172
192
|
|
|
173
|
-
if key == "models-mode" and value not in ["msrest", "dpg",
|
|
193
|
+
if key == "models-mode" and value not in ["msrest", "dpg", False]:
|
|
174
194
|
raise ValueError(
|
|
175
|
-
"--models-mode can only be '
|
|
176
|
-
"Pass in '
|
|
177
|
-
"'none' if you don't want any."
|
|
195
|
+
"--models-mode can only be 'dpg' or 'none'. "
|
|
196
|
+
"Pass in 'dpg' for DPG models, or 'none' if you don't want any."
|
|
178
197
|
)
|
|
179
198
|
if key == "package-mode":
|
|
180
199
|
if (
|
|
@@ -169,7 +169,9 @@ def build_type(yaml_data: dict[str, Any], code_model: CodeModel) -> BaseType:
|
|
|
169
169
|
# need to special case model to avoid recursion
|
|
170
170
|
if yaml_data["base"] == "typeddict":
|
|
171
171
|
model_type = TypedDictModelType # type: ignore
|
|
172
|
-
elif yaml_data["base"] == "json" or
|
|
172
|
+
elif yaml_data["base"] == "json" or (
|
|
173
|
+
not code_model.options["models-mode"] and not code_model.generate_typeddict_only
|
|
174
|
+
):
|
|
173
175
|
model_type = JSONModelType
|
|
174
176
|
elif yaml_data["base"] == "dpg":
|
|
175
177
|
model_type = DPGModelType # type: ignore
|
|
@@ -17,6 +17,7 @@ from .operation_group import OperationGroup
|
|
|
17
17
|
from .utils import NamespaceType
|
|
18
18
|
from .._utils import DEFAULT_HEADER_TEXT, DEFAULT_LICENSE_DESCRIPTION
|
|
19
19
|
from ... import OptionsDict
|
|
20
|
+
from ...utils import is_typeddict_only
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _is_legacy(options) -> bool:
|
|
@@ -89,7 +90,7 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
89
90
|
self.clients: list[Client] = [
|
|
90
91
|
Client.from_yaml(client_yaml_data, self) for client_yaml_data in yaml_data["clients"]
|
|
91
92
|
]
|
|
92
|
-
if self.options["models-mode"] and self.model_types:
|
|
93
|
+
if (self.options["models-mode"] or self.generate_typeddict_only) and self.model_types:
|
|
93
94
|
self.sort_model_types()
|
|
94
95
|
self.named_unions: list[CombinedType] = [
|
|
95
96
|
t for t in self.types_map.values() if isinstance(t, CombinedType) and t.name
|
|
@@ -174,6 +175,11 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
174
175
|
def get_unique_types_alias(self, serialize_namespace: str, imported_namespace: str) -> str:
|
|
175
176
|
return self._get_unique_import_alias(serialize_namespace, imported_namespace, "types")
|
|
176
177
|
|
|
178
|
+
@property
|
|
179
|
+
def generate_typeddict_only(self) -> bool:
|
|
180
|
+
"""Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
|
|
181
|
+
return is_typeddict_only(self.options)
|
|
182
|
+
|
|
177
183
|
@property
|
|
178
184
|
def client_namespace_types(self) -> dict[str, ClientNamespaceType]:
|
|
179
185
|
if not self._client_namespace_types:
|
|
@@ -40,7 +40,7 @@ class EnumValue(BaseType):
|
|
|
40
40
|
|
|
41
41
|
def type_annotation(self, **kwargs: Any) -> str:
|
|
42
42
|
"""The python type used for type annotation"""
|
|
43
|
-
if self.code_model.
|
|
43
|
+
if self.code_model.generate_typeddict_only:
|
|
44
44
|
# A single constant enum value must be
|
|
45
45
|
# annotated with its literal value directly (e.g. ``Literal["red"]``).
|
|
46
46
|
return f"Literal[{self.value_type.get_declaration(self.value)}]"
|
|
@@ -83,7 +83,7 @@ class EnumValue(BaseType):
|
|
|
83
83
|
file_import = FileImport(self.code_model)
|
|
84
84
|
file_import.merge(self.value_type.imports(**kwargs))
|
|
85
85
|
file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB, TypingSection.REGULAR)
|
|
86
|
-
if self.code_model.
|
|
86
|
+
if self.code_model.generate_typeddict_only:
|
|
87
87
|
# In typeddict mode the enums module (``_enums.py``) is never generated
|
|
88
88
|
return file_import
|
|
89
89
|
serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
|
|
@@ -176,7 +176,7 @@ class EnumType(BaseType):
|
|
|
176
176
|
|
|
177
177
|
@property
|
|
178
178
|
def is_typeddict_mode(self) -> bool:
|
|
179
|
-
return self.code_model.
|
|
179
|
+
return self.code_model.generate_typeddict_only
|
|
180
180
|
|
|
181
181
|
def type_annotation(self, **kwargs: Any) -> str:
|
|
182
182
|
"""The python type used for type annotation
|
|
@@ -184,7 +184,7 @@ class EnumType(BaseType):
|
|
|
184
184
|
:return: The type annotation for this schema
|
|
185
185
|
:rtype: str
|
|
186
186
|
"""
|
|
187
|
-
if self.code_model.options["models-mode"]:
|
|
187
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
188
188
|
if self.is_typeddict_mode:
|
|
189
189
|
# In typeddict mode, enums are Literal aliases defined in types.py
|
|
190
190
|
serialize_namespace_type = kwargs.get("serialize_namespace_type")
|
|
@@ -221,13 +221,13 @@ class EnumType(BaseType):
|
|
|
221
221
|
return self.value_type.get_declaration(value)
|
|
222
222
|
|
|
223
223
|
def docstring_text(self, **kwargs: Any) -> str:
|
|
224
|
-
if self.code_model.options["models-mode"]:
|
|
224
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
225
225
|
return self.name
|
|
226
226
|
return self.value_type.type_annotation(**kwargs)
|
|
227
227
|
|
|
228
228
|
def docstring_type(self, **kwargs: Any) -> str:
|
|
229
229
|
"""The python type used for RST syntax input and type annotation."""
|
|
230
|
-
if self.code_model.options["models-mode"]:
|
|
230
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
231
231
|
type_annotation = self.value_type.type_annotation(**kwargs)
|
|
232
232
|
enum_type_annotation = f"{self.client_namespace}.models.{self.name}"
|
|
233
233
|
return f"{type_annotation} or ~{enum_type_annotation}"
|
|
@@ -261,7 +261,7 @@ class EnumType(BaseType):
|
|
|
261
261
|
def imports(self, **kwargs: Any) -> FileImport:
|
|
262
262
|
file_import = FileImport(self.code_model)
|
|
263
263
|
file_import.merge(self.value_type.imports(**kwargs))
|
|
264
|
-
if self.code_model.options["models-mode"]:
|
|
264
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
265
265
|
if self.is_typeddict_mode:
|
|
266
266
|
# In typeddict mode, enums are Literal aliases in types.py — no Union needed
|
|
267
267
|
serialize_namespace_type = kwargs.get("serialize_namespace_type")
|
|
@@ -38,6 +38,7 @@ class ListType(BaseType):
|
|
|
38
38
|
self.code_model.options["version-tolerant"]
|
|
39
39
|
and self.element_type.is_xml
|
|
40
40
|
and not self.code_model.options["models-mode"]
|
|
41
|
+
and not self.code_model.generate_typeddict_only
|
|
41
42
|
):
|
|
42
43
|
# this means we're version tolerant XML, we just return the XML element
|
|
43
44
|
return self.element_type.type_annotation(**kwargs)
|
|
@@ -77,9 +77,7 @@ class ModelType(BaseType): # pylint: disable=too-many-instance-attributes, too-
|
|
|
77
77
|
self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId")
|
|
78
78
|
self.usage: int = self.yaml_data.get("usage", UsageFlags.Input.value | UsageFlags.Output.value)
|
|
79
79
|
self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace)
|
|
80
|
-
self.is_typed_dict_only: bool = (
|
|
81
|
-
self.yaml_data.get("typedDictOnly", False) or code_model.options["models-mode"] == "typeddict"
|
|
82
|
-
)
|
|
80
|
+
self.is_typed_dict_only: bool = self.yaml_data.get("typedDictOnly", False) or code_model.generate_typeddict_only
|
|
83
81
|
|
|
84
82
|
@property
|
|
85
83
|
def is_usage_output(self) -> bool:
|
|
@@ -334,7 +334,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
334
334
|
file_import.merge(
|
|
335
335
|
response.imports(async_mode=async_mode, need_import_iobase=self.need_import_iobase, **kwargs)
|
|
336
336
|
)
|
|
337
|
-
if self.code_model.options["models-mode"]:
|
|
337
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
338
338
|
for exception in self.exceptions:
|
|
339
339
|
file_import.merge(exception.imports(async_mode=async_mode, **kwargs))
|
|
340
340
|
|
|
@@ -552,7 +552,12 @@ class Operation(OperationBase[Response]):
|
|
|
552
552
|
"distributed_trace_async",
|
|
553
553
|
ImportType.SDKCORE,
|
|
554
554
|
)
|
|
555
|
-
if
|
|
555
|
+
if (
|
|
556
|
+
self.has_response_body
|
|
557
|
+
and not self.has_optional_return_type
|
|
558
|
+
and not self.code_model.options["models-mode"]
|
|
559
|
+
and not self.code_model.generate_typeddict_only
|
|
560
|
+
):
|
|
556
561
|
file_import.add_submodule_import("typing", "cast", ImportType.STDLIB)
|
|
557
562
|
|
|
558
563
|
return file_import
|
|
@@ -338,7 +338,9 @@ class Parameter(_ParameterBase):
|
|
|
338
338
|
) -> ParameterMethodLocation:
|
|
339
339
|
if not self.in_method_signature:
|
|
340
340
|
raise ValueError(f"Parameter '{self.client_name}' is not in the method.")
|
|
341
|
-
if
|
|
341
|
+
if (
|
|
342
|
+
self.code_model.options["models-mode"] == "dpg" or self.code_model.generate_typeddict_only
|
|
343
|
+
) and self.in_flattened_body:
|
|
342
344
|
return ParameterMethodLocation.KEYWORD_ONLY
|
|
343
345
|
if self.grouper:
|
|
344
346
|
return ParameterMethodLocation.POSITIONAL
|
|
@@ -34,6 +34,20 @@ def add_to_pylint_disable(curr_str: str, entry: str) -> str:
|
|
|
34
34
|
return f" # pylint: disable={entry}"
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def escape_sphinx_field_name(name: str) -> str:
|
|
38
|
+
"""Escape a name used as the target of a Sphinx info field (``:ivar``, ``:vartype``,
|
|
39
|
+
``:keyword``, ``:paramtype``, ``:param``, ``:type``).
|
|
40
|
+
|
|
41
|
+
A name containing ``@`` (e.g. a wire name such as ``@search.facets``) is wrapped in
|
|
42
|
+
double backticks so it is treated as an inline literal. This renders cleanly in Sphinx
|
|
43
|
+
without a leading ``@`` being misinterpreted, and — unlike a ``\\@`` escape — introduces
|
|
44
|
+
no invalid escape sequence into the generated Python docstring.
|
|
45
|
+
"""
|
|
46
|
+
if "@" in name:
|
|
47
|
+
return f"``{name}``"
|
|
48
|
+
return name
|
|
49
|
+
|
|
50
|
+
|
|
37
51
|
class NamespaceType(str, Enum):
|
|
38
52
|
"""Special signal for impports"""
|
|
39
53
|
|
|
@@ -207,10 +207,10 @@ class JinjaSerializer(ReaderAndWriter):
|
|
|
207
207
|
self._serialize_and_write_top_level_folder(env=env, namespace=client_namespace)
|
|
208
208
|
|
|
209
209
|
# add models folder if there are models in this namespace
|
|
210
|
-
is_typeddict_mode = self.code_model.
|
|
211
|
-
if (
|
|
212
|
-
self.code_model.
|
|
213
|
-
)
|
|
210
|
+
is_typeddict_mode = self.code_model.generate_typeddict_only
|
|
211
|
+
if (self.code_model.has_non_json_models(client_namespace_type.models) or client_namespace_type.enums) and (
|
|
212
|
+
self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
|
|
213
|
+
):
|
|
214
214
|
self._serialize_and_write_models_folder(
|
|
215
215
|
env=env,
|
|
216
216
|
namespace=client_namespace,
|
|
@@ -238,7 +238,7 @@ class JinjaSerializer(ReaderAndWriter):
|
|
|
238
238
|
).serialize(),
|
|
239
239
|
)
|
|
240
240
|
|
|
241
|
-
if not self.code_model.options["models-mode"]:
|
|
241
|
+
if not self.code_model.options["models-mode"] and not self.code_model.generate_typeddict_only:
|
|
242
242
|
# keep models file if users ended up just writing a models file
|
|
243
243
|
model_path = generation_path / Path("models.py")
|
|
244
244
|
if self.read_file(model_path):
|
|
@@ -321,7 +321,7 @@ class JinjaSerializer(ReaderAndWriter):
|
|
|
321
321
|
# Write the models folder
|
|
322
322
|
models_path = self.code_model.get_generation_dir(namespace) / "models"
|
|
323
323
|
models_mode = self.code_model.options["models-mode"]
|
|
324
|
-
if models_mode
|
|
324
|
+
if models_mode == "dpg" or self.code_model.generate_typeddict_only:
|
|
325
325
|
serializer = DpgModelSerializer
|
|
326
326
|
else:
|
|
327
327
|
serializer = MsrestModelSerializer
|