Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support generating Python SDK with Pyodide #4784

Open
wants to merge 54 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
277e199
Support generating with Pyodide
YalinLi0312 Oct 17, 2024
b2edb8f
Merge branch 'main' into pyodide
YalinLi0312 Oct 17, 2024
c80836c
Merge branch 'main' into pyodide
YalinLi0312 Oct 17, 2024
f7dc98a
Fix spell check
YalinLi0312 Oct 17, 2024
98df4ba
Fix format
YalinLi0312 Oct 17, 2024
b41d46c
Fix lint
YalinLi0312 Oct 17, 2024
16b5054
Merge branch 'main' into pyodide
YalinLi0312 Oct 17, 2024
f8a14d6
Use pygen wheel from fork
YalinLi0312 Oct 18, 2024
6089677
Align dependencies versions
YalinLi0312 Oct 18, 2024
db80270
Align dev requirements version
YalinLi0312 Oct 23, 2024
18422e8
Update run-python3.ts
YalinLi0312 Oct 24, 2024
4f7594f
Wait for runPython3 subprocess complete
YalinLi0312 Oct 25, 2024
b7464b6
Run install and prepare only in first generate
YalinLi0312 Oct 25, 2024
e66c541
Ignore SyntaxWarning from mistune 0.8.4
YalinLi0312 Oct 25, 2024
cd69beb
Fix lint
YalinLi0312 Oct 29, 2024
04f6507
Run codegen command async
YalinLi0312 Oct 29, 2024
88233b2
Merge branch 'main' into pyodide
YalinLi0312 Oct 29, 2024
ac1c1bd
Prepare venv and install pygen deps in build step
YalinLi0312 Nov 7, 2024
9a6c6d5
Merge branch 'main' into pyodide
YalinLi0312 Nov 7, 2024
5015c10
Update package.json
YalinLi0312 Nov 8, 2024
947cc56
Run build with --verbose
YalinLi0312 Nov 8, 2024
ae1aca4
Build and use local pygen wheel
YalinLi0312 Nov 8, 2024
ebfa971
Revert to use remote pygen wheel
YalinLi0312 Nov 9, 2024
222167e
Build with --verbose
YalinLi0312 Nov 9, 2024
8d0a423
Update dev_requirements.txt
YalinLi0312 Nov 9, 2024
379d9a8
Use black 24.8.0
YalinLi0312 Nov 9, 2024
69487a2
Build and use local pygen wheel
YalinLi0312 Nov 9, 2024
0620a37
Update build cmd
YalinLi0312 Nov 9, 2024
efdddcd
Create build_pygen_wheel.py
YalinLi0312 Nov 9, 2024
7abd521
Use docutils 0.20.1
YalinLi0312 Nov 9, 2024
27f520c
Update build python wheel cmd
YalinLi0312 Nov 9, 2024
90b037d
Update dev_requirements.txt
YalinLi0312 Nov 9, 2024
cefde86
Merge branch 'main' into pyodide
YalinLi0312 Nov 11, 2024
edd6db7
test
YalinLi0312 Nov 12, 2024
4bec00b
test
YalinLi0312 Nov 12, 2024
0b56a1f
test
YalinLi0312 Nov 12, 2024
5e601e0
test
YalinLi0312 Nov 12, 2024
cb25b96
Build wheel in a separate venv
YalinLi0312 Nov 12, 2024
903f319
Merge branch 'main' into pyodide
YalinLi0312 Nov 13, 2024
8c98d9b
Install python deps for pyodide in "npm install"
YalinLi0312 Nov 13, 2024
3c2c8f5
Change python check exceptions to warnings
YalinLi0312 Nov 13, 2024
49f459b
Add pyodide tests
YalinLi0312 Nov 13, 2024
68e0941
test
YalinLi0312 Nov 13, 2024
d551154
Uncomment test on venv
YalinLi0312 Nov 14, 2024
790911a
Update dev_requirements.txt
YalinLi0312 Nov 14, 2024
8f54d6e
Merge branch 'main' into pyodide
YalinLi0312 Nov 14, 2024
783e899
Fix spell, format, lint
YalinLi0312 Nov 14, 2024
d04db9c
Create venv when it's not found in emitter
YalinLi0312 Nov 19, 2024
6dd488c
Update warning info
YalinLi0312 Nov 19, 2024
5110c8d
Fix format
YalinLi0312 Nov 20, 2024
86bf692
Update emitter.ts
YalinLi0312 Nov 22, 2024
9be5ab7
Cleanup scripts
YalinLi0312 Nov 22, 2024
0f53dfb
Fix format and lint
YalinLi0312 Nov 22, 2024
22ab1a8
Fix a bug in diff check in ci
YalinLi0312 Nov 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ words:
- xplat
- xxsubtype
- yamls
- pyodide
- pyimport
- NODEFS
- deps
- pyyaml
ignorePaths:
- "**/node_modules/**"
- "**/dist/**"
Expand Down
154 changes: 111 additions & 43 deletions packages/http-client-python/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import {
SdkServiceOperation,
} from "@azure-tools/typespec-client-generator-core";
import { EmitContext } from "@typespec/compiler";
import { execSync } from "child_process";
import { exec } from "child_process";
import fs from "fs";
import path, { dirname } from "path";
import { loadPyodide } from "pyodide";
import { fileURLToPath } from "url";
import { emitCodeModel } from "./code-model.js";
import { saveCodeModelAsYaml } from "./external-process.js";
import { PythonEmitterOptions, PythonSdkContext } from "./lib.js";
import { runPython3 } from "./run-python3.js";
import { removeUnderscoresFromNamespace } from "./utils.js";

