diff --git a/cspell.yaml b/cspell.yaml index ecc77a9a929..97a025dea0e 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -47,6 +47,7 @@ words: - debugpy - Declipse - Dedupes + - deps - destructures - devdiv - Diagnoser @@ -132,6 +133,7 @@ words: - nanos - nexted - nihao + - NODEFS - noformat - noopener - noreferrer @@ -164,12 +166,15 @@ words: - pwsh - pyexpat - pygen + - pyimport - pylint - pylintrc + - pyodide - pyproject - pyright - pyrightconfig - pytest + - pyyaml - rcfile - reactivex - recase diff --git a/packages/http-client-python/CHANGELOG.md b/packages/http-client-python/CHANGELOG.md index f575b7e5e97..0ffca955794 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.5.0 + +### Features + +- Add support for generation in enviroments without a Python installation + ## 0.4.4 ### Bug Fixes diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index 906ef1e69cc..4441db2b550 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -8,10 +8,12 @@ import { EmitContext, NoTarget } from "@typespec/compiler"; import { execSync } 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,51 +87,114 @@ export async function $onEmit(context: EmitContext) { }); return; } - - addDefaultOptions(sdkContext); const yamlPath = await saveCodeModelAsYaml("python-yaml-path", yamlMap); - let venvPath = path.join(root, "venv"); - if (fs.existsSync(path.join(venvPath, "bin"))) { - venvPath = path.join(venvPath, "bin", "python"); - } else if (fs.existsSync(path.join(venvPath, "Scripts"))) { - venvPath = path.join(venvPath, "Scripts", "python.exe"); - } else { - throw new Error("Virtual environment doesn't exist."); - } - const commandArgs = [ - venvPath, - `${root}/eng/scripts/setup/run_tsp.py`, - `--output-folder=${outputDir}`, - `--cadl-file=${yamlPath}`, - ]; + addDefaultOptions(sdkContext); const resolvedOptions = sdkContext.emitContext.options; + 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 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(path.join(root, "/eng/scripts/setup/install.py")); + await runPython3(path.join(root, "/eng/scripts/setup/prepare.py")); + } catch (error) { + // if the python env is not ready, we use pyodide instead + resolvedOptions["use-pyodide"] = true; + } + } + + 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=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(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; + const commandFlags = Object.entries(commandArgs) + .map(([key, value]) => `--${key}=${value}`) + .join(" "); + const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`; + execSync(command); + } } } + +async function setupPyodideCall(root: string) { + 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"); + return pyodide; +} diff --git a/packages/http-client-python/emitter/src/lib.ts b/packages/http-client-python/emitter/src/lib.ts index c79e0ffdf2a..b028aaeadc9 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 00000000000..e4df6e8a714 --- /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 00000000000..7f12ff5b5a7 --- /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/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index 629c29c411a..d8c77f5b83d 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" }, + pyodide: { 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; @@ -280,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"); @@ -289,8 +295,14 @@ async function regenerate(flags: RegenerateFlagsInput): Promise { const cmdList: TspCommand[] = subdirectories.flatMap((subdirectory) => _getCmdList(subdirectory, flagsResolved), ); - const PromiseCommands = cmdList.map((tspCommand) => executeCommand(tspCommand)); - await Promise.all(PromiseCommands); + const chunks: TspCommand[][] = []; + for (let i = 0; i < cmdList.length; i += 10) { + chunks.push(cmdList.slice(i, i + 10)); + } + for (const chunk of chunks) { + const promiseCommands = chunk.map((tspCommand) => executeCommand(tspCommand)); + await Promise.all(promiseCommands); + } } } 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 00000000000..c13770edeb8 --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/build.ts @@ -0,0 +1,16 @@ +import { exec } from "child_process"; +import { runPython3 } from "./run-python3.js"; + +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 + }); +} + +main(); 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 00000000000..79f85e086ae --- /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 8b10f58bde7..d6bd0d9e079 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 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 00000000000..62940075b8a --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/install.ts @@ -0,0 +1,32 @@ +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() { + 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() { + const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + 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"); + 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.py b/packages/http-client-python/eng/scripts/setup/prepare.py index 4c2c46bc03d..4e5ffd432fc 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 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/eng/scripts/setup/prepare.ts b/packages/http-client-python/eng/scripts/setup/prepare.ts new file mode 100644 index 00000000000..32d9157a243 --- /dev/null +++ b/packages/http-client-python/eng/scripts/setup/prepare.ts @@ -0,0 +1,11 @@ +import { runPython3 } from "./run-python3.js"; + +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 + } +} + +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 6de4922a404..e4df6e8a714 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", @@ -18,8 +18,3 @@ 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); -}); diff --git a/packages/http-client-python/generator/requirements.txt b/packages/http-client-python/generator/requirements.txt index bebbbb5a645..9b5c9c12d50 100644 --- a/packages/http-client-python/generator/requirements.txt +++ b/packages/http-client-python/generator/requirements.txt @@ -1,12 +1,7 @@ -black==24.4.0 -click==8.1.3 -docutils==0.19 -Jinja2==3.1.4 -m2r2==0.3.3 -MarkupSafe==2.1.2 -mistune==0.8.4 -pathspec==0.11.1 -platformdirs==3.2.0 +black==24.8.0 +docutils>=0.20.1 +Jinja2==3.1.3 +m2r2==0.3.3.post2 PyYAML==6.0.1 tomli==2.0.1 -setuptools==69.2.0 +setuptools==69.5.1 diff --git a/packages/http-client-python/generator/setup.py b/packages/http-client-python/generator/setup.py index 865617f88d7..74fbd267f8a 100644 --- a/packages/http-client-python/generator/setup.py +++ b/packages/http-client-python/generator/setup.py @@ -48,9 +48,12 @@ ] ), 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", + "docutils>=0.20.1", + "Jinja2==3.1.3", + "m2r2==0.3.3.post2", + "PyYAML==6.0.1", + "tomli==2.0.1", + "setuptools==69.5.1", ], ) diff --git a/packages/http-client-python/generator/test/azure/requirements.txt b/packages/http-client-python/generator/test/azure/requirements.txt index 6b3a176ec81..288969dbf5b 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 a9f92acb816..f4625694f3d 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 diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 64186ede21d..76de8434de3 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.4", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@typespec/http-client-python", - "version": "0.4.4", + "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" }, @@ -5488,6 +5489,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", @@ -7540,6 +7552,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 8d2c61c42b6..2dea1208bf3 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.4.4", + "version": "0.5.0", "author": "Microsoft Corporation", "description": "TypeSpec emitter for Python SDKs", "homepage": "https://typespec.io", @@ -30,13 +30,13 @@ }, "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/pygen", "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", - "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", @@ -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.63.0",