From a7aecc6159ad50efaec57b529997ff36e720e2a1 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 6 Dec 2024 16:06:53 -0500 Subject: [PATCH 01/39] adopt yalin's code with comments addressed --- cspell.yaml | 5 + .../http-client-python/emitter/src/emitter.ts | 81 ++++-- .../http-client-python/emitter/src/lib.ts | 2 + .../emitter/src/run-python3.ts | 20 ++ .../emitter/src/system-requirements.ts | 261 ++++++++++++++++++ .../eng/scripts/Generate-WithPyodide.ps1 | 11 + .../eng/scripts/Test-Packages.ps1 | 14 +- .../eng/scripts/ci/regenerate.ts | 6 + .../eng/scripts/setup/build.ts | 14 + .../eng/scripts/setup/build_pygen_wheel.py | 40 +++ .../eng/scripts/setup/install.py | 6 +- .../eng/scripts/setup/install.ts | 27 ++ .../eng/scripts/setup/prepare.py | 2 +- .../generator/requirements.txt | 16 +- packages/http-client-python/package.json | 7 +- 15 files changed, 479 insertions(+), 33 deletions(-) create mode 100644 packages/http-client-python/emitter/src/run-python3.ts create mode 100644 packages/http-client-python/emitter/src/system-requirements.ts create mode 100644 packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 create mode 100644 packages/http-client-python/eng/scripts/setup/build.ts create mode 100644 packages/http-client-python/eng/scripts/setup/build_pygen_wheel.py create mode 100644 packages/http-client-python/eng/scripts/setup/install.ts diff --git a/cspell.yaml b/cspell.yaml index 8bd06fe5d0..23c272f227 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -47,6 +47,7 @@ words: - debugpy - Declipse - Dedupes + - deps - destructures - devdiv - Diagnoser @@ -131,6 +132,7 @@ words: - nanos - nexted - nihao + - NODEFS - noformat - noopener - noreferrer @@ -163,12 +165,15 @@ words: - pwsh - pyexpat - pygen + - pyimport - pylint - pylintrc + - pyodide - pyproject - pyright - pyrightconfig - pytest + - pyyaml - rcfile - reactivex - recase diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 906ef1e69c..a8838a82ff 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -5,13 +5,15 @@ import { SdkServiceOperation, } from "@azure-tools/typespec-client-generator-core"; import { EmitContext, NoTarget } 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, reportDiagnostic } from "./lib.js"; +import { runPython3 } from "./run-python3.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; export function getModelsMode(context: SdkContext): "dpg" | "none" { @@ -85,9 +87,8 @@ export async function $onEmit(context: EmitContext) { }); return; } - - addDefaultOptions(sdkContext); const yamlPath = await saveCodeModelAsYaml("python-yaml-path", yamlMap); + addDefaultOptions(sdkContext); let venvPath = path.join(root, "venv"); if (fs.existsSync(path.join(venvPath, "bin"))) { venvPath = path.join(venvPath, "bin", "python"); @@ -96,40 +97,88 @@ export async function $onEmit(context: EmitContext) { } 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}`, - ]; const resolvedOptions = sdkContext.emitContext.options; + const commandArgs: Record = {} 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("|")}'`); + 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"]}"`; + resolvedOptions["package-pprint-name"] = `${resolvedOptions["package-pprint-name"]}`; } for (const [key, value] of Object.entries(resolvedOptions)) { - commandArgs.push(`--${key}=${value}`); + commandArgs[key] = value; } if (sdkContext.arm === true) { - commandArgs.push("--azure-arm=true"); + commandArgs["azure-arm"] = "true"; } if (resolvedOptions.flavor === "azure") { - commandArgs.push("--emit-cross-language-definition-file=true"); + commandArgs["emit-cross-language-definition-file"] = "true"; } - commandArgs.push("--from-typespec=true"); + commandArgs["from-typespec"] = "true"; if (!program.compilerOptions.noEmit && !program.hasError()) { - execSync(commandArgs.join(" ")); + if (resolvedOptions["use-pyodide"]) { + // here we run with pyodide + const outputFolder = path.relative(root, outputDir); + const pyodide = await setupPyodideCall(root, outputFolder); + const yamlRelativePath = path.relative(root, yamlPath); + const globals = pyodide.toPy({ outputFolder, yamlRelativePath, commandArgs }); + const pythonCode = ` + async def main(): + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore", SyntaxWarning) # bc of m2r2 dep issues + 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()`; + await pyodide.runPythonAsync(pythonCode, { globals }); + } 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."); + } + commandArgs["output-folder"] = outputDir; + commandArgs["cadl-file"] = yamlPath; + await exec(Object.entries(commandArgs).map(([key, value]) => `--${key} ${value}`).join(" ")); + } } } + +async function setupPyodideCall(root: string, outputFolder: string) { + + 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"); + return pyodide; + +} + diff --git a/packages/http-client-python/emitter/src/lib.ts b/packages/http-client-python/emitter/src/lib.ts index c79e0ffdf2..b028aaeadc 100644 --- a/packages/http-client-python/emitter/src/lib.ts +++ b/packages/http-client-python/emitter/src/lib.ts @@ -17,6 +17,7 @@ export interface PythonEmitterOptions { debug?: boolean; flavor?: "azure"; "examples-dir"?: string; + "use-pyodide"?: boolean; } export interface PythonSdkContext @@ -43,6 +44,7 @@ const EmitterOptionsSchema: JSONSchemaType = { 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: [], }; diff --git a/packages/http-client-python/emitter/src/run-python3.ts b/packages/http-client-python/emitter/src/run-python3.ts new file mode 100644 index 0000000000..e4df6e8a71 --- /dev/null +++ b/packages/http-client-python/emitter/src/run-python3.ts @@ -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], + }); +} diff --git a/packages/http-client-python/emitter/src/system-requirements.ts b/packages/http-client-python/emitter/src/system-requirements.ts new file mode 100644 index 0000000000..7f12ff5b5a --- /dev/null +++ b/packages/http-client-python/emitter/src/system-requirements.ts @@ -0,0 +1,261 @@ +import { ChildProcess, spawn, SpawnOptions } from "child_process"; +import { coerce, satisfies } from "semver"; + +/* + * Copied from @autorest/system-requirements + */ + +const execute = ( + command: string, + cmdlineargs: Array, + options: MoreOptions = {}, +): Promise => { + return new Promise((resolve, reject) => { + const cp = spawn(command, cmdlineargs, { ...options, stdio: "pipe", shell: true }); + if (options.onCreate) { + options.onCreate(cp); + } + + options.onStdOutData && cp.stdout.on("data", options.onStdOutData); + options.onStdErrData && cp.stderr.on("data", options.onStdErrData); + + let err = ""; + let out = ""; + let all = ""; + cp.stderr.on("data", (chunk) => { + err += chunk; + all += chunk; + }); + cp.stdout.on("data", (chunk) => { + out += chunk; + all += chunk; + }); + + cp.on("error", (err) => { + reject(err); + }); + cp.on("close", (code, signal) => + resolve({ + stdout: out, + stderr: err, + log: all, + error: code ? new Error("Process Failed.") : null, + code, + }), + ); + }); +}; + +const versionIsSatisfied = (version: string, requirement: string): boolean => { + const cleanedVersion = coerce(version); + if (!cleanedVersion) { + throw new Error(`Invalid version ${version}.`); + } + return satisfies(cleanedVersion, requirement, true); +}; + +/** + * Validate the provided system requirement resolution is satisfying the version requirement if applicable. + * @param resolution Command resolution. + * @param actualVersion Version for that resolution. + * @param requirement Requirement. + * @returns the resolution if it is valid or an @see SystemRequirementError if not. + */ +const validateVersionRequirement = ( + resolution: SystemRequirementResolution, + actualVersion: string, + requirement: SystemRequirement, +): SystemRequirementResolution | SystemRequirementError => { + if (!requirement.version) { + return resolution; // No version requirement. + } + + try { + if (versionIsSatisfied(actualVersion, requirement.version)) { + return resolution; + } + return { + ...resolution, + error: true, + message: `'${resolution.command}' version is '${actualVersion}' but doesn't satisfy requirement '${requirement.version}'. Please update.`, + actualVersion: actualVersion, + neededVersion: requirement.version, + }; + } catch { + return { + ...resolution, + error: true, + message: `Couldn't parse the version ${actualVersion}. This is not a valid semver version.`, + actualVersion: actualVersion, + neededVersion: requirement.version, + }; + } +}; + +const tryPython = async ( + requirement: SystemRequirement, + command: string, + additionalArgs: string[] = [], +): Promise => { + const resolution: SystemRequirementResolution = { + name: PythonRequirement, + command, + additionalArgs: additionalArgs.length > 0 ? additionalArgs : undefined, + }; + + try { + const result = await execute(command, [ + ...additionalArgs, + "-c", + `"${PRINT_PYTHON_VERSION_SCRIPT}"`, + ]); + return validateVersionRequirement(resolution, result.stdout.trim(), requirement); + } catch (e) { + return { + error: true, + ...resolution, + message: `'${command}' command line is not found in the path. Make sure to have it installed.`, + }; + } +}; + +/** + * Returns the path to the executable as asked in the requirement. + * @param requirement System requirement definition. + * @returns If the requirement provide an environment variable for the path returns the value of that environment variable. undefined otherwise. + */ +const getExecutablePath = (requirement: SystemRequirement): string | undefined => + requirement.environmentVariable && process.env[requirement.environmentVariable]; + +const createPythonErrorMessage = ( + requirement: SystemRequirement, + errors: SystemRequirementError[], +): SystemRequirementError => { + const versionReq = requirement.version ?? "*"; + const lines = [ + `Couldn't find a valid python interpreter satisfying the requirement (version: ${versionReq}). Tried:`, + ...errors.map((x) => ` - ${x.command} (${x.message})`), + ]; + + return { + error: true, + name: "python", + command: "python", + message: lines.join("\n"), + }; +}; + +const resolvePythonRequirement = async ( + requirement: SystemRequirement, +): Promise => { + // Hardcoding AUTOREST_PYTHON_EXE is for backward compatibility + const path = getExecutablePath(requirement) ?? process.env["AUTOREST_PYTHON_EXE"]; + if (path) { + return await tryPython(requirement, path); + } + + const errors: SystemRequirementError[] = []; + // On windows try `py` executable with `-3` flag. + if (process.platform === "win32") { + const pyResult = await tryPython(requirement, "py", ["-3"]); + if ("error" in pyResult) { + errors.push(pyResult); + } else { + return pyResult; + } + } + + const python3Result = await tryPython(requirement, "python3"); + if ("error" in python3Result) { + errors.push(python3Result); + } else { + return python3Result; + } + + const pythonResult = await tryPython(requirement, "python"); + if ("error" in pythonResult) { + errors.push(pythonResult); + } else { + return pythonResult; + } + + return createPythonErrorMessage(requirement, errors); +}; + +/** + * @param command list of the command and arguments. First item in array must be a python exe @see KnownPythonExe. (e.g. ["python", "my_python_file.py"] + * @param requirement + */ +export const patchPythonPath = async ( + command: PythonCommandLine, + requirement: SystemRequirement, +): Promise => { + const [_, ...args] = command; + const resolution = await resolvePythonRequirement(requirement); + if ("error" in resolution) { + throw new Error(`Failed to find compatible python version. ${resolution.message}`); + } + return [resolution.command, ...(resolution.additionalArgs ?? []), ...args]; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// TYPES +const PythonRequirement = "python"; +const PRINT_PYTHON_VERSION_SCRIPT = "import sys; print('.'.join(map(str, sys.version_info[:3])))"; + +type KnownPythonExe = "python.exe" | "python3.exe" | "python" | "python3"; +type PythonCommandLine = [KnownPythonExe, ...string[]]; + +interface MoreOptions extends SpawnOptions { + onCreate?(cp: ChildProcess): void; + onStdOutData?(chunk: any): void; + onStdErrData?(chunk: any): void; +} + +interface SystemRequirement { + version?: string; + /** + * Name of an environment variable where the user could provide the path to the exe. + * @example "AUTOREST_PYTHON_PATH" + */ + environmentVariable?: string; +} + +interface SystemRequirementResolution { + /** + * Name of the requirement. + * @example python, java, etc. + */ + name: string; + + /** + * Name of the command + * @example python3, /home/my_user/python39/python, java, etc. + */ + command: string; + + /** + * List of additional arguments to pass to this command. + * @example '-3' for 'py' to specify to use python 3 + */ + additionalArgs?: string[]; +} + +interface ExecResult { + stdout: string; + stderr: string; + + /** + * Union of stdout and stderr. + */ + log: string; + error: Error | null; + code: number | null; +} + +interface SystemRequirementError extends SystemRequirementResolution { + error: true; + message: string; + neededVersion?: string; + actualVersion?: string; +} diff --git a/packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 b/packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 new file mode 100644 index 0000000000..c56126bd74 --- /dev/null +++ b/packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 @@ -0,0 +1,11 @@ +#Requires -Version 7.0 + +Import-Module "$PSScriptRoot\Generation.psm1" -DisableNameChecking -Force; + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..') + +Write-Host "Building project ..." +& npm run build + +Write-Host "Regenerating project with Pyodide ..." +& npm run regenerate -- --pyodide diff --git a/packages/http-client-python/eng/scripts/Test-Packages.ps1 b/packages/http-client-python/eng/scripts/Test-Packages.ps1 index 0df7f33952..3b604f2792 100644 --- a/packages/http-client-python/eng/scripts/Test-Packages.ps1 +++ b/packages/http-client-python/eng/scripts/Test-Packages.ps1 @@ -34,7 +34,14 @@ try { Set-StrictMode -Version 1 # run E2E Test for TypeSpec emitter - Write-Host "Generating test projects ..." + Write-Host "Generating test projects with pyodide ..." + & "$packageRoot/eng/scripts/Generate-WithPyodide.ps1" + Write-Host 'Code generation is completed.' + + # force add updates + Invoke-LoggedCommand "git add $packageRoot/generator/test -f" + + Write-Host "Generating test projects with venv ..." & "$packageRoot/eng/scripts/Generate.ps1" Write-Host 'Code generation is completed.' @@ -44,9 +51,12 @@ try { Write-Host 'Done. No code generation differences detected.' } catch { - Write-Error 'Generated code is not up to date. Please run: eng/Generate.ps1' + Write-Error 'Generated code is not up to date. Please run: eng/scripts/Generate.ps1' } + # reset force updates + Invoke-LoggedCommand "git reset ." + try { Write-Host "Pip List" & pip list diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 629c29c411..5fb9df459c 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -16,6 +16,7 @@ const argv = parseArgs({ pluginDir: { type: "string" }, emitterName: { type: "string" }, generatedFolder: { type: "string" }, + usePyodide: { type: "boolean" }, }, }); @@ -158,12 +159,14 @@ interface RegenerateFlagsInput { flavor?: string; debug?: boolean; name?: string; + pyodide?: boolean; } interface RegenerateFlags { flavor: string; debug: boolean; name?: string; + pyodide?: boolean; } const SpecialFlags: Record> = { @@ -242,6 +245,9 @@ function addOptions( const emitterConfigs: EmitterConfig[] = []; for (const config of getEmitterOption(spec)) { const options: Record = { ...config }; + if (flags.pyodide) { + options["use-pyodide"] = "true"; + } options["flavor"] = flags.flavor; for (const [k, v] of Object.entries(SpecialFlags[flags.flavor] ?? {})) { options[k] = v; diff --git a/packages/http-client-python/eng/scripts/setup/build.ts b/packages/http-client-python/eng/scripts/setup/build.ts new file mode 100644 index 0000000000..2ed768f472 --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/build.ts @@ -0,0 +1,14 @@ +import { exec } from "child_process"; + +// Define the command you want to run +const command = + "tsx ./eng/scripts/setup/run-python3.ts ./eng/scripts/setup/build_pygen_wheel.py && rimraf ./venv_build_wheel"; + +// Execute the command +exec(command, (error, stdout, stderr) => { + if (error) { + console.error(`Error executing command: ${error.message}`); // eslint-disable-line no-console + return; + } + console.log(`Command output:\n${stdout}`); // eslint-disable-line no-console +}); diff --git a/packages/http-client-python/eng/scripts/setup/build_pygen_wheel.py b/packages/http-client-python/eng/scripts/setup/build_pygen_wheel.py new file mode 100644 index 0000000000..79f85e086a --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/build_pygen_wheel.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import sys + +if not sys.version_info >= (3, 8, 0): + raise Exception("Autorest for Python extension requires Python 3.8 at least") + +try: + import pip +except ImportError: + raise Exception("Your Python installation doesn't have pip available") + + +# Now we have pip and Py >= 3.8, go to work + +from pathlib import Path + +from venvtools import ExtendedEnvBuilder, python_run + +_ROOT_DIR = Path(__file__).parent.parent.parent.parent + + +def main(): + venv_path = _ROOT_DIR / "venv_build_wheel" + env_builder = ExtendedEnvBuilder(with_pip=True, upgrade_deps=True) + env_builder.create(venv_path) + venv_context = env_builder.context + + python_run(venv_context, "pip", ["install", "-U", "pip"]) + python_run(venv_context, "pip", ["install", "build"]) + python_run(venv_context, "build", ["--wheel"], additional_dir="generator") + + +if __name__ == "__main__": + main() diff --git a/packages/http-client-python/eng/scripts/setup/install.py b/packages/http-client-python/eng/scripts/setup/install.py index 8b10f58bde..fba572f5a8 100644 --- a/packages/http-client-python/eng/scripts/setup/install.py +++ b/packages/http-client-python/eng/scripts/setup/install.py @@ -8,17 +8,17 @@ import sys if not sys.version_info >= (3, 8, 0): - raise Exception("Autorest for Python extension requires Python 3.8 at least") + raise Warning("Autorest for Python extension requires Python 3.8 at least. We will run your code with Pyodide since your Python version isn't adequate.") try: import pip except ImportError: - raise Exception("Your Python installation doesn't have pip available") + raise Warning("Your Python installation doesn't have pip available. We will run your code with Pyodide since your Python version isn't adequate.") try: import venv except ImportError: - raise Exception("Your Python installation doesn't have venv available") + raise Warning("Your Python installation doesn't have venv available. We will run your code with Pyodide since your Python version isn't adequate.") # Now we have pip and Py >= 3.8, go to work diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts new file mode 100644 index 0000000000..2662c38773 --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -0,0 +1,27 @@ +import path, { dirname } from "path"; +import { loadPyodide } from "pyodide"; +import { fileURLToPath } from "url"; + +async function installPyodideDeps() { + const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); + await pyodide.loadPackage("micropip"); + const micropip = pyodide.pyimport("micropip"); + await micropip.install([ + "black", + "click", + "docutils==0.21.2", + "Jinja2==3.1.4", + "m2r2==0.3.3.post2", + "MarkupSafe", + "pathspec", + "platformdirs", + "pyyaml", + "tomli", + "setuptools", + ]); +} + +installPyodideDeps() + .then(() => console.log("Successfully installed all required Python packages in Pyodide!")) // eslint-disable-line no-console + .catch((error) => console.error(`Installation in Pyodide failed: ${error.message}`)); // eslint-disable-line no-console diff --git a/packages/http-client-python/eng/scripts/setup/prepare.py b/packages/http-client-python/eng/scripts/setup/prepare.py index 4c2c46bc03..99d1cc915f 100644 --- a/packages/http-client-python/eng/scripts/setup/prepare.py +++ b/packages/http-client-python/eng/scripts/setup/prepare.py @@ -10,7 +10,7 @@ import argparse if not sys.version_info >= (3, 8, 0): - raise Exception("Autorest for Python extension requires Python 3.8 at least") + raise Warning("Autorest for Python extension requires Python 3.8 at least. We will run your code with Pyodide since your Python version isn't adequate.") from pathlib import Path import venv diff --git a/packages/http-client-python/generator/requirements.txt b/packages/http-client-python/generator/requirements.txt index bebbbb5a64..d112c8465c 100644 --- a/packages/http-client-python/generator/requirements.txt +++ b/packages/http-client-python/generator/requirements.txt @@ -1,12 +1,12 @@ -black==24.4.0 -click==8.1.3 -docutils==0.19 +black==24.8.0 +click==8.1.7 +docutils==0.20.1 Jinja2==3.1.4 -m2r2==0.3.3 -MarkupSafe==2.1.2 +m2r2==0.3.3.post2 +MarkupSafe==2.1.5 mistune==0.8.4 -pathspec==0.11.1 -platformdirs==3.2.0 +pathspec==0.12.1 +platformdirs==4.3.6 PyYAML==6.0.1 tomli==2.0.1 -setuptools==69.2.0 +setuptools==69.5.1 diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 34627e8a8d..c22844253e 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -30,12 +30,12 @@ }, "scripts": { "clean": "rimraf ./dist ./temp ./emitter/temp ./generator/test/azure/generated ./generator/test/unbranded/generated ./venv", - "build": "tsc -p ./emitter/tsconfig.build.json", + "build": "tsc -p ./emitter/tsconfig.build.json && tsx ./eng/scripts/setup/build.ts", "watch": "tsc -p ./emitter/tsconfig.build.json --watch", "lint": "eslint . --max-warnings=0", "lint:py": "tsx ./eng/scripts/ci/lint.ts --folderName generator", "format": "pnpm -w format:dir packages/http-client-python && tsx ./eng/scripts/ci/format.ts", - "install": "tsx ./eng/scripts/setup/run-python3.ts ./eng/scripts/setup/install.py", + "install": "tsx ./eng/scripts/setup/run-python3.ts ./eng/scripts/setup/install.py && tsx ./eng/scripts/setup/install.ts", "prepare": "tsx ./eng/scripts/setup/run-python3.ts ./eng/scripts/setup/prepare.py", "regenerate": "tsx ./eng/scripts/ci/regenerate.ts", "ci": "npm run test:emitter && npm run ci:generator --", @@ -65,7 +65,8 @@ "dependencies": { "js-yaml": "~4.1.0", "semver": "~7.6.2", - "tsx": "~4.19.1" + "tsx": "~4.19.1", + "pyodide": "0.26.2" }, "devDependencies": { "@typespec/compiler": "~0.62.0", From 7e4e3a2dc9695304385d03d4d40c8daf24d7973e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 6 Dec 2024 16:15:13 -0500 Subject: [PATCH 02/39] run with pyodide if user doesn't have venv --- packages/http-client-python/emitter/src/emitter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index a8838a82ff..0e2c31a528 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -126,8 +126,8 @@ export async function $onEmit(context: EmitContext) { } commandArgs["from-typespec"] = "true"; if (!program.compilerOptions.noEmit && !program.hasError()) { - if (resolvedOptions["use-pyodide"]) { - // here we run with pyodide + if (resolvedOptions["use-pyodide"] || !fs.existsSync(venvPath)) { + // here we run with pyodide, if there's no venv or if the user specifies to use pyodide const outputFolder = path.relative(root, outputDir); const pyodide = await setupPyodideCall(root, outputFolder); const yamlRelativePath = path.relative(root, yamlPath); From 208dd376c5e23f82f41a9acf10908681609666b1 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 6 Dec 2024 16:17:30 -0500 Subject: [PATCH 03/39] update changelog and package.json --- packages/http-client-python/CHANGELOG.md | 6 ++++++ packages/http-client-python/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/CHANGELOG.md b/packages/http-client-python/CHANGELOG.md index 8248785288..75a1513434 100644 --- a/packages/http-client-python/CHANGELOG.md +++ b/packages/http-client-python/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log - @typespec/http-client-python +## 0.4.0 + +### Features + +- Add support for generation using `pyodide` instead of a local python environment + ## 0.3.12 ### Other Changes diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index c22844253e..89fda17fd7 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/http-client-python", - "version": "0.3.12", + "version": "0.4.0", "author": "Microsoft Corporation", "description": "TypeSpec emitter for Python SDKs", "homepage": "https://typespec.io", From 6332813874951fde7cc7fd23194786dd18910651 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 9 Dec 2024 11:09:02 -0500 Subject: [PATCH 04/39] format --- .../http-client-python/emitter/src/emitter.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 0e2c31a528..a3466d5ec1 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -98,7 +98,7 @@ export async function $onEmit(context: EmitContext) { throw new Error("Virtual environment doesn't exist."); } const resolvedOptions = sdkContext.emitContext.options; - const commandArgs: Record = {} + const commandArgs: Record = {}; if (resolvedOptions["packaging-files-config"]) { const keyValuePairs = Object.entries(resolvedOptions["packaging-files-config"]).map( ([key, value]) => { @@ -144,7 +144,7 @@ export async function $onEmit(context: EmitContext) { black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process() await main()`; - await pyodide.runPythonAsync(pythonCode, { globals }); + await pyodide.runPythonAsync(pythonCode, { globals }); } else { let venvPath = path.join(root, "venv"); if (!fs.existsSync(venvPath)) { @@ -160,25 +160,26 @@ export async function $onEmit(context: EmitContext) { } commandArgs["output-folder"] = outputDir; commandArgs["cadl-file"] = yamlPath; - await exec(Object.entries(commandArgs).map(([key, value]) => `--${key} ${value}`).join(" ")); + await exec( + Object.entries(commandArgs) + .map(([key, value]) => `--${key} ${value}`) + .join(" "), + ); } } } async function setupPyodideCall(root: string, outputFolder: string) { - 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"); + 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"); return pyodide; - } - From c2b721b50c2a9769de11a9eee10904cf0e3ad8dd Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 9 Dec 2024 11:33:15 -0500 Subject: [PATCH 05/39] wrap calls to install python deps in a try catch --- .../http-client-python/emitter/src/emitter.ts | 2 +- .../emitter/src/run-python3.ts | 20 -- .../emitter/src/system-requirements.ts | 261 ------------------ .../http-client-python/emitter/tsconfig.json | 2 +- .../eng/scripts/setup/install.ts | 15 +- .../eng/scripts/setup/prepare.ts | 13 + .../eng/scripts/setup/run-python3.ts | 2 +- packages/http-client-python/package-lock.json | 36 ++- packages/http-client-python/package.json | 4 +- 9 files changed, 66 insertions(+), 289 deletions(-) delete mode 100644 packages/http-client-python/emitter/src/run-python3.ts delete mode 100644 packages/http-client-python/emitter/src/system-requirements.ts create mode 100644 packages/http-client-python/eng/scripts/setup/prepare.ts diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index a3466d5ec1..0ffc3bee13 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -13,8 +13,8 @@ import { fileURLToPath } from "url"; import { emitCodeModel } from "./code-model.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js"; -import { runPython3 } from "./run-python3.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; +import { runPython3 } from "../../eng/scripts/setup/run-python3.js"; export function getModelsMode(context: SdkContext): "dpg" | "none" { const specifiedModelsMode = context.emitContext.options["models-mode"]; diff --git a/packages/http-client-python/emitter/src/run-python3.ts b/packages/http-client-python/emitter/src/run-python3.ts deleted file mode 100644 index e4df6e8a71..0000000000 --- a/packages/http-client-python/emitter/src/run-python3.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 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], - }); -} diff --git a/packages/http-client-python/emitter/src/system-requirements.ts b/packages/http-client-python/emitter/src/system-requirements.ts deleted file mode 100644 index 7f12ff5b5a..0000000000 --- a/packages/http-client-python/emitter/src/system-requirements.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { ChildProcess, spawn, SpawnOptions } from "child_process"; -import { coerce, satisfies } from "semver"; - -/* - * Copied from @autorest/system-requirements - */ - -const execute = ( - command: string, - cmdlineargs: Array, - options: MoreOptions = {}, -): Promise => { - return new Promise((resolve, reject) => { - const cp = spawn(command, cmdlineargs, { ...options, stdio: "pipe", shell: true }); - if (options.onCreate) { - options.onCreate(cp); - } - - options.onStdOutData && cp.stdout.on("data", options.onStdOutData); - options.onStdErrData && cp.stderr.on("data", options.onStdErrData); - - let err = ""; - let out = ""; - let all = ""; - cp.stderr.on("data", (chunk) => { - err += chunk; - all += chunk; - }); - cp.stdout.on("data", (chunk) => { - out += chunk; - all += chunk; - }); - - cp.on("error", (err) => { - reject(err); - }); - cp.on("close", (code, signal) => - resolve({ - stdout: out, - stderr: err, - log: all, - error: code ? new Error("Process Failed.") : null, - code, - }), - ); - }); -}; - -const versionIsSatisfied = (version: string, requirement: string): boolean => { - const cleanedVersion = coerce(version); - if (!cleanedVersion) { - throw new Error(`Invalid version ${version}.`); - } - return satisfies(cleanedVersion, requirement, true); -}; - -/** - * Validate the provided system requirement resolution is satisfying the version requirement if applicable. - * @param resolution Command resolution. - * @param actualVersion Version for that resolution. - * @param requirement Requirement. - * @returns the resolution if it is valid or an @see SystemRequirementError if not. - */ -const validateVersionRequirement = ( - resolution: SystemRequirementResolution, - actualVersion: string, - requirement: SystemRequirement, -): SystemRequirementResolution | SystemRequirementError => { - if (!requirement.version) { - return resolution; // No version requirement. - } - - try { - if (versionIsSatisfied(actualVersion, requirement.version)) { - return resolution; - } - return { - ...resolution, - error: true, - message: `'${resolution.command}' version is '${actualVersion}' but doesn't satisfy requirement '${requirement.version}'. Please update.`, - actualVersion: actualVersion, - neededVersion: requirement.version, - }; - } catch { - return { - ...resolution, - error: true, - message: `Couldn't parse the version ${actualVersion}. This is not a valid semver version.`, - actualVersion: actualVersion, - neededVersion: requirement.version, - }; - } -}; - -const tryPython = async ( - requirement: SystemRequirement, - command: string, - additionalArgs: string[] = [], -): Promise => { - const resolution: SystemRequirementResolution = { - name: PythonRequirement, - command, - additionalArgs: additionalArgs.length > 0 ? additionalArgs : undefined, - }; - - try { - const result = await execute(command, [ - ...additionalArgs, - "-c", - `"${PRINT_PYTHON_VERSION_SCRIPT}"`, - ]); - return validateVersionRequirement(resolution, result.stdout.trim(), requirement); - } catch (e) { - return { - error: true, - ...resolution, - message: `'${command}' command line is not found in the path. Make sure to have it installed.`, - }; - } -}; - -/** - * Returns the path to the executable as asked in the requirement. - * @param requirement System requirement definition. - * @returns If the requirement provide an environment variable for the path returns the value of that environment variable. undefined otherwise. - */ -const getExecutablePath = (requirement: SystemRequirement): string | undefined => - requirement.environmentVariable && process.env[requirement.environmentVariable]; - -const createPythonErrorMessage = ( - requirement: SystemRequirement, - errors: SystemRequirementError[], -): SystemRequirementError => { - const versionReq = requirement.version ?? "*"; - const lines = [ - `Couldn't find a valid python interpreter satisfying the requirement (version: ${versionReq}). Tried:`, - ...errors.map((x) => ` - ${x.command} (${x.message})`), - ]; - - return { - error: true, - name: "python", - command: "python", - message: lines.join("\n"), - }; -}; - -const resolvePythonRequirement = async ( - requirement: SystemRequirement, -): Promise => { - // Hardcoding AUTOREST_PYTHON_EXE is for backward compatibility - const path = getExecutablePath(requirement) ?? process.env["AUTOREST_PYTHON_EXE"]; - if (path) { - return await tryPython(requirement, path); - } - - const errors: SystemRequirementError[] = []; - // On windows try `py` executable with `-3` flag. - if (process.platform === "win32") { - const pyResult = await tryPython(requirement, "py", ["-3"]); - if ("error" in pyResult) { - errors.push(pyResult); - } else { - return pyResult; - } - } - - const python3Result = await tryPython(requirement, "python3"); - if ("error" in python3Result) { - errors.push(python3Result); - } else { - return python3Result; - } - - const pythonResult = await tryPython(requirement, "python"); - if ("error" in pythonResult) { - errors.push(pythonResult); - } else { - return pythonResult; - } - - return createPythonErrorMessage(requirement, errors); -}; - -/** - * @param command list of the command and arguments. First item in array must be a python exe @see KnownPythonExe. (e.g. ["python", "my_python_file.py"] - * @param requirement - */ -export const patchPythonPath = async ( - command: PythonCommandLine, - requirement: SystemRequirement, -): Promise => { - const [_, ...args] = command; - const resolution = await resolvePythonRequirement(requirement); - if ("error" in resolution) { - throw new Error(`Failed to find compatible python version. ${resolution.message}`); - } - return [resolution.command, ...(resolution.additionalArgs ?? []), ...args]; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// TYPES -const PythonRequirement = "python"; -const PRINT_PYTHON_VERSION_SCRIPT = "import sys; print('.'.join(map(str, sys.version_info[:3])))"; - -type KnownPythonExe = "python.exe" | "python3.exe" | "python" | "python3"; -type PythonCommandLine = [KnownPythonExe, ...string[]]; - -interface MoreOptions extends SpawnOptions { - onCreate?(cp: ChildProcess): void; - onStdOutData?(chunk: any): void; - onStdErrData?(chunk: any): void; -} - -interface SystemRequirement { - version?: string; - /** - * Name of an environment variable where the user could provide the path to the exe. - * @example "AUTOREST_PYTHON_PATH" - */ - environmentVariable?: string; -} - -interface SystemRequirementResolution { - /** - * Name of the requirement. - * @example python, java, etc. - */ - name: string; - - /** - * Name of the command - * @example python3, /home/my_user/python39/python, java, etc. - */ - command: string; - - /** - * List of additional arguments to pass to this command. - * @example '-3' for 'py' to specify to use python 3 - */ - additionalArgs?: string[]; -} - -interface ExecResult { - stdout: string; - stderr: string; - - /** - * Union of stdout and stderr. - */ - log: string; - error: Error | null; - code: number | null; -} - -interface SystemRequirementError extends SystemRequirementResolution { - error: true; - message: string; - neededVersion?: string; - actualVersion?: string; -} diff --git a/packages/http-client-python/emitter/tsconfig.json b/packages/http-client-python/emitter/tsconfig.json index 32cb2aff3f..d9fd71175a 100644 --- a/packages/http-client-python/emitter/tsconfig.json +++ b/packages/http-client-python/emitter/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": ["src/**/*"] + "include": ["src/**/*", "../eng/scripts/setup/*"] } diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 2662c38773..61d90e2890 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -1,6 +1,15 @@ import path, { dirname } from "path"; import { loadPyodide } from "pyodide"; import { fileURLToPath } from "url"; +import { runPython3 } from "./run-python3.js"; + +async function installPythonDeps() { + try { + await runPython3("./eng/scripts/setup/install.py"); + } catch (error) { + console.log("No Python installation found. Skipping Python dependencies installation."); // eslint-disable-line no-console + } +} async function installPyodideDeps() { const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); @@ -22,6 +31,10 @@ async function installPyodideDeps() { ]); } +installPythonDeps() + .then(() => console.log("Successfully installed all required Python packages")) // eslint-disable-line no-console + .catch((error) => console.error(`Installation failed: ${error.message}`)); // eslint-disable-line no-console + installPyodideDeps() - .then(() => console.log("Successfully installed all required Python packages in Pyodide!")) // eslint-disable-line no-console + .then(() => console.log("Successfully installed all required Python packages in Pyodide")) // eslint-disable-line no-console .catch((error) => console.error(`Installation in Pyodide failed: ${error.message}`)); // eslint-disable-line no-console diff --git a/packages/http-client-python/eng/scripts/setup/prepare.ts b/packages/http-client-python/eng/scripts/setup/prepare.ts new file mode 100644 index 0000000000..ac1d1824fe --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/prepare.ts @@ -0,0 +1,13 @@ +import { runPython3 } from "./run-python3.js"; + +async function preparePythonDeps() { + try { + await runPython3("./eng/scripts/setup/prepare.py"); + } catch (error) { + console.log("No Python installation found. Skipping Python dependencies preparation."); // eslint-disable-line no-console + } +} + +preparePythonDeps() + .then(() => console.log("Successfully prepared all required Python packages")) // eslint-disable-line no-console + .catch((error) => console.error(`Preparation failed: ${error.message}`)); // eslint-disable-line no-console diff --git a/packages/http-client-python/eng/scripts/setup/run-python3.ts b/packages/http-client-python/eng/scripts/setup/run-python3.ts index 6de4922a40..1cd75a1508 100644 --- a/packages/http-client-python/eng/scripts/setup/run-python3.ts +++ b/packages/http-client-python/eng/scripts/setup/run-python3.ts @@ -9,7 +9,7 @@ import cp from "child_process"; import { patchPythonPath } from "./system-requirements.js"; -async function runPython3(...args: string[]) { +export async function runPython3(...args: string[]) { const command = await patchPythonPath(["python", ...args], { version: ">=3.8", environmentVariable: "AUTOREST_PYTHON_EXE", diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index aa6c583052..f6229b91c4 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -1,16 +1,17 @@ { "name": "@typespec/http-client-python", - "version": "0.4.2", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@typespec/http-client-python", - "version": "0.4.2", + "version": "0.5.0", "hasInstallScript": true, "license": "MIT", "dependencies": { "js-yaml": "~4.1.0", + "pyodide": "0.26.2", "semver": "~7.6.2", "tsx": "~4.19.1" }, @@ -5931,6 +5932,17 @@ "node": ">=6" } }, + "node_modules/pyodide": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.2.tgz", + "integrity": "sha512-8VCRdFX83gBsWs6XP2rhG8HMaB+JaVyyav4q/EMzoV8fXH8HN6T5IISC92SNma6i1DRA3SVXA61S1rJcB8efgA==", + "dependencies": { + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -7565,6 +7577,26 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-formatter": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.6.3.tgz", diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index daf94672f4..a732791850 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -35,8 +35,8 @@ "lint": "eslint . --max-warnings=0", "lint:py": "tsx ./eng/scripts/ci/lint.ts --folderName generator", "format": "pnpm -w format:dir packages/http-client-python && tsx ./eng/scripts/ci/format.ts", - "install": "tsx ./eng/scripts/setup/run-python3.ts ./eng/scripts/setup/install.py && tsx ./eng/scripts/setup/install.ts", - "prepare": "tsx ./eng/scripts/setup/run-python3.ts ./eng/scripts/setup/prepare.py", + "install": "tsx ./eng/scripts/setup/install.ts", + "prepare": "tsx ./eng/scripts/setup/prepare.ts", "regenerate": "tsx ./eng/scripts/ci/regenerate.ts", "ci": "npm run test:emitter && npm run ci:generator --", "ci:generator": "tsx ./eng/scripts/ci/run-ci.ts", From f1c28a87385412722b38b57fd1510cb38439cd60 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 9 Dec 2024 11:38:19 -0500 Subject: [PATCH 06/39] black formatting on generator code --- .../http-client-python/emitter/src/emitter.ts | 2 +- .../eng/scripts/setup/install.py | 12 ++++++--- .../eng/scripts/setup/prepare.py | 4 ++- .../generator/pygen/codegen/__init__.py | 13 ++++++--- .../pygen/codegen/models/operation.py | 8 ++++-- .../codegen/serializers/builder_serializer.py | 27 ++++++++++++------- .../test_azure_arm_resource_async.py | 4 +-- .../mock_api_tests/test_azure_arm_resource.py | 4 +-- 8 files changed, 49 insertions(+), 25 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 0ffc3bee13..06f10ca23c 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -10,11 +10,11 @@ import fs from "fs"; import path, { dirname } from "path"; import { loadPyodide } from "pyodide"; import { fileURLToPath } from "url"; +import { runPython3 } from "../../eng/scripts/setup/run-python3.js"; import { emitCodeModel } from "./code-model.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; -import { runPython3 } from "../../eng/scripts/setup/run-python3.js"; export function getModelsMode(context: SdkContext): "dpg" | "none" { const specifiedModelsMode = context.emitContext.options["models-mode"]; diff --git a/packages/http-client-python/eng/scripts/setup/install.py b/packages/http-client-python/eng/scripts/setup/install.py index fba572f5a8..d6bd0d9e07 100644 --- a/packages/http-client-python/eng/scripts/setup/install.py +++ b/packages/http-client-python/eng/scripts/setup/install.py @@ -8,17 +8,23 @@ import sys if not sys.version_info >= (3, 8, 0): - raise Warning("Autorest for Python extension requires Python 3.8 at least. We will run your code with Pyodide since your Python version isn't adequate.") + raise Warning( + "Autorest for Python extension requires Python 3.8 at least. We will run your code with Pyodide since your Python version isn't adequate." + ) try: import pip except ImportError: - raise Warning("Your Python installation doesn't have pip available. We will run your code with Pyodide since your Python version isn't adequate.") + raise Warning( + "Your Python installation doesn't have pip available. We will run your code with Pyodide since your Python version isn't adequate." + ) try: import venv except ImportError: - raise Warning("Your Python installation doesn't have venv available. We will run your code with Pyodide since your Python version isn't adequate.") + raise Warning( + "Your Python installation doesn't have venv available. We will run your code with Pyodide since your Python version isn't adequate." + ) # Now we have pip and Py >= 3.8, go to work diff --git a/packages/http-client-python/eng/scripts/setup/prepare.py b/packages/http-client-python/eng/scripts/setup/prepare.py index 99d1cc915f..4e5ffd432f 100644 --- a/packages/http-client-python/eng/scripts/setup/prepare.py +++ b/packages/http-client-python/eng/scripts/setup/prepare.py @@ -10,7 +10,9 @@ import argparse if not sys.version_info >= (3, 8, 0): - raise Warning("Autorest for Python extension requires Python 3.8 at least. We will run your code with Pyodide since your Python version isn't adequate.") + raise Warning( + "Autorest for Python extension requires Python 3.8 at least. We will run your code with Pyodide since your Python version isn't adequate." + ) from pathlib import Path import venv diff --git a/packages/http-client-python/generator/pygen/codegen/__init__.py b/packages/http-client-python/generator/pygen/codegen/__init__.py index 1ab9bd6237..8a08e8e6a2 100644 --- a/packages/http-client-python/generator/pygen/codegen/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/__init__.py @@ -249,8 +249,15 @@ def sort_exceptions(yaml_data: Dict[str, Any]) -> None: if not operation.get("exceptions"): continue # sort exceptions by status code, first single status code, then range, then default - operation["exceptions"] = sorted(operation["exceptions"], key=lambda x: 3 if x["statusCodes"][0] == "default" else (1 if isinstance(x["statusCodes"][0], int) else 2)) - + operation["exceptions"] = sorted( + operation["exceptions"], + key=lambda x: ( + 3 + if x["statusCodes"][0] == "default" + else (1 if isinstance(x["statusCodes"][0], int) else 2) + ), + ) + @staticmethod def remove_cloud_errors(yaml_data: Dict[str, Any]) -> None: for client in yaml_data["clients"]: @@ -325,7 +332,7 @@ def process(self) -> bool: self._validate_code_model_options() options = self._build_code_model_options() yaml_data = self.get_yaml() - + self.sort_exceptions(yaml_data) if self.options_retriever.azure_arm: diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index 22aed0fdb2..ce4f1ac25b 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -206,7 +206,9 @@ def default_error_deserialization(self) -> Optional[str]: @property def non_default_errors(self) -> List[Response]: - return [e for e in self.exceptions if "default" not in e.status_codes and e.type and isinstance(e.type, ModelType)] + return [ + e for e in self.exceptions if "default" not in e.status_codes and e.type and isinstance(e.type, ModelType) + ] def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: # pylint: disable=unused-argument file_import = FileImport(self.code_model) @@ -419,7 +421,9 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements elif any(r.type for r in self.responses): file_import.add_submodule_import(f"{relative_path}_model_base", "_deserialize", ImportType.LOCAL) if self.default_error_deserialization or self.non_default_errors: - file_import.add_submodule_import(f"{relative_path}_model_base", "_failsafe_deserialize", ImportType.LOCAL) + file_import.add_submodule_import( + f"{relative_path}_model_base", "_failsafe_deserialize", ImportType.LOCAL + ) return file_import def get_response_from_status(self, status_code: Optional[Union[str, int]]) -> ResponseType: diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 34924397e1..563197567d 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -991,9 +991,7 @@ def handle_error_response(self, builder: OperationType) -> List[str]: elif isinstance(builder.stream_value, str): # _stream is not sure, so we need to judge it retval.append(" if _stream:") retval.extend([f" {l}" for l in response_read]) - retval.append( - f" map_error(status_code=response.status_code, response=response, error_map=error_map)" - ) + retval.append(f" map_error(status_code=response.status_code, response=response, error_map=error_map)") error_model = "" if builder.non_default_errors and self.code_model.options["models_mode"]: error_model = ", model=error" @@ -1005,7 +1003,9 @@ def handle_error_response(self, builder: OperationType) -> List[str]: for status_code in e.status_codes: retval.append(f" {condition} response.status_code == {status_code}:") if self.code_model.options["models_mode"] == "dpg": - retval.append(f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())") + retval.append( + f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())" + ) else: retval.append( f" error = self._deserialize.failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, " @@ -1043,9 +1043,13 @@ def handle_error_response(self, builder: OperationType) -> List[str]: ) # ranged status code only exist in typespec and will not have multiple status codes else: - retval.append(f" {condition} {e.status_codes[0][0]} <= response.status_code <= {e.status_codes[0][1]}:") + retval.append( + f" {condition} {e.status_codes[0][0]} <= response.status_code <= {e.status_codes[0][1]}:" + ) if self.code_model.options["models_mode"] == "dpg": - retval.append(f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())") + retval.append( + f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())" + ) else: retval.append( f" error = self._deserialize.failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, " @@ -1059,7 +1063,9 @@ def handle_error_response(self, builder: OperationType) -> List[str]: if builder.non_default_errors: retval.append(" else:") if self.code_model.options["models_mode"] == "dpg": - retval.append(f"{indent}error = _failsafe_deserialize({builder.default_error_deserialization}, response.json())") + retval.append( + f"{indent}error = _failsafe_deserialize({builder.default_error_deserialization}, response.json())" + ) else: retval.append( f"{indent}error = self._deserialize.failsafe_deserialize({builder.default_error_deserialization}, " @@ -1143,10 +1149,13 @@ def _need_specific_error_map(self, code: int, builder: OperationType) -> bool: if code in non_default_error.status_codes: return False # ranged status code - if isinstance(non_default_error.status_codes[0], list) and non_default_error.status_codes[0][0] <= code <= non_default_error.status_codes[0][1]: + if ( + isinstance(non_default_error.status_codes[0], list) + and non_default_error.status_codes[0][0] <= code <= non_default_error.status_codes[0][1] + ): return False return True - + def error_map(self, builder: OperationType) -> List[str]: retval = ["error_map: MutableMapping = {"] if builder.non_default_errors and self.code_model.options["models_mode"]: diff --git a/packages/http-client-python/generator/test/azure/mock_api_tests/asynctests/test_azure_arm_resource_async.py b/packages/http-client-python/generator/test/azure/mock_api_tests/asynctests/test_azure_arm_resource_async.py index 1e8fbe0213..abf03c911c 100644 --- a/packages/http-client-python/generator/test/azure/mock_api_tests/asynctests/test_azure_arm_resource_async.py +++ b/packages/http-client-python/generator/test/azure/mock_api_tests/asynctests/test_azure_arm_resource_async.py @@ -36,9 +36,7 @@ async def test_client_signature(credential, authentication_policy): # make sure signautre order is correct await client.top_level.get(RESOURCE_GROUP_NAME, "top") # make sure signautre name is correct - await client.top_level.get( - resource_group_name=RESOURCE_GROUP_NAME, top_level_tracked_resource_name="top" - ) + await client.top_level.get(resource_group_name=RESOURCE_GROUP_NAME, top_level_tracked_resource_name="top") @pytest.mark.asyncio diff --git a/packages/http-client-python/generator/test/azure/mock_api_tests/test_azure_arm_resource.py b/packages/http-client-python/generator/test/azure/mock_api_tests/test_azure_arm_resource.py index d79bc05168..ecc550477e 100644 --- a/packages/http-client-python/generator/test/azure/mock_api_tests/test_azure_arm_resource.py +++ b/packages/http-client-python/generator/test/azure/mock_api_tests/test_azure_arm_resource.py @@ -35,9 +35,7 @@ def test_client_signature(credential, authentication_policy): # make sure signautre order is correct client.top_level.get(RESOURCE_GROUP_NAME, "top") # make sure signautre name is correct - client.top_level.get( - resource_group_name=RESOURCE_GROUP_NAME, top_level_tracked_resource_name="top" - ) + client.top_level.get(resource_group_name=RESOURCE_GROUP_NAME, top_level_tracked_resource_name="top") def test_top_level_begin_create_or_replace(client): From a65e1c3eae543c48f309643047dee0b648d8544e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 9 Dec 2024 12:08:12 -0500 Subject: [PATCH 07/39] try diff building format --- packages/http-client-python/emitter/tsconfig.build.json | 6 +++--- packages/http-client-python/eng/scripts/setup/install.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/http-client-python/emitter/tsconfig.build.json b/packages/http-client-python/emitter/tsconfig.build.json index 663c3cc0d5..2f98a8be79 100644 --- a/packages/http-client-python/emitter/tsconfig.build.json +++ b/packages/http-client-python/emitter/tsconfig.build.json @@ -2,10 +2,10 @@ "extends": "./tsconfig.json", "compilerOptions": { "noEmit": false, - "rootDir": "./src", - "outDir": "../dist/emitter", + "rootDir": "../", + "outDir": "../dist", "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo" }, "references": [], - "include": ["src/**/*"] + "include": ["src/**/*", "../eng/scripts/setup/*"] } diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 61d90e2890..8ed7d24376 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -35,6 +35,6 @@ installPythonDeps() .then(() => console.log("Successfully installed all required Python packages")) // eslint-disable-line no-console .catch((error) => console.error(`Installation failed: ${error.message}`)); // eslint-disable-line no-console -installPyodideDeps() - .then(() => console.log("Successfully installed all required Python packages in Pyodide")) // eslint-disable-line no-console - .catch((error) => console.error(`Installation in Pyodide failed: ${error.message}`)); // eslint-disable-line no-console +// installPyodideDeps() +// .then(() => console.log("Successfully installed all required Python packages in Pyodide")) // eslint-disable-line no-console +// .catch((error) => console.error(`Installation in Pyodide failed: ${error.message}`)); // eslint-disable-line no-console From 19e1fb8cf831943a4773cf0e64a6a3fbaeba7510 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 9 Dec 2024 12:16:51 -0500 Subject: [PATCH 08/39] install pyodide deps --- packages/http-client-python/eng/scripts/setup/install.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 8ed7d24376..61d90e2890 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -35,6 +35,6 @@ installPythonDeps() .then(() => console.log("Successfully installed all required Python packages")) // eslint-disable-line no-console .catch((error) => console.error(`Installation failed: ${error.message}`)); // eslint-disable-line no-console -// installPyodideDeps() -// .then(() => console.log("Successfully installed all required Python packages in Pyodide")) // eslint-disable-line no-console -// .catch((error) => console.error(`Installation in Pyodide failed: ${error.message}`)); // eslint-disable-line no-console +installPyodideDeps() + .then(() => console.log("Successfully installed all required Python packages in Pyodide")) // eslint-disable-line no-console + .catch((error) => console.error(`Installation in Pyodide failed: ${error.message}`)); // eslint-disable-line no-console From f3d4a258b3a42d18a07e17da47c0ce516d359e34 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 9 Dec 2024 13:27:41 -0500 Subject: [PATCH 09/39] try to not have python command line pop up in call --- packages/http-client-python/eng/scripts/setup/install.ts | 8 ++------ packages/http-client-python/eng/scripts/setup/prepare.ts | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 61d90e2890..54117d3885 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -4,11 +4,7 @@ import { fileURLToPath } from "url"; import { runPython3 } from "./run-python3.js"; async function installPythonDeps() { - try { - await runPython3("./eng/scripts/setup/install.py"); - } catch (error) { - console.log("No Python installation found. Skipping Python dependencies installation."); // eslint-disable-line no-console - } + await runPython3("./eng/scripts/setup/install.py"); } async function installPyodideDeps() { @@ -33,7 +29,7 @@ async function installPyodideDeps() { installPythonDeps() .then(() => console.log("Successfully installed all required Python packages")) // eslint-disable-line no-console - .catch((error) => console.error(`Installation failed: ${error.message}`)); // eslint-disable-line no-console + .catch((error) => console.log("No Python installation found. Skipping Python dependencies installation.")); // eslint-disable-line no-console installPyodideDeps() .then(() => console.log("Successfully installed all required Python packages in Pyodide")) // eslint-disable-line no-console diff --git a/packages/http-client-python/eng/scripts/setup/prepare.ts b/packages/http-client-python/eng/scripts/setup/prepare.ts index ac1d1824fe..b26aae370e 100644 --- a/packages/http-client-python/eng/scripts/setup/prepare.ts +++ b/packages/http-client-python/eng/scripts/setup/prepare.ts @@ -1,13 +1,9 @@ import { runPython3 } from "./run-python3.js"; async function preparePythonDeps() { - try { - await runPython3("./eng/scripts/setup/prepare.py"); - } catch (error) { - console.log("No Python installation found. Skipping Python dependencies preparation."); // eslint-disable-line no-console - } + await runPython3("./eng/scripts/setup/prepare.py"); } preparePythonDeps() .then(() => console.log("Successfully prepared all required Python packages")) // eslint-disable-line no-console - .catch((error) => console.error(`Preparation failed: ${error.message}`)); // eslint-disable-line no-console + .catch((error) => console.log(`Preparation failed: ${error.message}`)); // eslint-disable-line no-console From d6da8966a86582ff8b776c5d497b21cd64e1f4c8 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 9 Dec 2024 14:56:41 -0500 Subject: [PATCH 10/39] don't rerun python command in python3 script --- .../eng/scripts/setup/install.ts | 20 ++++++++++--------- .../eng/scripts/setup/run-python3.ts | 5 ----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 54117d3885..99f910dea8 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -3,8 +3,16 @@ import { loadPyodide } from "pyodide"; import { fileURLToPath } from "url"; import { runPython3 } from "./run-python3.js"; -async function installPythonDeps() { - await runPython3("./eng/scripts/setup/install.py"); +async function main() { + try { + await runPython3("./eng/scripts/setup/install.py"); + console.log("Found Python on your local environment and created a venv with all requirements."); // eslint-disable-line no-console + } catch (error) { + console.log("No Python found on your local environment. We will use Pyodide instead."); // eslint-disable-line no-console + } finally { + await installPyodideDeps(); + console.log("Successfully installed all required Python packages in Pyodide"); // eslint-disable-line no-console + } } async function installPyodideDeps() { @@ -27,10 +35,4 @@ async function installPyodideDeps() { ]); } -installPythonDeps() - .then(() => console.log("Successfully installed all required Python packages")) // eslint-disable-line no-console - .catch((error) => console.log("No Python installation found. Skipping Python dependencies installation.")); // eslint-disable-line no-console - -installPyodideDeps() - .then(() => console.log("Successfully installed all required Python packages in Pyodide")) // eslint-disable-line no-console - .catch((error) => console.error(`Installation in Pyodide failed: ${error.message}`)); // eslint-disable-line no-console +main(); diff --git a/packages/http-client-python/eng/scripts/setup/run-python3.ts b/packages/http-client-python/eng/scripts/setup/run-python3.ts index 1cd75a1508..e4df6e8a71 100644 --- a/packages/http-client-python/eng/scripts/setup/run-python3.ts +++ b/packages/http-client-python/eng/scripts/setup/run-python3.ts @@ -18,8 +18,3 @@ export async function runPython3(...args: string[]) { stdio: [0, 1, 2], }); } - -runPython3(...process.argv.slice(2)).catch((err) => { - console.error(err.toString()); // eslint-disable-line no-console - process.exit(1); -}); From e8f93f2ef91fe19a437d0b741d909d3941e8ba88 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 10 Dec 2024 11:32:10 -0500 Subject: [PATCH 11/39] update location of compiled files --- .../http-client-python/eng/scripts/setup/prepare.ts | 13 +++++++------ packages/http-client-python/package.json | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/http-client-python/eng/scripts/setup/prepare.ts b/packages/http-client-python/eng/scripts/setup/prepare.ts index b26aae370e..5ac6868af8 100644 --- a/packages/http-client-python/eng/scripts/setup/prepare.ts +++ b/packages/http-client-python/eng/scripts/setup/prepare.ts @@ -1,9 +1,10 @@ import { runPython3 } from "./run-python3.js"; - -async function preparePythonDeps() { - await runPython3("./eng/scripts/setup/prepare.py"); +async function main() { + try { + await runPython3("./eng/scripts/setup/prepare.py"); + } catch (error) { + console.log("No Python found on your local environment. We will use Pyodide instead."); // eslint-disable-line no-console + } } -preparePythonDeps() - .then(() => console.log("Successfully prepared all required Python packages")) // eslint-disable-line no-console - .catch((error) => console.log(`Preparation failed: ${error.message}`)); // eslint-disable-line no-console +main(); diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 8feef61562..1948a0b834 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -18,11 +18,11 @@ "python" ], "type": "module", - "main": "dist/emitter/index.js", + "main": "dist/emitter/src/index.js", "exports": { ".": { - "types": "./dist/emitter/index.d.ts", - "default": "./dist/emitter/index.js" + "types": "./dist/emitter/src/index.d.ts", + "default": "./dist/emitter/src/index.js" } }, "engines": { From e6ec03ac07bf511fc8ad230b36fd765f30cf3381 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 10 Dec 2024 13:28:46 -0500 Subject: [PATCH 12/39] fix check for pyodide --- packages/http-client-python/emitter/src/emitter.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 06f10ca23c..8cc6cdee76 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -89,14 +89,6 @@ export async function $onEmit(context: EmitContext) { } const yamlPath = await saveCodeModelAsYaml("python-yaml-path", yamlMap); addDefaultOptions(sdkContext); - 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 resolvedOptions = sdkContext.emitContext.options; const commandArgs: Record = {}; if (resolvedOptions["packaging-files-config"]) { @@ -126,7 +118,7 @@ export async function $onEmit(context: EmitContext) { } commandArgs["from-typespec"] = "true"; if (!program.compilerOptions.noEmit && !program.hasError()) { - if (resolvedOptions["use-pyodide"] || !fs.existsSync(venvPath)) { + if (resolvedOptions["use-pyodide"] || !fs.existsSync(path.join(root, "venv"))) { // here we run with pyodide, if there's no venv or if the user specifies to use pyodide const outputFolder = path.relative(root, outputDir); const pyodide = await setupPyodideCall(root, outputFolder); From 14ef204b29faa6f03214e0b932fdde69d35eaeb8 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 10 Dec 2024 13:30:30 -0500 Subject: [PATCH 13/39] try online --- packages/http-client-python/eng/scripts/ci/regenerate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 5fb9df459c..001c884a81 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -279,7 +279,7 @@ function _getCmdList(spec: string, flags: RegenerateFlags): TspCommand[] { return addOptions(spec, GENERATED_FOLDER, flags).map((option) => { return { outputDir: option.outputDir, - command: `tsp compile ${spec} --emit=${toPosix(PLUGIN_DIR)} ${option.optionsStr}`, + command: `tsp compile ${spec} --emit=${toPosix(resolve(PLUGIN_DIR, "dist/emitter/src"))} ${option.optionsStr}`, }; }); } From 2fe4bf7cab2ffdd172e4e9a05799b6ccb5a4a23a Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 10 Dec 2024 17:53:46 -0500 Subject: [PATCH 14/39] temp --- .../http-client-python/emitter/src/emitter.ts | 6 +++-- .../eng/scripts/ci/regenerate.ts | 2 +- .../eng/scripts/setup/build.ts | 24 ++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 8cc6cdee76..bba91ceea5 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -77,7 +77,7 @@ async function createPythonSdkContext) { const program = context.program; const sdkContext = await createPythonSdkContext(context); - const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); const outputDir = context.emitterOutputDir; const yamlMap = emitCodeModel(sdkContext); if (yamlMap.clients.length === 0) { @@ -126,6 +126,8 @@ export async function $onEmit(context: EmitContext) { const globals = pyodide.toPy({ outputFolder, yamlRelativePath, commandArgs }); const pythonCode = ` async def main(): + import pathlib + import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", SyntaxWarning) # bc of m2r2 dep issues @@ -166,7 +168,7 @@ async function setupPyodideCall(root: string, outputFolder: string) { fs.mkdirSync(outputFolder, { recursive: true }); } const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); - pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, "."); + pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "./" }, "."); await pyodide.loadPackage("setuptools"); await pyodide.loadPackage("tomli"); await pyodide.loadPackage("docutils"); diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 001c884a81..76392c4632 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -16,7 +16,7 @@ const argv = parseArgs({ pluginDir: { type: "string" }, emitterName: { type: "string" }, generatedFolder: { type: "string" }, - usePyodide: { type: "boolean" }, + pyodide: { type: "boolean" }, }, }); diff --git a/packages/http-client-python/eng/scripts/setup/build.ts b/packages/http-client-python/eng/scripts/setup/build.ts index 2ed768f472..446dfd9de0 100644 --- a/packages/http-client-python/eng/scripts/setup/build.ts +++ b/packages/http-client-python/eng/scripts/setup/build.ts @@ -1,14 +1,16 @@ import { exec } from "child_process"; +import { runPython3 } from "./run-python3.js"; -// Define the command you want to run -const command = - "tsx ./eng/scripts/setup/run-python3.ts ./eng/scripts/setup/build_pygen_wheel.py && rimraf ./venv_build_wheel"; +async function main() { + await runPython3("./eng/scripts/setup/build_pygen_wheel.py"); + // remove the venv_build_wheel directory + exec("rimraf ./venv_build_wheel", (error, stdout, stderr) => { + if (error) { + console.error(`Error executing command: ${error.message}`); // eslint-disable-line no-console + return; + } + console.log(`Command output:\n${stdout}`); // eslint-disable-line no-console + }); +} -// Execute the command -exec(command, (error, stdout, stderr) => { - if (error) { - console.error(`Error executing command: ${error.message}`); // eslint-disable-line no-console - return; - } - console.log(`Command output:\n${stdout}`); // eslint-disable-line no-console -}); +main() From 5fefb9353c7e08f251a90af7c36448eb565ed99a Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 11 Dec 2024 18:50:10 +0800 Subject: [PATCH 15/39] try to fix path issue --- .../http-client-python/emitter/src/emitter.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index bba91ceea5..f5fead8f07 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -4,7 +4,7 @@ import { SdkHttpOperation, SdkServiceOperation, } from "@azure-tools/typespec-client-generator-core"; -import { EmitContext, NoTarget } from "@typespec/compiler"; +import { EmitContext, joinPaths, NoTarget } from "@typespec/compiler"; import { exec } from "child_process"; import fs from "fs"; import path, { dirname } from "path"; @@ -15,6 +15,7 @@ import { emitCodeModel } from "./code-model.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; +import os from "os"; export function getModelsMode(context: SdkContext): "dpg" | "none" { const specifiedModelsMode = context.emitContext.options["models-mode"]; @@ -122,8 +123,8 @@ export async function $onEmit(context: EmitContext) { // here we run with pyodide, if there's no venv or if the user specifies to use pyodide const outputFolder = path.relative(root, outputDir); const pyodide = await setupPyodideCall(root, outputFolder); - const yamlRelativePath = path.relative(root, yamlPath); - const globals = pyodide.toPy({ outputFolder, yamlRelativePath, commandArgs }); + const yamlFilePath = path.join("/tmp", path.basename(yamlPath)); + const globals = pyodide.toPy({ outputFolder, yamlFilePath, commandArgs }); const pythonCode = ` async def main(): import pathlib @@ -132,9 +133,9 @@ export async function $onEmit(context: EmitContext) { with warnings.catch_warnings(): warnings.simplefilter("ignore", SyntaxWarning) # bc of m2r2 dep issues 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() + m2r.M2R(output_folder=outputFolder, cadl_file=yamlFilePath, **commandArgs).process() + preprocess.PreProcessPlugin(output_folder=outputFolder, cadl_file=yamlFilePath, **commandArgs).process() + codegen.CodeGenerator(output_folder=outputFolder, cadl_file=yamlFilePath, **commandArgs).process() black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process() await main()`; @@ -168,7 +169,8 @@ async function setupPyodideCall(root: string, outputFolder: string) { fs.mkdirSync(outputFolder, { recursive: true }); } const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); - pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "./" }, "."); + pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, "."); + pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: joinPaths(os.tmpdir(), "cadl-codegen")}, "./tmp"); await pyodide.loadPackage("setuptools"); await pyodide.loadPackage("tomli"); await pyodide.loadPackage("docutils"); From 5299c1cddd4b752b280074099a0c98c68ccdb2ed Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 11 Dec 2024 12:40:33 -0500 Subject: [PATCH 16/39] roll back packaging of eng in dist --- packages/http-client-python/emitter/src/emitter.ts | 11 +++++------ .../http-client-python/emitter/tsconfig.build.json | 6 +++--- packages/http-client-python/emitter/tsconfig.json | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index f5fead8f07..888b857051 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -123,8 +123,8 @@ export async function $onEmit(context: EmitContext) { // here we run with pyodide, if there's no venv or if the user specifies to use pyodide const outputFolder = path.relative(root, outputDir); const pyodide = await setupPyodideCall(root, outputFolder); - const yamlFilePath = path.join("/tmp", path.basename(yamlPath)); - const globals = pyodide.toPy({ outputFolder, yamlFilePath, commandArgs }); + const yamlRelativePath = path.relative(root, yamlPath); + const globals = pyodide.toPy({ outputFolder, yamlRelativePath, commandArgs }); const pythonCode = ` async def main(): import pathlib @@ -133,9 +133,9 @@ export async function $onEmit(context: EmitContext) { with warnings.catch_warnings(): warnings.simplefilter("ignore", SyntaxWarning) # bc of m2r2 dep issues from pygen import m2r, preprocess, codegen, black - m2r.M2R(output_folder=outputFolder, cadl_file=yamlFilePath, **commandArgs).process() - preprocess.PreProcessPlugin(output_folder=outputFolder, cadl_file=yamlFilePath, **commandArgs).process() - codegen.CodeGenerator(output_folder=outputFolder, cadl_file=yamlFilePath, **commandArgs).process() + 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()`; @@ -170,7 +170,6 @@ async function setupPyodideCall(root: string, outputFolder: string) { } const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, "."); - pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: joinPaths(os.tmpdir(), "cadl-codegen")}, "./tmp"); await pyodide.loadPackage("setuptools"); await pyodide.loadPackage("tomli"); await pyodide.loadPackage("docutils"); diff --git a/packages/http-client-python/emitter/tsconfig.build.json b/packages/http-client-python/emitter/tsconfig.build.json index 2f98a8be79..663c3cc0d5 100644 --- a/packages/http-client-python/emitter/tsconfig.build.json +++ b/packages/http-client-python/emitter/tsconfig.build.json @@ -2,10 +2,10 @@ "extends": "./tsconfig.json", "compilerOptions": { "noEmit": false, - "rootDir": "../", - "outDir": "../dist", + "rootDir": "./src", + "outDir": "../dist/emitter", "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo" }, "references": [], - "include": ["src/**/*", "../eng/scripts/setup/*"] + "include": ["src/**/*"] } diff --git a/packages/http-client-python/emitter/tsconfig.json b/packages/http-client-python/emitter/tsconfig.json index d9fd71175a..32cb2aff3f 100644 --- a/packages/http-client-python/emitter/tsconfig.json +++ b/packages/http-client-python/emitter/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": ["src/**/*", "../eng/scripts/setup/*"] + "include": ["src/**/*"] } From 63d4a15f5e079746479dce32ecfef52039be0efd Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 11 Dec 2024 12:43:40 -0500 Subject: [PATCH 17/39] add back duplicate run-python3 files --- .../http-client-python/emitter/src/emitter.ts | 5 +- .../emitter/src/run-python3.ts | 20 ++ .../emitter/src/system-requirements.ts | 261 ++++++++++++++++++ 3 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 packages/http-client-python/emitter/src/run-python3.ts create mode 100644 packages/http-client-python/emitter/src/system-requirements.ts diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 888b857051..a33a26716b 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -4,18 +4,17 @@ import { SdkHttpOperation, SdkServiceOperation, } from "@azure-tools/typespec-client-generator-core"; -import { EmitContext, joinPaths, NoTarget } from "@typespec/compiler"; +import { EmitContext, NoTarget } from "@typespec/compiler"; import { exec } from "child_process"; import fs from "fs"; import path, { dirname } from "path"; import { loadPyodide } from "pyodide"; import { fileURLToPath } from "url"; -import { runPython3 } from "../../eng/scripts/setup/run-python3.js"; +import { runPython3 } from "./run-python3.js"; import { emitCodeModel } from "./code-model.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; -import os from "os"; export function getModelsMode(context: SdkContext): "dpg" | "none" { const specifiedModelsMode = context.emitContext.options["models-mode"]; diff --git a/packages/http-client-python/emitter/src/run-python3.ts b/packages/http-client-python/emitter/src/run-python3.ts new file mode 100644 index 0000000000..e4df6e8a71 --- /dev/null +++ b/packages/http-client-python/emitter/src/run-python3.ts @@ -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], + }); +} diff --git a/packages/http-client-python/emitter/src/system-requirements.ts b/packages/http-client-python/emitter/src/system-requirements.ts new file mode 100644 index 0000000000..7f12ff5b5a --- /dev/null +++ b/packages/http-client-python/emitter/src/system-requirements.ts @@ -0,0 +1,261 @@ +import { ChildProcess, spawn, SpawnOptions } from "child_process"; +import { coerce, satisfies } from "semver"; + +/* + * Copied from @autorest/system-requirements + */ + +const execute = ( + command: string, + cmdlineargs: Array, + options: MoreOptions = {}, +): Promise => { + return new Promise((resolve, reject) => { + const cp = spawn(command, cmdlineargs, { ...options, stdio: "pipe", shell: true }); + if (options.onCreate) { + options.onCreate(cp); + } + + options.onStdOutData && cp.stdout.on("data", options.onStdOutData); + options.onStdErrData && cp.stderr.on("data", options.onStdErrData); + + let err = ""; + let out = ""; + let all = ""; + cp.stderr.on("data", (chunk) => { + err += chunk; + all += chunk; + }); + cp.stdout.on("data", (chunk) => { + out += chunk; + all += chunk; + }); + + cp.on("error", (err) => { + reject(err); + }); + cp.on("close", (code, signal) => + resolve({ + stdout: out, + stderr: err, + log: all, + error: code ? new Error("Process Failed.") : null, + code, + }), + ); + }); +}; + +const versionIsSatisfied = (version: string, requirement: string): boolean => { + const cleanedVersion = coerce(version); + if (!cleanedVersion) { + throw new Error(`Invalid version ${version}.`); + } + return satisfies(cleanedVersion, requirement, true); +}; + +/** + * Validate the provided system requirement resolution is satisfying the version requirement if applicable. + * @param resolution Command resolution. + * @param actualVersion Version for that resolution. + * @param requirement Requirement. + * @returns the resolution if it is valid or an @see SystemRequirementError if not. + */ +const validateVersionRequirement = ( + resolution: SystemRequirementResolution, + actualVersion: string, + requirement: SystemRequirement, +): SystemRequirementResolution | SystemRequirementError => { + if (!requirement.version) { + return resolution; // No version requirement. + } + + try { + if (versionIsSatisfied(actualVersion, requirement.version)) { + return resolution; + } + return { + ...resolution, + error: true, + message: `'${resolution.command}' version is '${actualVersion}' but doesn't satisfy requirement '${requirement.version}'. Please update.`, + actualVersion: actualVersion, + neededVersion: requirement.version, + }; + } catch { + return { + ...resolution, + error: true, + message: `Couldn't parse the version ${actualVersion}. This is not a valid semver version.`, + actualVersion: actualVersion, + neededVersion: requirement.version, + }; + } +}; + +const tryPython = async ( + requirement: SystemRequirement, + command: string, + additionalArgs: string[] = [], +): Promise => { + const resolution: SystemRequirementResolution = { + name: PythonRequirement, + command, + additionalArgs: additionalArgs.length > 0 ? additionalArgs : undefined, + }; + + try { + const result = await execute(command, [ + ...additionalArgs, + "-c", + `"${PRINT_PYTHON_VERSION_SCRIPT}"`, + ]); + return validateVersionRequirement(resolution, result.stdout.trim(), requirement); + } catch (e) { + return { + error: true, + ...resolution, + message: `'${command}' command line is not found in the path. Make sure to have it installed.`, + }; + } +}; + +/** + * Returns the path to the executable as asked in the requirement. + * @param requirement System requirement definition. + * @returns If the requirement provide an environment variable for the path returns the value of that environment variable. undefined otherwise. + */ +const getExecutablePath = (requirement: SystemRequirement): string | undefined => + requirement.environmentVariable && process.env[requirement.environmentVariable]; + +const createPythonErrorMessage = ( + requirement: SystemRequirement, + errors: SystemRequirementError[], +): SystemRequirementError => { + const versionReq = requirement.version ?? "*"; + const lines = [ + `Couldn't find a valid python interpreter satisfying the requirement (version: ${versionReq}). Tried:`, + ...errors.map((x) => ` - ${x.command} (${x.message})`), + ]; + + return { + error: true, + name: "python", + command: "python", + message: lines.join("\n"), + }; +}; + +const resolvePythonRequirement = async ( + requirement: SystemRequirement, +): Promise => { + // Hardcoding AUTOREST_PYTHON_EXE is for backward compatibility + const path = getExecutablePath(requirement) ?? process.env["AUTOREST_PYTHON_EXE"]; + if (path) { + return await tryPython(requirement, path); + } + + const errors: SystemRequirementError[] = []; + // On windows try `py` executable with `-3` flag. + if (process.platform === "win32") { + const pyResult = await tryPython(requirement, "py", ["-3"]); + if ("error" in pyResult) { + errors.push(pyResult); + } else { + return pyResult; + } + } + + const python3Result = await tryPython(requirement, "python3"); + if ("error" in python3Result) { + errors.push(python3Result); + } else { + return python3Result; + } + + const pythonResult = await tryPython(requirement, "python"); + if ("error" in pythonResult) { + errors.push(pythonResult); + } else { + return pythonResult; + } + + return createPythonErrorMessage(requirement, errors); +}; + +/** + * @param command list of the command and arguments. First item in array must be a python exe @see KnownPythonExe. (e.g. ["python", "my_python_file.py"] + * @param requirement + */ +export const patchPythonPath = async ( + command: PythonCommandLine, + requirement: SystemRequirement, +): Promise => { + const [_, ...args] = command; + const resolution = await resolvePythonRequirement(requirement); + if ("error" in resolution) { + throw new Error(`Failed to find compatible python version. ${resolution.message}`); + } + return [resolution.command, ...(resolution.additionalArgs ?? []), ...args]; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// TYPES +const PythonRequirement = "python"; +const PRINT_PYTHON_VERSION_SCRIPT = "import sys; print('.'.join(map(str, sys.version_info[:3])))"; + +type KnownPythonExe = "python.exe" | "python3.exe" | "python" | "python3"; +type PythonCommandLine = [KnownPythonExe, ...string[]]; + +interface MoreOptions extends SpawnOptions { + onCreate?(cp: ChildProcess): void; + onStdOutData?(chunk: any): void; + onStdErrData?(chunk: any): void; +} + +interface SystemRequirement { + version?: string; + /** + * Name of an environment variable where the user could provide the path to the exe. + * @example "AUTOREST_PYTHON_PATH" + */ + environmentVariable?: string; +} + +interface SystemRequirementResolution { + /** + * Name of the requirement. + * @example python, java, etc. + */ + name: string; + + /** + * Name of the command + * @example python3, /home/my_user/python39/python, java, etc. + */ + command: string; + + /** + * List of additional arguments to pass to this command. + * @example '-3' for 'py' to specify to use python 3 + */ + additionalArgs?: string[]; +} + +interface ExecResult { + stdout: string; + stderr: string; + + /** + * Union of stdout and stderr. + */ + log: string; + error: Error | null; + code: number | null; +} + +interface SystemRequirementError extends SystemRequirementResolution { + error: true; + message: string; + neededVersion?: string; + actualVersion?: string; +} From 871fed1cdedbb2acd27bf2ea324c56247b1fadd0 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 11 Dec 2024 12:52:18 -0500 Subject: [PATCH 18/39] fix emit path --- packages/http-client-python/eng/scripts/ci/regenerate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 76392c4632..000a3a9400 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -279,7 +279,7 @@ function _getCmdList(spec: string, flags: RegenerateFlags): TspCommand[] { return addOptions(spec, GENERATED_FOLDER, flags).map((option) => { return { outputDir: option.outputDir, - command: `tsp compile ${spec} --emit=${toPosix(resolve(PLUGIN_DIR, "dist/emitter/src"))} ${option.optionsStr}`, + command: `tsp compile ${spec} --emit=${toPosix(PLUGIN_DIR)} ${option.optionsStr}`, }; }); } From 0b66e28dcef82dab23790e3e68ce312720747e8e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 11 Dec 2024 13:11:18 -0500 Subject: [PATCH 19/39] format --- packages/http-client-python/emitter/src/emitter.ts | 2 +- packages/http-client-python/eng/scripts/setup/build.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index a33a26716b..5382751abb 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -10,10 +10,10 @@ import fs from "fs"; import path, { dirname } from "path"; import { loadPyodide } from "pyodide"; import { fileURLToPath } from "url"; -import { runPython3 } from "./run-python3.js"; import { emitCodeModel } from "./code-model.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js"; +import { runPython3 } from "./run-python3.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; export function getModelsMode(context: SdkContext): "dpg" | "none" { diff --git a/packages/http-client-python/eng/scripts/setup/build.ts b/packages/http-client-python/eng/scripts/setup/build.ts index 446dfd9de0..c13770edeb 100644 --- a/packages/http-client-python/eng/scripts/setup/build.ts +++ b/packages/http-client-python/eng/scripts/setup/build.ts @@ -13,4 +13,4 @@ async function main() { }); } -main() +main(); From 8584e56e3508ba463b8cbb36b14f8974469ea085 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 11 Dec 2024 13:14:10 -0500 Subject: [PATCH 20/39] fix root --- packages/http-client-python/emitter/src/emitter.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 5382751abb..1c92fc256a 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -77,7 +77,7 @@ async function createPythonSdkContext) { const program = context.program; const sdkContext = await createPythonSdkContext(context); - const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const outputDir = context.emitterOutputDir; const yamlMap = emitCodeModel(sdkContext); if (yamlMap.clients.length === 0) { @@ -126,8 +126,6 @@ export async function $onEmit(context: EmitContext) { const globals = pyodide.toPy({ outputFolder, yamlRelativePath, commandArgs }); const pythonCode = ` async def main(): - import pathlib - import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", SyntaxWarning) # bc of m2r2 dep issues From 5d35a5dbdc3a87f14065106c3bf298c01d80b4bc Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 11 Dec 2024 15:53:54 -0500 Subject: [PATCH 21/39] general command working --- packages/http-client-python/emitter/src/emitter.ts | 13 +++++++------ packages/http-client-python/package.json | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 1c92fc256a..204e6bc2a6 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -5,7 +5,7 @@ import { SdkServiceOperation, } from "@azure-tools/typespec-client-generator-core"; import { EmitContext, NoTarget } from "@typespec/compiler"; -import { exec } from "child_process"; +import { execSync } from "child_process"; import fs from "fs"; import path, { dirname } from "path"; import { loadPyodide } from "pyodide"; @@ -117,6 +117,7 @@ export async function $onEmit(context: EmitContext) { commandArgs["emit-cross-language-definition-file"] = "true"; } commandArgs["from-typespec"] = "true"; + if (!program.compilerOptions.noEmit && !program.hasError()) { if (resolvedOptions["use-pyodide"] || !fs.existsSync(path.join(root, "venv"))) { // here we run with pyodide, if there's no venv or if the user specifies to use pyodide @@ -152,11 +153,11 @@ export async function $onEmit(context: EmitContext) { } commandArgs["output-folder"] = outputDir; commandArgs["cadl-file"] = yamlPath; - await exec( - Object.entries(commandArgs) - .map(([key, value]) => `--${key} ${value}`) - .join(" "), - ); + const commandFlags = Object.entries(commandArgs) + .map(([key, value]) => `--${key}=${value}`) + .join(" "); + const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`; + execSync(command); } } } diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 1948a0b834..8feef61562 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -18,11 +18,11 @@ "python" ], "type": "module", - "main": "dist/emitter/src/index.js", + "main": "dist/emitter/index.js", "exports": { ".": { - "types": "./dist/emitter/src/index.d.ts", - "default": "./dist/emitter/src/index.js" + "types": "./dist/emitter/index.d.ts", + "default": "./dist/emitter/index.js" } }, "engines": { From 20280dccb37fd7c53adbb4f2a530c136234ee0dd Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 16:21:48 +0800 Subject: [PATCH 22/39] resolve all path issues --- .../http-client-python/emitter/src/emitter.ts | 58 +++++++++++++------ .../eng/scripts/setup/install.ts | 18 ++---- .../eng/scripts/setup/prepare.ts | 1 + 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 204e6bc2a6..dc47f75f42 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -119,31 +119,58 @@ export async function $onEmit(context: EmitContext) { commandArgs["from-typespec"] = "true"; if (!program.compilerOptions.noEmit && !program.hasError()) { - if (resolvedOptions["use-pyodide"] || !fs.existsSync(path.join(root, "venv"))) { - // here we run with pyodide, if there's no venv or if the user specifies to use pyodide - const outputFolder = path.relative(root, outputDir); - const pyodide = await setupPyodideCall(root, outputFolder); - const yamlRelativePath = path.relative(root, yamlPath); - const globals = pyodide.toPy({ outputFolder, yamlRelativePath, commandArgs }); + // if not using pyodide and there's no venv, we try to create venv + if (!resolvedOptions["use-pyodide"] && !fs.existsSync(path.join(root, "venv"))) { + try { + await runPython3("./eng/scripts/setup/install.py"); + await runPython3("./eng/scripts/setup/prepare.py"); + } catch (error) { + if ( + error instanceof Error && + error.message.includes("Failed to find compatible python version.") + ) { + // if we can't find python with compatible version, we use pyodide instead + resolvedOptions["use-pyodide"] = true; + } else { + throw error; + } + } + } + + if (resolvedOptions["use-pyodide"]) { + // here we run with pyodide + const pyodide = await setupPyodideCall(root); + // create the output folder if not exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + // mount output folder to pyodide + pyodide.FS.mkdirTree("/output"); + pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: outputDir }, "/output"); + // mount yaml file to pyodide + pyodide.FS.mkdirTree("/yaml"); + pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.dirname(yamlPath) }, "/yaml"); + const globals = pyodide.toPy({ + outputFolder: "/output", + yamlFile: `/yaml/${path.basename(yamlPath)}`, + commandArgs, + }); const pythonCode = ` async def main(): import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", SyntaxWarning) # bc of m2r2 dep issues 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() + m2r.M2R(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process() + preprocess.PreProcessPlugin(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process() + codegen.CodeGenerator(output_folder=outputFolder, cadl_file=yamlFile, **commandArgs).process() black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process() await main()`; await pyodide.runPythonAsync(pythonCode, { globals }); } else { + // here we run with native python 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"))) { @@ -162,10 +189,7 @@ export async function $onEmit(context: EmitContext) { } } -async function setupPyodideCall(root: string, outputFolder: string) { - if (!fs.existsSync(outputFolder)) { - fs.mkdirSync(outputFolder, { recursive: true }); - } +async function setupPyodideCall(root: string) { const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, "."); await pyodide.loadPackage("setuptools"); diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 99f910dea8..535d447895 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -2,6 +2,7 @@ import path, { dirname } from "path"; import { loadPyodide } from "pyodide"; import { fileURLToPath } from "url"; import { runPython3 } from "./run-python3.js"; +import fs from "fs"; async function main() { try { @@ -20,19 +21,10 @@ async function installPyodideDeps() { const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); await pyodide.loadPackage("micropip"); const micropip = pyodide.pyimport("micropip"); - await micropip.install([ - "black", - "click", - "docutils==0.21.2", - "Jinja2==3.1.4", - "m2r2==0.3.3.post2", - "MarkupSafe", - "pathspec", - "platformdirs", - "pyyaml", - "tomli", - "setuptools", - ]); + const requirementsPath = path.join(root, "generator", "requirements.txt"); + const requirementsText = fs.readFileSync(requirementsPath, "utf-8"); + const requirementsArray = requirementsText.split("\n").filter((line) => line.trim() !== ""); + await micropip.install(requirementsArray); } main(); diff --git a/packages/http-client-python/eng/scripts/setup/prepare.ts b/packages/http-client-python/eng/scripts/setup/prepare.ts index 5ac6868af8..32d9157a24 100644 --- a/packages/http-client-python/eng/scripts/setup/prepare.ts +++ b/packages/http-client-python/eng/scripts/setup/prepare.ts @@ -1,4 +1,5 @@ import { runPython3 } from "./run-python3.js"; + async function main() { try { await runPython3("./eng/scripts/setup/prepare.py"); From b2adef486cc11763fb0941f052076e6ea5b7c042 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 16:48:40 +0800 Subject: [PATCH 23/39] changelog --- packages/http-client-python/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/CHANGELOG.md b/packages/http-client-python/CHANGELOG.md index cf1a0bae6d..0ffca95579 100644 --- a/packages/http-client-python/CHANGELOG.md +++ b/packages/http-client-python/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.5.0 -### Bug Fixes +### Features - Add support for generation in enviroments without a Python installation From 5e26307dc3fb93085116d66baef5600ef344314c Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 17:04:51 +0800 Subject: [PATCH 24/39] fix format --- packages/http-client-python/eng/scripts/setup/install.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 535d447895..4dcb1225c5 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -1,8 +1,8 @@ +import fs from "fs"; import path, { dirname } from "path"; import { loadPyodide } from "pyodide"; import { fileURLToPath } from "url"; import { runPython3 } from "./run-python3.js"; -import fs from "fs"; async function main() { try { From 930c71cb8c5f21c28ede2fea10778e72013d56c6 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 18:31:49 +0800 Subject: [PATCH 25/39] consolidate dep version --- .../generator/requirements.txt | 4 ++-- packages/http-client-python/generator/setup.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/http-client-python/generator/requirements.txt b/packages/http-client-python/generator/requirements.txt index d112c8465c..27c1a80297 100644 --- a/packages/http-client-python/generator/requirements.txt +++ b/packages/http-client-python/generator/requirements.txt @@ -1,7 +1,7 @@ black==24.8.0 click==8.1.7 -docutils==0.20.1 -Jinja2==3.1.4 +docutils==0.21.1 +Jinja2==3.1.3 m2r2==0.3.3.post2 MarkupSafe==2.1.5 mistune==0.8.4 diff --git a/packages/http-client-python/generator/setup.py b/packages/http-client-python/generator/setup.py index 865617f88d..3fc252f927 100644 --- a/packages/http-client-python/generator/setup.py +++ b/packages/http-client-python/generator/setup.py @@ -48,9 +48,17 @@ ] ), install_requires=[ - "Jinja2 >= 2.11", # I need "include" and auto-context + blank line are not indented by default - "pyyaml", - "m2r2", - "black", + "black==24.8.0", + "click==8.1.7", + "docutils==0.21.1", + "Jinja2==3.1.3", + "m2r2==0.3.3.post2", + "MarkupSafe==2.1.5", + "mistune==0.8.4", + "pathspec==0.12.1", + "platformdirs==4.3.6", + "PyYAML==6.0.1", + "tomli==2.0.1", + "setuptools==69.5.1", ], ) From 2fe27eac576dca2c750a53eca0f8e4385491ede3 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 20:47:31 +0800 Subject: [PATCH 26/39] fix version conflict --- packages/http-client-python/generator/requirements.txt | 2 +- packages/http-client-python/generator/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/requirements.txt b/packages/http-client-python/generator/requirements.txt index 27c1a80297..080c6b33bd 100644 --- a/packages/http-client-python/generator/requirements.txt +++ b/packages/http-client-python/generator/requirements.txt @@ -1,6 +1,6 @@ black==24.8.0 click==8.1.7 -docutils==0.21.1 +docutils>=0.20.1 Jinja2==3.1.3 m2r2==0.3.3.post2 MarkupSafe==2.1.5 diff --git a/packages/http-client-python/generator/setup.py b/packages/http-client-python/generator/setup.py index 3fc252f927..0f3e61f4ab 100644 --- a/packages/http-client-python/generator/setup.py +++ b/packages/http-client-python/generator/setup.py @@ -50,7 +50,7 @@ install_requires=[ "black==24.8.0", "click==8.1.7", - "docutils==0.21.1", + "docutils>=0.20.1", "Jinja2==3.1.3", "m2r2==0.3.3.post2", "MarkupSafe==2.1.5", From ff2955c46ff579321a80666a0ade800a4053d6ee Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 22:37:45 +0800 Subject: [PATCH 27/39] remove useless dep --- packages/http-client-python/generator/requirements.txt | 5 ----- packages/http-client-python/generator/setup.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/packages/http-client-python/generator/requirements.txt b/packages/http-client-python/generator/requirements.txt index 080c6b33bd..9b5c9c12d5 100644 --- a/packages/http-client-python/generator/requirements.txt +++ b/packages/http-client-python/generator/requirements.txt @@ -1,12 +1,7 @@ black==24.8.0 -click==8.1.7 docutils>=0.20.1 Jinja2==3.1.3 m2r2==0.3.3.post2 -MarkupSafe==2.1.5 -mistune==0.8.4 -pathspec==0.12.1 -platformdirs==4.3.6 PyYAML==6.0.1 tomli==2.0.1 setuptools==69.5.1 diff --git a/packages/http-client-python/generator/setup.py b/packages/http-client-python/generator/setup.py index 0f3e61f4ab..74fbd267f8 100644 --- a/packages/http-client-python/generator/setup.py +++ b/packages/http-client-python/generator/setup.py @@ -49,14 +49,9 @@ ), install_requires=[ "black==24.8.0", - "click==8.1.7", "docutils>=0.20.1", "Jinja2==3.1.3", "m2r2==0.3.3.post2", - "MarkupSafe==2.1.5", - "mistune==0.8.4", - "pathspec==0.12.1", - "platformdirs==4.3.6", "PyYAML==6.0.1", "tomli==2.0.1", "setuptools==69.5.1", From 1414b949a479497d194cfeca4d6758b331f927cd Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 23:01:01 +0800 Subject: [PATCH 28/39] remove load package --- packages/http-client-python/emitter/src/emitter.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index dc47f75f42..6dcf7c9752 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -192,9 +192,6 @@ export async function $onEmit(context: EmitContext) { async function setupPyodideCall(root: string) { 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"); From 5255adc8b0550430e82714cc5d61dc54bd21ece2 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 18 Dec 2024 23:40:58 +0800 Subject: [PATCH 29/39] consolidate dep version --- .../http-client-python/generator/test/azure/requirements.txt | 2 +- .../generator/test/unbranded/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/test/azure/requirements.txt b/packages/http-client-python/generator/test/azure/requirements.txt index 6b3a176ec8..288969dbf5 100644 --- a/packages/http-client-python/generator/test/azure/requirements.txt +++ b/packages/http-client-python/generator/test/azure/requirements.txt @@ -1,4 +1,4 @@ -setuptools==69.2.0 +setuptools==69.5.1 -e ../../ aiohttp;python_full_version>="3.5.2" requests==2.32.2 diff --git a/packages/http-client-python/generator/test/unbranded/requirements.txt b/packages/http-client-python/generator/test/unbranded/requirements.txt index a9f92acb81..f4625694f3 100644 --- a/packages/http-client-python/generator/test/unbranded/requirements.txt +++ b/packages/http-client-python/generator/test/unbranded/requirements.txt @@ -1,4 +1,4 @@ -setuptools==69.2.0 +setuptools==69.5.1 -e ../../ aiohttp;python_full_version>="3.5.2" requests==2.32.2 From efd55ffefc7e58ba182102940a53d535c7859040 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 19 Dec 2024 13:44:30 +0800 Subject: [PATCH 30/39] refine --- packages/http-client-python/emitter/src/emitter.ts | 6 ++---- .../http-client-python/emitter/src/system-requirements.ts | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 6dcf7c9752..4e7be56e3d 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -14,6 +14,7 @@ import { emitCodeModel } from "./code-model.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js"; import { runPython3 } from "./run-python3.js"; +import { pythonVersionErrorMsg } from "./system-requirements.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; export function getModelsMode(context: SdkContext): "dpg" | "none" { @@ -125,10 +126,7 @@ export async function $onEmit(context: EmitContext) { await runPython3("./eng/scripts/setup/install.py"); await runPython3("./eng/scripts/setup/prepare.py"); } catch (error) { - if ( - error instanceof Error && - error.message.includes("Failed to find compatible python version.") - ) { + if (error instanceof Error && error.message.includes(pythonVersionErrorMsg)) { // if we can't find python with compatible version, we use pyodide instead resolvedOptions["use-pyodide"] = true; } else { diff --git a/packages/http-client-python/emitter/src/system-requirements.ts b/packages/http-client-python/emitter/src/system-requirements.ts index 7f12ff5b5a..141b39e984 100644 --- a/packages/http-client-python/emitter/src/system-requirements.ts +++ b/packages/http-client-python/emitter/src/system-requirements.ts @@ -193,11 +193,13 @@ export const patchPythonPath = async ( const [_, ...args] = command; const resolution = await resolvePythonRequirement(requirement); if ("error" in resolution) { - throw new Error(`Failed to find compatible python version. ${resolution.message}`); + throw new Error(`${pythonVersionErrorMsg} ${resolution.message}`); } return [resolution.command, ...(resolution.additionalArgs ?? []), ...args]; }; +export const pythonVersionErrorMsg = "Failed to find compatible python version."; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // TYPES const PythonRequirement = "python"; From 8c69582dc42231a64b03b5ea100f2fb7d469fcf2 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 19 Dec 2024 16:52:43 +0800 Subject: [PATCH 31/39] try remove useless part --- .../eng/scripts/setup/install.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 4dcb1225c5..7cd266bbdc 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -1,7 +1,3 @@ -import fs from "fs"; -import path, { dirname } from "path"; -import { loadPyodide } from "pyodide"; -import { fileURLToPath } from "url"; import { runPython3 } from "./run-python3.js"; async function main() { @@ -10,21 +6,7 @@ async function main() { console.log("Found Python on your local environment and created a venv with all requirements."); // eslint-disable-line no-console } catch (error) { console.log("No Python found on your local environment. We will use Pyodide instead."); // eslint-disable-line no-console - } finally { - await installPyodideDeps(); - console.log("Successfully installed all required Python packages in Pyodide"); // eslint-disable-line no-console } } -async function installPyodideDeps() { - const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); - const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); - await pyodide.loadPackage("micropip"); - const micropip = pyodide.pyimport("micropip"); - const requirementsPath = path.join(root, "generator", "requirements.txt"); - const requirementsText = fs.readFileSync(requirementsPath, "utf-8"); - const requirementsArray = requirementsText.split("\n").filter((line) => line.trim() !== ""); - await micropip.install(requirementsArray); -} - main(); From 99aa67b99e86b0efda81b289bce4a7fe2d0fd447 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 19 Dec 2024 22:46:42 +0800 Subject: [PATCH 32/39] fix pyodide load path issue --- packages/http-client-python/emitter/src/emitter.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 4e7be56e3d..55c74ea43c 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -188,10 +188,12 @@ export async function $onEmit(context: EmitContext) { } async function setupPyodideCall(root: string) { - const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); - pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, "."); + const pyodide = await loadPyodide({ indexURL: path.dirname(fileURLToPath(import.meta.resolve("pyodide"))) }); + // mount generator to pyodide + pyodide.FS.mkdirTree("/generator"); + pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.join(root, "generator") }, "/generator"); await pyodide.loadPackage("micropip"); const micropip = pyodide.pyimport("micropip"); - await micropip.install("emfs:generator/dist/pygen-0.1.0-py3-none-any.whl"); + await micropip.install("emfs:/generator/dist/pygen-0.1.0-py3-none-any.whl"); return pyodide; } From 8324181cb5bbbaa0ca0c87f51ec6963c0198588b Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 19 Dec 2024 23:09:10 +0800 Subject: [PATCH 33/39] format --- packages/http-client-python/emitter/src/emitter.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 55c74ea43c..1341c6882a 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -188,10 +188,16 @@ export async function $onEmit(context: EmitContext) { } async function setupPyodideCall(root: string) { - const pyodide = await loadPyodide({ indexURL: path.dirname(fileURLToPath(import.meta.resolve("pyodide"))) }); + const pyodide = await loadPyodide({ + indexURL: path.dirname(fileURLToPath(import.meta.resolve("pyodide"))), + }); // mount generator to pyodide pyodide.FS.mkdirTree("/generator"); - pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.join(root, "generator") }, "/generator"); + pyodide.FS.mount( + pyodide.FS.filesystems.NODEFS, + { root: path.join(root, "generator") }, + "/generator", + ); await pyodide.loadPackage("micropip"); const micropip = pyodide.pyimport("micropip"); await micropip.install("emfs:/generator/dist/pygen-0.1.0-py3-none-any.whl"); From 706db7ae46d953249de050020316125cdca74b36 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 19 Dec 2024 23:13:33 +0800 Subject: [PATCH 34/39] fix path --- packages/http-client-python/emitter/src/emitter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 1341c6882a..349f03c329 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -123,8 +123,8 @@ export async function $onEmit(context: EmitContext) { // if not using pyodide and there's no venv, we try to create venv if (!resolvedOptions["use-pyodide"] && !fs.existsSync(path.join(root, "venv"))) { try { - await runPython3("./eng/scripts/setup/install.py"); - await runPython3("./eng/scripts/setup/prepare.py"); + await runPython3(path.join(root, "/eng/scripts/setup/install.py")); + await runPython3(path.join(root, "/eng/scripts/setup/prepare.py")); } catch (error) { if (error instanceof Error && error.message.includes(pythonVersionErrorMsg)) { // if we can't find python with compatible version, we use pyodide instead From 43ab26c95749957ceada80f9d147414f380da315 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Mon, 23 Dec 2024 10:31:48 +0800 Subject: [PATCH 35/39] resolve pip not found issue --- packages/http-client-python/emitter/src/emitter.ts | 9 ++------- .../emitter/src/system-requirements.ts | 4 +--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 349f03c329..4441db2b55 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -14,7 +14,6 @@ import { emitCodeModel } from "./code-model.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js"; import { runPython3 } from "./run-python3.js"; -import { pythonVersionErrorMsg } from "./system-requirements.js"; import { removeUnderscoresFromNamespace } from "./utils.js"; export function getModelsMode(context: SdkContext): "dpg" | "none" { @@ -126,12 +125,8 @@ export async function $onEmit(context: EmitContext) { await runPython3(path.join(root, "/eng/scripts/setup/install.py")); await runPython3(path.join(root, "/eng/scripts/setup/prepare.py")); } catch (error) { - if (error instanceof Error && error.message.includes(pythonVersionErrorMsg)) { - // if we can't find python with compatible version, we use pyodide instead - resolvedOptions["use-pyodide"] = true; - } else { - throw error; - } + // if the python env is not ready, we use pyodide instead + resolvedOptions["use-pyodide"] = true; } } diff --git a/packages/http-client-python/emitter/src/system-requirements.ts b/packages/http-client-python/emitter/src/system-requirements.ts index 141b39e984..7f12ff5b5a 100644 --- a/packages/http-client-python/emitter/src/system-requirements.ts +++ b/packages/http-client-python/emitter/src/system-requirements.ts @@ -193,13 +193,11 @@ export const patchPythonPath = async ( const [_, ...args] = command; const resolution = await resolvePythonRequirement(requirement); if ("error" in resolution) { - throw new Error(`${pythonVersionErrorMsg} ${resolution.message}`); + throw new Error(`Failed to find compatible python version. ${resolution.message}`); } return [resolution.command, ...(resolution.additionalArgs ?? []), ...args]; }; -export const pythonVersionErrorMsg = "Failed to find compatible python version."; - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // TYPES const PythonRequirement = "python"; From cbe96e6366e38fd37595d4a23074a214e20e96a5 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Mon, 23 Dec 2024 15:44:17 +0800 Subject: [PATCH 36/39] refine ci script --- .../eng/scripts/Test-Packages.ps1 | 13 ++++++++----- .../eng/scripts/setup/install.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/http-client-python/eng/scripts/Test-Packages.ps1 b/packages/http-client-python/eng/scripts/Test-Packages.ps1 index 3b604f2792..97c1056bcd 100644 --- a/packages/http-client-python/eng/scripts/Test-Packages.ps1 +++ b/packages/http-client-python/eng/scripts/Test-Packages.ps1 @@ -38,8 +38,14 @@ try { & "$packageRoot/eng/scripts/Generate-WithPyodide.ps1" Write-Host 'Code generation is completed.' - # force add updates - Invoke-LoggedCommand "git add $packageRoot/generator/test -f" + try { + Write-Host 'Checking for differences in generated code...' + & "$packageRoot/eng/scripts/Check-GitChanges.ps1" + Write-Host 'Done. No code generation differences detected.' + } + catch { + Write-Error 'Generated code is not up to date. Please run: eng/scripts/Generate.ps1' + } Write-Host "Generating test projects with venv ..." & "$packageRoot/eng/scripts/Generate.ps1" @@ -54,9 +60,6 @@ try { Write-Error 'Generated code is not up to date. Please run: eng/scripts/Generate.ps1' } - # reset force updates - Invoke-LoggedCommand "git reset ." - try { Write-Host "Pip List" & pip list diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 7cd266bbdc..4dcb1225c5 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -1,3 +1,7 @@ +import fs from "fs"; +import path, { dirname } from "path"; +import { loadPyodide } from "pyodide"; +import { fileURLToPath } from "url"; import { runPython3 } from "./run-python3.js"; async function main() { @@ -6,7 +10,21 @@ async function main() { console.log("Found Python on your local environment and created a venv with all requirements."); // eslint-disable-line no-console } catch (error) { console.log("No Python found on your local environment. We will use Pyodide instead."); // eslint-disable-line no-console + } finally { + await installPyodideDeps(); + console.log("Successfully installed all required Python packages in Pyodide"); // eslint-disable-line no-console } } +async function installPyodideDeps() { + const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); + await pyodide.loadPackage("micropip"); + const micropip = pyodide.pyimport("micropip"); + const requirementsPath = path.join(root, "generator", "requirements.txt"); + const requirementsText = fs.readFileSync(requirementsPath, "utf-8"); + const requirementsArray = requirementsText.split("\n").filter((line) => line.trim() !== ""); + await micropip.install(requirementsArray); +} + main(); From 161f92d6df800447a4407af649ee1a57e7820ef1 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Mon, 23 Dec 2024 16:02:12 +0800 Subject: [PATCH 37/39] fix path --- packages/http-client-python/eng/scripts/setup/install.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/eng/scripts/setup/install.ts b/packages/http-client-python/eng/scripts/setup/install.ts index 4dcb1225c5..62940075b8 100644 --- a/packages/http-client-python/eng/scripts/setup/install.ts +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -18,7 +18,9 @@ async function main() { async function installPyodideDeps() { const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); - const pyodide = await loadPyodide({ indexURL: path.join(root, "node_modules", "pyodide") }); + const pyodide = await loadPyodide({ + indexURL: path.dirname(fileURLToPath(import.meta.resolve("pyodide"))), + }); await pyodide.loadPackage("micropip"); const micropip = pyodide.pyimport("micropip"); const requirementsPath = path.join(root, "generator", "requirements.txt"); From 1589a897e8d5a1811bdeea7e59ecf43000cf197e Mon Sep 17 00:00:00 2001 From: tadelesh Date: Tue, 24 Dec 2024 16:03:14 +0800 Subject: [PATCH 38/39] run test for both generation --- .../eng/scripts/Test-Packages.ps1 | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/http-client-python/eng/scripts/Test-Packages.ps1 b/packages/http-client-python/eng/scripts/Test-Packages.ps1 index 97c1056bcd..eaae16718d 100644 --- a/packages/http-client-python/eng/scripts/Test-Packages.ps1 +++ b/packages/http-client-python/eng/scripts/Test-Packages.ps1 @@ -47,6 +47,16 @@ try { Write-Error 'Generated code is not up to date. Please run: eng/scripts/Generate.ps1' } + try { + # Run test + Write-Host 'Running tests based on generated code with pyodide' + & npm run ci + Write-Host 'All tests passed' + } + catch { + Write-Error "Tests failed: $_" + } + Write-Host "Generating test projects with venv ..." & "$packageRoot/eng/scripts/Generate.ps1" Write-Host 'Code generation is completed.' @@ -61,15 +71,13 @@ try { } try { - Write-Host "Pip List" - & pip list - # Run tox - Write-Host 'Running tests' + # Run test + Write-Host 'Running tests based on generated code with venv' & npm run ci - Write-Host 'tox tests passed' + Write-Host 'All tests passed' } catch { - Write-Error "Cadl ranch tests failed: $_" + Write-Error "Tests failed: $_" } } } From 0db802e28dcc6de7be1d08abcf5cee02582b633e Mon Sep 17 00:00:00 2001 From: tadelesh Date: Tue, 24 Dec 2024 16:20:03 +0800 Subject: [PATCH 39/39] regen with half pyodide --- .../eng/scripts/Generate-WithPyodide.ps1 | 11 ------ .../eng/scripts/Test-Packages.ps1 | 37 ++++--------------- .../eng/scripts/ci/regenerate.ts | 4 +- 3 files changed, 10 insertions(+), 42 deletions(-) delete mode 100644 packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 diff --git a/packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 b/packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 deleted file mode 100644 index c56126bd74..0000000000 --- a/packages/http-client-python/eng/scripts/Generate-WithPyodide.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -#Requires -Version 7.0 - -Import-Module "$PSScriptRoot\Generation.psm1" -DisableNameChecking -Force; - -$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..') - -Write-Host "Building project ..." -& npm run build - -Write-Host "Regenerating project with Pyodide ..." -& npm run regenerate -- --pyodide diff --git a/packages/http-client-python/eng/scripts/Test-Packages.ps1 b/packages/http-client-python/eng/scripts/Test-Packages.ps1 index eaae16718d..0df7f33952 100644 --- a/packages/http-client-python/eng/scripts/Test-Packages.ps1 +++ b/packages/http-client-python/eng/scripts/Test-Packages.ps1 @@ -34,30 +34,7 @@ try { Set-StrictMode -Version 1 # run E2E Test for TypeSpec emitter - Write-Host "Generating test projects with pyodide ..." - & "$packageRoot/eng/scripts/Generate-WithPyodide.ps1" - Write-Host 'Code generation is completed.' - - try { - Write-Host 'Checking for differences in generated code...' - & "$packageRoot/eng/scripts/Check-GitChanges.ps1" - Write-Host 'Done. No code generation differences detected.' - } - catch { - Write-Error 'Generated code is not up to date. Please run: eng/scripts/Generate.ps1' - } - - try { - # Run test - Write-Host 'Running tests based on generated code with pyodide' - & npm run ci - Write-Host 'All tests passed' - } - catch { - Write-Error "Tests failed: $_" - } - - Write-Host "Generating test projects with venv ..." + Write-Host "Generating test projects ..." & "$packageRoot/eng/scripts/Generate.ps1" Write-Host 'Code generation is completed.' @@ -67,17 +44,19 @@ try { Write-Host 'Done. No code generation differences detected.' } catch { - Write-Error 'Generated code is not up to date. Please run: eng/scripts/Generate.ps1' + Write-Error 'Generated code is not up to date. Please run: eng/Generate.ps1' } try { - # Run test - Write-Host 'Running tests based on generated code with venv' + Write-Host "Pip List" + & pip list + # Run tox + Write-Host 'Running tests' & npm run ci - Write-Host 'All tests passed' + Write-Host 'tox tests passed' } catch { - Write-Error "Tests failed: $_" + Write-Error "Cadl ranch tests failed: $_" } } } diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 000a3a9400..aeb9adb05b 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -286,8 +286,8 @@ function _getCmdList(spec: string, flags: RegenerateFlags): TspCommand[] { async function regenerate(flags: RegenerateFlagsInput): Promise { if (flags.flavor === undefined) { - await regenerate({ ...flags, flavor: "azure" }); - await regenerate({ ...flags, flavor: "unbranded" }); + await regenerate({ flavor: "azure", ...flags }); + await regenerate({ flavor: "unbranded", pyodide: true, ...flags, }); } else { const flagsResolved = { debug: false, flavor: flags.flavor, ...flags }; const CADL_RANCH_DIR = resolve(PLUGIN_DIR, "node_modules/@azure-tools/cadl-ranch-specs/http");