export function getModelsMode(context: SdkContext): "dpg" | "none" {
Expand Down Expand Up @@ -78,50 +80,116 @@ export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const outputDir = context.emitterOutputDir;
const yamlMap = emitCodeModel(sdkContext);
addDefaultOptions(sdkContext);
const yamlPath = await saveCodeModelAsYaml("python-yaml-path", yamlMap);
let venvPath = path.join(root, "venv");
if (fs.existsSync(path.join(venvPath, "bin"))) {
venvPath = path.join(venvPath, "bin", "python");
} else if (fs.existsSync(path.join(venvPath, "Scripts"))) {
venvPath = path.join(venvPath, "Scripts", "python.exe");
} else {
throw new Error("Virtual environment doesn't exist.");
}
const commandArgs = [
venvPath,
`${root}/eng/scripts/setup/run_tsp.py`,
`--output-folder=${outputDir}`,
`--cadl-file=${yamlPath}`,
];
addDefaultOptions(sdkContext);
const resolvedOptions = sdkContext.emitContext.options;
if (resolvedOptions["packaging-files-config"]) {
const keyValuePairs = Object.entries(resolvedOptions["packaging-files-config"]).map(
([key, value]) => {
return `${key}:${value}`;
},
);
commandArgs.push(`--packaging-files-config='${keyValuePairs.join("|")}'`);
resolvedOptions["packaging-files-config"] = undefined;
}
if (
resolvedOptions["package-pprint-name"] !== undefined &&
!resolvedOptions["package-pprint-name"].startsWith('"')
) {
resolvedOptions["package-pprint-name"] = `"${resolvedOptions["package-pprint-name"]}"`;
}
if (resolvedOptions["use-pyodide"]) {
const commandArgs: Record<string, string> = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the command args should be same whatever use pyodide or not, it's better to consolidate the logic.

if (resolvedOptions["packaging-files-config"]) {
const keyValuePairs = Object.entries(resolvedOptions["packaging-files-config"]).map(
([key, value]) => {
return `${key}:${value}`;
},
);
commandArgs["packaging-files-config"] = keyValuePairs.join("|");
resolvedOptions["packaging-files-config"] = undefined;
}
if (
resolvedOptions["package-pprint-name"] !== undefined &&
!resolvedOptions["package-pprint-name"].startsWith('"')
) {
resolvedOptions["package-pprint-name"] = `${resolvedOptions["package-pprint-name"]}`;
}

for (const [key, value] of Object.entries(resolvedOptions)) {
commandArgs.push(`--${key}=${value}`);
}
if (sdkContext.arm === true) {
commandArgs.push("--azure-arm=true");
}
if (resolvedOptions.flavor === "azure") {
commandArgs.push("--emit-cross-language-definition-file=true");
}
commandArgs.push("--from-typespec=true");
if (!program.compilerOptions.noEmit && !program.hasError()) {
execSync(commandArgs.join(" "));
for (const [key, value] of Object.entries(resolvedOptions)) {
commandArgs[key] = value;
}
if (sdkContext.arm === true) {
commandArgs["azure-arm"] = "true";
}
if (resolvedOptions.flavor === "azure") {
commandArgs["emit-cross-language-definition-file"] = "true";
}
commandArgs["from-typespec"] = "true";

if (!program.compilerOptions.noEmit && !program.hasError()) {
const outputFolder = path.relative(root, outputDir);
if (!fs.existsSync(outputFolder)) {
fs.mkdirSync(outputFolder, { recursive: true });
}
const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") });
pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, ".");
await pyodide.loadPackage("setuptools");
await pyodide.loadPackage("tomli");
await pyodide.loadPackage("docutils");
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install("emfs:generator/dist/pygen-0.1.0-py3-none-any.whl");
const yamlRelativePath = path.relative(root, yamlPath);
const globals = pyodide.toPy({ outputFolder, yamlRelativePath, commandArgs });
const python = `
async def main():
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", SyntaxWarning)
from pygen import m2r, preprocess, codegen, black

m2r.M2R(output_folder=outputFolder, cadl_file=yamlRelativePath, **commandArgs).process()
preprocess.PreProcessPlugin(output_folder=outputFolder, cadl_file=yamlRelativePath, **commandArgs).process()
codegen.CodeGenerator(output_folder=outputFolder, cadl_file=yamlRelativePath, **commandArgs).process()
black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process()

await main()
Comment on lines +131 to +142
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is pyodide able to run a py file? i just wondering if two paths could be consolidated. previously, we used py file. here, we use inline code.

`;
await pyodide.runPythonAsync(python, { globals });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how will pyodide deal with exception?

}
} else {
let venvPath = path.join(root, "venv");
if (!fs.existsSync(venvPath)) {
await runPython3("./eng/scripts/setup/install.py");
await runPython3("./eng/scripts/setup/prepare.py");
}
if (fs.existsSync(path.join(venvPath, "bin"))) {
venvPath = path.join(venvPath, "bin", "python");
} else if (fs.existsSync(path.join(venvPath, "Scripts"))) {
venvPath = path.join(venvPath, "Scripts", "python.exe");
} else {
throw new Error("Virtual environment doesn't exist.");
}
const commandArgs = [
venvPath,
`${root}/eng/scripts/setup/run_tsp.py`,
`--output-folder=${outputDir}`,
`--cadl-file=${yamlPath}`,
];
if (resolvedOptions["packaging-files-config"]) {
const keyValuePairs = Object.entries(resolvedOptions["packaging-files-config"]).map(
([key, value]) => {
return `${key}:${value}`;
},
);
commandArgs.push(`--packaging-files-config='${keyValuePairs.join("|")}'`);
resolvedOptions["packaging-files-config"] = undefined;
}
if (
resolvedOptions["package-pprint-name"] !== undefined &&
!resolvedOptions["package-pprint-name"].startsWith('"')
) {
resolvedOptions["package-pprint-name"] = `"${resolvedOptions["package-pprint-name"]}"`;
}

for (const [key, value] of Object.entries(resolvedOptions)) {
commandArgs.push(`--${key}=${value}`);
}
if (sdkContext.arm === true) {
commandArgs.push("--azure-arm=true");
}
if (resolvedOptions.flavor === "azure") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic about options (e.g. "azure-arm"/"flavor") between python mode and pyodide mode is same so it is better to handle them in one central place.

commandArgs.push("--emit-cross-language-definition-file=true");
}
commandArgs.push("--from-typespec=true");
if (!program.compilerOptions.noEmit && !program.hasError()) {
await exec(commandArgs.join(" "));
}
}
}
2 changes: 2 additions & 0 deletions packages/http-client-python/emitter/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface PythonEmitterOptions {
debug?: boolean;
flavor?: "azure";
"examples-dir"?: string;
"use-pyodide"?: boolean;
}

