-
Notifications
You must be signed in to change notification settings - Fork 222
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
base: main
Are you sure you want to change the base?
Changes from all commits
277e199
b2edb8f
c80836c
f7dc98a
98df4ba
b41d46c
16b5054
f8a14d6
6089677
db80270
18422e8
4f7594f
b7464b6
e66c541
cd69beb
04f6507
88233b2
ac1c1bd
9a6c6d5
5015c10
947cc56
ae1aca4
ebfa971
222167e
8d0a423
379d9a8
69487a2
0620a37
efdddcd
7abd521
27f520c
90b037d
cefde86
edd6db7
4bec00b
0b56a1f
5e601e0
cb25b96
903f319
8c98d9b
3c2c8f5
49f459b
68e0941
d551154
790911a
8f54d6e
783e899
d04db9c
6dd488c
5110c8d
86bf692
9be5ab7
0f53dfb
22ab1a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" { | ||
|
@@ -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> = {}; | ||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(" ")); | ||
} | ||
} | ||
} |
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], | ||
}); | ||
} |
There was a problem hiding this comment.
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.