From f63f2d33379eda62820c72462fca92979918b925 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Wed, 16 Oct 2024 23:43:28 +0900 Subject: [PATCH 1/6] Call pyodide.loadPackagesFromImports() for each Python file and dispatch an event including the result --- js/lite/src/LiteIndex.svelte | 8 ++++- js/lite/src/dev/App.svelte | 6 ++++ js/lite/src/index.ts | 57 ++++++++++++++++++++++------------ js/wasm/src/message-types.ts | 10 +++++- js/wasm/src/webworker/index.ts | 52 +++++++++++++++++++++++++++++-- js/wasm/src/worker-proxy.ts | 7 +++++ 6 files changed, 115 insertions(+), 25 deletions(-) diff --git a/js/lite/src/LiteIndex.svelte b/js/lite/src/LiteIndex.svelte index 13ec67b02308..4058d414be6e 100644 --- a/js/lite/src/LiteIndex.svelte +++ b/js/lite/src/LiteIndex.svelte @@ -4,7 +4,7 @@ import "@gradio/theme/pollen.css"; import "@gradio/theme/typography.css"; - import { onDestroy, SvelteComponent } from "svelte"; + import { onDestroy, SvelteComponent, createEventDispatcher } from "svelte"; import Index from "@self/spa"; import Playground from "./Playground.svelte"; import ErrorDisplay from "./ErrorDisplay.svelte"; @@ -94,6 +94,12 @@ error = (event as CustomEvent).detail; }); + const dispatch = createEventDispatcher(); + + worker_proxy.addEventListener("modules-auto-loaded", (event) => { + dispatch("modules-auto-loaded", (event as CustomEvent).detail); + }); + // Internally, the execution of `runPythonCode()` or `runPythonFile()` is queued // and its promise will be resolved after the Pyodide is loaded and the worker initialization is done // (see the await in the `onmessage` callback in the webworker code) diff --git a/js/lite/src/dev/App.svelte b/js/lite/src/dev/App.svelte index 8c82dd151ca2..f7b52aa83331 100644 --- a/js/lite/src/dev/App.svelte +++ b/js/lite/src/dev/App.svelte @@ -79,6 +79,12 @@ def hi(name): playground: false, layout: null }); + controller.addEventListener("modules-auto-loaded", (event) => { + const packages = (event as CustomEvent).detail as { name: string }[]; + const packageNames = packages.map((pkg) => pkg.name); + requirements_txt += + "\n" + packageNames.map((line) => line + " # auto-loaded").join("\n"); + }); }); onDestroy(() => { controller.unmount(); diff --git a/js/lite/src/index.ts b/js/lite/src/index.ts index 4b039745fdee..dee6d40281b5 100644 --- a/js/lite/src/index.ts +++ b/js/lite/src/index.ts @@ -21,18 +21,45 @@ import LiteIndex from "./LiteIndex.svelte"; // As a result, the users of the Wasm app will have to load the CSS file manually. // const ENTRY_CSS = "__ENTRY_CSS__"; -export interface GradioAppController { - run_code: (code: string) => Promise; - run_file: (path: string) => Promise; - write: ( +export class GradioAppController extends EventTarget { + constructor(private lite_svelte_app: LiteIndex) { + super(); + + this.lite_svelte_app.$on("error", (event: CustomEvent) => { + this.dispatchEvent(new CustomEvent("error", { detail: event.detail })); + }); + this.lite_svelte_app.$on("modules-auto-loaded", (event: CustomEvent) => { + this.dispatchEvent( + new CustomEvent("modules-auto-loaded", { detail: event.detail }) + ); + }); + } + + run_code(code: string): Promise { + return this.lite_svelte_app.run_code(code); + } + run_file(path: string): Promise { + return this.lite_svelte_app.run_file(path); + } + write( path: string, data: string | ArrayBufferView, opts: any - ) => Promise; - rename: (old_path: string, new_path: string) => Promise; - unlink: (path: string) => Promise; - install: (requirements: string[]) => Promise; - unmount: () => void; + ): Promise { + return this.lite_svelte_app.write(path, data, opts); + } + rename(old_path: string, new_path: string): Promise { + return this.lite_svelte_app.rename(old_path, new_path); + } + unlink(path: string): Promise { + return this.lite_svelte_app.unlink(path); + } + install(requirements: string[]): Promise { + return this.lite_svelte_app.install(requirements); + } + unmount(): void { + this.lite_svelte_app.$destroy(); + } } export interface Options { @@ -88,17 +115,7 @@ export function create(options: Options): GradioAppController { } }); - return { - run_code: app.run_code, - run_file: app.run_file, - write: app.write, - rename: app.rename, - unlink: app.unlink, - install: app.install, - unmount() { - app.$destroy(); - } - }; + return new GradioAppController(app); } /** diff --git a/js/wasm/src/message-types.ts b/js/wasm/src/message-types.ts index 8649c9eaf431..24996fbe5a9a 100644 --- a/js/wasm/src/message-types.ts +++ b/js/wasm/src/message-types.ts @@ -1,4 +1,5 @@ import type { ASGIScope } from "./asgi-types"; +import type { PackageData } from "pyodide"; export interface EmscriptenFile { data: string | ArrayBufferView; @@ -113,4 +114,11 @@ export interface OutMessageProgressUpdate extends OutMessageBase { log: string; }; } -export type OutMessage = OutMessageProgressUpdate; +export interface OutMessageModulesAutoLoaded extends OutMessageBase { + type: "modules-auto-loaded"; + data: { + packages: PackageData[]; + }; +} + +export type OutMessage = OutMessageProgressUpdate | OutMessageModulesAutoLoaded; diff --git a/js/wasm/src/webworker/index.ts b/js/wasm/src/webworker/index.ts index 888fafe70cab..30f6b9f0ce7b 100644 --- a/js/wasm/src/webworker/index.ts +++ b/js/wasm/src/webworker/index.ts @@ -2,6 +2,7 @@ /* eslint-env worker */ import type { + PackageData, PyodideInterface, loadPyodide as loadPyodideValue } from "pyodide"; @@ -186,7 +187,8 @@ anyio.to_thread.run_sync = mocked_anyio_to_thread_run_sync async function initializeApp( appId: string, options: InMessageInitApp["data"], - updateProgress: (log: string) => void + updateProgress: (log: string) => void, + onModulesAutoLoaded: (packages: PackageData[]) => void ): Promise { const appHomeDir = getAppHomeDir(appId); console.debug("Creating a home directory for the app.", { @@ -197,6 +199,7 @@ async function initializeApp( console.debug("Mounting files.", options.files); updateProgress("Mounting files"); + const pythonFileContents: string[] = []; await Promise.all( Object.keys(options.files).map(async (path) => { const file = options.files[path]; @@ -215,6 +218,10 @@ async function initializeApp( const appifiedPath = resolveAppHomeBasedPath(appId, path); console.debug(`Write a file "${appifiedPath}"`); writeFileWithParents(pyodide, appifiedPath, data, opts); + + if (typeof data === "string" && path.endsWith(".py")) { + pythonFileContents.push(data); + } }) ); console.debug("Files are mounted."); @@ -224,7 +231,22 @@ async function initializeApp( await micropip.install.callKwargs(options.requirements, { keep_going: true }); console.debug("Packages are installed."); - if (options.requirements.includes("matplotlib")) { + console.debug("Auto-loading modules."); + const loadedPackagesArr = await Promise.all( + pythonFileContents.map((source) => pyodide.loadPackagesFromImports(source)) + ); + const loadedPackagesSet = new Set(loadedPackagesArr.flat()); // Remove duplicates + const loadedPackages = Array.from(loadedPackagesSet); + if (loadedPackages.length > 0) { + onModulesAutoLoaded(loadedPackages); + } + const loadedPackageNames = loadedPackages.map((pkg) => pkg.name); + console.debug("Modules are auto-loaded.", loadedPackages); + + if ( + options.requirements.includes("matplotlib") || + loadedPackageNames.includes("matplotlib") + ) { console.debug("Setting matplotlib backend."); updateProgress("Setting matplotlib backend"); // Ref: https://github.com/pyodide/pyodide/issues/561#issuecomment-1992613717 @@ -285,6 +307,15 @@ function setupMessageHandler(receiver: MessageTransceiver): void { }; receiver.postMessage(message); }; + const onModulesAutoLoaded = (packages: PackageData[]) => { + const message: OutMessage = { + type: "modules-auto-loaded", + data: { + packages + } + }; + receiver.postMessage(message); + }; // App initialization is per app or receiver, so its promise is managed in this scope. let appReadyPromise: Promise | undefined = undefined; @@ -331,7 +362,12 @@ function setupMessageHandler(receiver: MessageTransceiver): void { await envReadyPromise; if (msg.type === "init-app") { - appReadyPromise = initializeApp(appId, msg.data, updateProgress); + appReadyPromise = initializeApp( + appId, + msg.data, + updateProgress, + onModulesAutoLoaded + ); const replyMessage: ReplyMessageSuccess = { type: "reply:success", @@ -391,6 +427,16 @@ function setupMessageHandler(receiver: MessageTransceiver): void { case "file:write": { const { path, data: fileData, opts } = msg.data; + if (typeof fileData === "string" && path.endsWith(".py")) { + console.debug(`Auto install the requirements in ${path}`); + const loadedPackages = + await pyodide.loadPackagesFromImports(fileData); + if (loadedPackages.length > 0) { + onModulesAutoLoaded(loadedPackages); + } + console.debug("Modules are auto-loaded.", loadedPackages); + } + const appifiedPath = resolveAppHomeBasedPath(appId, path); console.debug(`Write a file "${appifiedPath}"`); diff --git a/js/wasm/src/worker-proxy.ts b/js/wasm/src/worker-proxy.ts index 03304ba173f1..484bf852bb46 100644 --- a/js/wasm/src/worker-proxy.ts +++ b/js/wasm/src/worker-proxy.ts @@ -166,6 +166,13 @@ export class WorkerProxy extends EventTarget { ); break; } + case "modules-auto-loaded": { + this.dispatchEvent( + new CustomEvent("modules-auto-loaded", { + detail: msg.data.packages + }) + ); + } } } From 8638260299a11ebc970ba0cbdb090f912612d377 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Thu, 17 Oct 2024 00:10:32 +0900 Subject: [PATCH 2/6] Fix --- js/lite/src/index.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/js/lite/src/index.ts b/js/lite/src/index.ts index dee6d40281b5..7b0713eb678c 100644 --- a/js/lite/src/index.ts +++ b/js/lite/src/index.ts @@ -35,31 +35,31 @@ export class GradioAppController extends EventTarget { }); } - run_code(code: string): Promise { + run_code = (code: string): Promise => { return this.lite_svelte_app.run_code(code); - } - run_file(path: string): Promise { + }; + run_file = (path: string): Promise => { return this.lite_svelte_app.run_file(path); - } - write( + }; + write = ( path: string, data: string | ArrayBufferView, opts: any - ): Promise { + ): Promise => { return this.lite_svelte_app.write(path, data, opts); - } - rename(old_path: string, new_path: string): Promise { + }; + rename = (old_path: string, new_path: string): Promise => { return this.lite_svelte_app.rename(old_path, new_path); - } - unlink(path: string): Promise { + }; + unlink = (path: string): Promise => { return this.lite_svelte_app.unlink(path); - } - install(requirements: string[]): Promise { + }; + install = (requirements: string[]): Promise => { return this.lite_svelte_app.install(requirements); - } - unmount(): void { + }; + unmount = (): void => { this.lite_svelte_app.$destroy(); - } + }; } export interface Options { From 333f6313ab7c77183718f3c4c819686c5c28f0f3 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Thu, 17 Oct 2024 00:45:50 +0900 Subject: [PATCH 3/6] Call pyodide.loadPackagesFromImports from run_code --- js/wasm/src/webworker/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js/wasm/src/webworker/index.ts b/js/wasm/src/webworker/index.ts index 30f6b9f0ce7b..ce42ec584089 100644 --- a/js/wasm/src/webworker/index.ts +++ b/js/wasm/src/webworker/index.ts @@ -394,6 +394,15 @@ function setupMessageHandler(receiver: MessageTransceiver): void { case "run-python-code": { unload_local_modules(); + console.debug(`Auto install the requirements`); + const loadedPackages = await pyodide.loadPackagesFromImports( + msg.data.code + ); + if (loadedPackages.length > 0) { + onModulesAutoLoaded(loadedPackages); + } + console.debug("Modules are auto-loaded.", loadedPackages); + await run_code(appId, getAppHomeDir(appId), msg.data.code); const replyMessage: ReplyMessageSuccess = { From 34e7d42c7b82ced930f99097c213be3c4dfec7ae Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Thu, 17 Oct 2024 00:47:20 +0900 Subject: [PATCH 4/6] Update DemosLite to listen the modules-auto-loaded event and upload the requirements automatically --- js/_website/src/lib/components/DemosLite.svelte | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/js/_website/src/lib/components/DemosLite.svelte b/js/_website/src/lib/components/DemosLite.svelte index 652fd1c84ce1..8f914d002dd5 100644 --- a/js/_website/src/lib/components/DemosLite.svelte +++ b/js/_website/src/lib/components/DemosLite.svelte @@ -198,7 +198,7 @@ let controller: { run_code: (code: string) => Promise; install: (requirements: string[]) => Promise; - }; + } & EventTarget; function debounce( func: (...args: T) => Promise, @@ -251,6 +251,14 @@ debounced_run_code = debounce(controller.run_code, debounce_timeout); debounced_install = debounce(controller.install, debounce_timeout); + controller.addEventListener("modules-auto-loaded", (event) => { + console.debug("Modules auto-loaded", event); + const packages = (event as CustomEvent).detail as { name: string }[]; + const packageNames = packages.map((pkg) => pkg.name); + selected_demo.requirements = + selected_demo.requirements.concat(packageNames); + }); + mounted = true; } catch (error) { console.error("Error loading Gradio Lite:", error); From 645297c8fcfece2f2ca955d16d60aead2f849aa2 Mon Sep 17 00:00:00 2001 From: gradio-pr-bot Date: Wed, 16 Oct 2024 15:49:47 +0000 Subject: [PATCH 5/6] add changeset --- .changeset/heavy-dogs-start.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/heavy-dogs-start.md diff --git a/.changeset/heavy-dogs-start.md b/.changeset/heavy-dogs-start.md new file mode 100644 index 000000000000..46f9f1227ec8 --- /dev/null +++ b/.changeset/heavy-dogs-start.md @@ -0,0 +1,8 @@ +--- +"@gradio/lite": minor +"@gradio/wasm": minor +"gradio": minor +"website": minor +--- + +feat:Lite auto load modules From 5a4cfea44bd208fa90c7edb0b66e8b8d58376fc8 Mon Sep 17 00:00:00 2001 From: gradio-pr-bot Date: Wed, 16 Oct 2024 16:01:59 +0000 Subject: [PATCH 6/6] add changeset --- .changeset/heavy-dogs-start.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/heavy-dogs-start.md b/.changeset/heavy-dogs-start.md index 46f9f1227ec8..df5d2e939ab0 100644 --- a/.changeset/heavy-dogs-start.md +++ b/.changeset/heavy-dogs-start.md @@ -5,4 +5,4 @@ "website": minor --- -feat:Lite auto load modules +feat:Lite auto-load imported modules with `pyodide.loadPackagesFromImports`