export interface PythonSdkContext<TServiceOperation extends SdkServiceOperation>
Expand All @@ -43,6 +44,7 @@ const EmitterOptionsSchema: JSONSchemaType<PythonEmitterOptions> = {
debug: { type: "boolean", nullable: true },
flavor: { type: "string", nullable: true },
"examples-dir": { type: "string", nullable: true, format: "absolute-path" },
"use-pyodide": { type: "boolean", nullable: true },
},
required: [],
};
Expand Down
20 changes: 20 additions & 0 deletions packages/http-client-python/emitter/src/run-python3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// This script wraps logic in @azure-tools/extension to resolve
// the path to Python 3 so that a Python script file can be run
// from an npm script in package.json. It uses the same Python 3
// path resolution algorithm as AutoRest so that the behavior
// is fully consistent (and also supports AUTOREST_PYTHON_EXE).
//
// Invoke it like so: "tsx run-python3.ts script.py"

import cp from "child_process";
import { patchPythonPath } from "./system-requirements.js";

export async function runPython3(...args: string[]) {
const command = await patchPythonPath(["python", ...args], {
version: ">=3.8",
environmentVariable: "AUTOREST_PYTHON_EXE",
});
cp.execSync(command.join(" "), {
stdio: [0, 1, 2],
});
}
Loading
Loading