mirror of
https://github.com/gradio-app/gradio.git
synced 2024-11-27 01:40:20 +08:00
Lite: Add methods to run Python files mounted on a virtual file system, and unmount() method (#4785)
* Create WorkerProxy.runPythonFile() to run a mounted Python file and rename .runPythonAsync() to .runPythonCode() * Fix lite/index.html and app/lite/index.ts adjusting the new option name * Add changeset * Add controller.unmount() * Add changeset --------- Co-authored-by: pngwn <hello@pngwn.io>
This commit is contained in:
parent
8d0d4e0a8e
commit
da0e94479a
5
.changeset/large-items-reflect.md
Normal file
5
.changeset/large-items-reflect.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gradio/lite": minor
|
||||
---
|
||||
|
||||
Add methods to execute mounted Python files
|
5
.changeset/smooth-ways-battle.md
Normal file
5
.changeset/smooth-ways-battle.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gradio/lite": patch
|
||||
---
|
||||
|
||||
Add controller.unmount()
|
@ -29,18 +29,21 @@ declare let GRADIO_VERSION: string;
|
||||
// const ENTRY_CSS = "__ENTRY_CSS__";
|
||||
|
||||
interface GradioAppController {
|
||||
rerun: (code: string) => Promise<void>;
|
||||
run_code: (code: string) => Promise<void>;
|
||||
run_file: (path: string) => Promise<void>;
|
||||
write: (path: string, data: string | ArrayBufferView, opts: any) => Promise<void>;
|
||||
rename: (old_path: string, new_path: string) => Promise<void>;
|
||||
unlink: (path: string) => Promise<void>;
|
||||
install: (requirements: string[]) => Promise<void>;
|
||||
unmount: () => void;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
target: HTMLElement;
|
||||
files?: WorkerProxyOptions["files"];
|
||||
requirements?: WorkerProxyOptions["requirements"];
|
||||
pyCode: string;
|
||||
code?: string;
|
||||
entrypoint?: string;
|
||||
info: boolean;
|
||||
container: boolean;
|
||||
isEmbed: boolean;
|
||||
@ -67,11 +70,17 @@ export function create(options: Options): GradioAppController {
|
||||
requirements: options.requirements ?? [],
|
||||
});
|
||||
|
||||
// Internally, the execution of `runPythonAsync()` is queued
|
||||
// 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)
|
||||
// So we don't await this promise because we want to mount the `Index` immediately and start the app initialization asynchronously.
|
||||
worker_proxy.runPythonAsync(options.pyCode);
|
||||
if (options.code != null) {
|
||||
worker_proxy.runPythonCode(options.code);
|
||||
} else if (options.entrypoint != null) {
|
||||
worker_proxy.runPythonFile(options.entrypoint);
|
||||
} else {
|
||||
throw new Error("Either code or entrypoint must be provided.");
|
||||
}
|
||||
|
||||
mount_prebuilt_css(document.head);
|
||||
|
||||
@ -122,8 +131,12 @@ export function create(options: Options): GradioAppController {
|
||||
launchNewApp();
|
||||
|
||||
return {
|
||||
rerun: async (code: string): Promise<void> => {
|
||||
await worker_proxy.runPythonAsync(code);
|
||||
run_code: async (code: string): Promise<void> => {
|
||||
await worker_proxy.runPythonCode(code);
|
||||
launchNewApp();
|
||||
},
|
||||
run_file: async (path: string): Promise<void> => {
|
||||
await worker_proxy.runPythonFile(path);
|
||||
launchNewApp();
|
||||
},
|
||||
write(path, data, opts) {
|
||||
@ -137,6 +150,10 @@ export function create(options: Options): GradioAppController {
|
||||
},
|
||||
install(requirements) {
|
||||
return worker_proxy.install(requirements);
|
||||
},
|
||||
unmount() {
|
||||
app.$destroy();
|
||||
worker_proxy.terminate();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -186,7 +203,7 @@ if (BUILD_MODE === "dev") {
|
||||
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/guides/assets/logo.png"
|
||||
}
|
||||
},
|
||||
pyCode: initial_code,
|
||||
code: initial_code,
|
||||
requirements: initial_requirements,
|
||||
info: true,
|
||||
container: true,
|
||||
@ -201,7 +218,7 @@ if (BUILD_MODE === "dev") {
|
||||
|
||||
exec_button.onclick = () => {
|
||||
console.debug("exec_button.onclick");
|
||||
controller.rerun(code_input.value);
|
||||
controller.run_code(code_input.value);
|
||||
console.debug("Rerun finished")
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
<script type="module"> // type="module" is necessary to use `createGradioApp()`, which is loaded with <script type="module" /> tag above.
|
||||
createGradioApp({
|
||||
target: document.getElementById("gradio-app"),
|
||||
pyCode: `
|
||||
code: `
|
||||
import gradio as gr
|
||||
|
||||
def greet(name):
|
||||
|
@ -33,12 +33,18 @@ export interface InMessageInit extends InMessageBase {
|
||||
requirements: string[];
|
||||
};
|
||||
}
|
||||
export interface InMessageRunPython extends InMessageBase {
|
||||
type: "run-python";
|
||||
export interface InMessageRunPythonCode extends InMessageBase {
|
||||
type: "run-python-code";
|
||||
data: {
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
export interface InMessageRunPythonFile extends InMessageBase {
|
||||
type: "run-python-file";
|
||||
data: {
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
export interface InMessageHttpRequest extends InMessageBase {
|
||||
type: "http-request";
|
||||
data: {
|
||||
@ -81,7 +87,8 @@ export interface InMessageEcho extends InMessageBase {
|
||||
|
||||
export type InMessage =
|
||||
| InMessageInit
|
||||
| InMessageRunPython
|
||||
| InMessageRunPythonCode
|
||||
| InMessageRunPythonFile
|
||||
| InMessageHttpRequest
|
||||
| InMessageFileWrite
|
||||
| InMessageFileRename
|
||||
|
@ -11,6 +11,7 @@ import type {
|
||||
import { writeFileWithParents, renameWithParents } from "./file";
|
||||
import { verifyRequirements } from "./requirements";
|
||||
import { makeHttpRequest } from "./http";
|
||||
import scriptRunnerPySource from "./py/script_runner.py?raw"
|
||||
|
||||
importScripts("https://cdn.jsdelivr.net/pyodide/v0.23.2/full/pyodide.js");
|
||||
|
||||
@ -23,6 +24,7 @@ let call_asgi_app_from_js: (
|
||||
receive: () => Promise<unknown>,
|
||||
send: (event: any) => Promise<void>
|
||||
) => Promise<void>;
|
||||
let run_script: (path: string) => void;
|
||||
|
||||
async function loadPyodideAndPackages(options: InMessageInit["data"]): Promise<void> {
|
||||
console.debug("Loading Pyodide.");
|
||||
@ -156,6 +158,11 @@ import matplotlib
|
||||
matplotlib.use("agg")
|
||||
`);
|
||||
console.debug("matplotlib backend is set.");
|
||||
|
||||
console.debug("Set up a script runner");
|
||||
await pyodide.runPythonAsync(scriptRunnerPySource);
|
||||
run_script = pyodide.globals.get("_run_script");
|
||||
console.debug("A script runner is set up.");
|
||||
}
|
||||
|
||||
self.onmessage = async (event: MessageEvent<InMessage>): Promise<void> => {
|
||||
@ -191,7 +198,7 @@ self.onmessage = async (event: MessageEvent<InMessage>): Promise<void> => {
|
||||
messagePort.postMessage(replyMessage);
|
||||
break;
|
||||
}
|
||||
case "run-python": {
|
||||
case "run-python-code": {
|
||||
await pyodide.runPythonAsync(msg.data.code);
|
||||
const replyMessage: ReplyMessageSuccess = {
|
||||
type: "reply:success",
|
||||
@ -200,6 +207,16 @@ self.onmessage = async (event: MessageEvent<InMessage>): Promise<void> => {
|
||||
messagePort.postMessage(replyMessage);
|
||||
break;
|
||||
}
|
||||
case "run-python-file": {
|
||||
run_script(msg.data.path);
|
||||
|
||||
const replyMessage: ReplyMessageSuccess = {
|
||||
type: "reply:success",
|
||||
data: null
|
||||
};
|
||||
messagePort.postMessage(replyMessage);
|
||||
break;
|
||||
}
|
||||
case "http-request": {
|
||||
const request = msg.data.request;
|
||||
const response = await makeHttpRequest(call_asgi_app_from_js, request);
|
||||
|
3
js/wasm/src/webworker/py/.editorconfig
Normal file
3
js/wasm/src/webworker/py/.editorconfig
Normal file
@ -0,0 +1,3 @@
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
120
js/wasm/src/webworker/py/script_runner.py
Normal file
120
js/wasm/src/webworker/py/script_runner.py
Normal file
@ -0,0 +1,120 @@
|
||||
import tokenize
|
||||
import types
|
||||
import sys
|
||||
|
||||
# BSD 3-Clause License
|
||||
#
|
||||
# - Copyright (c) 2008-Present, IPython Development Team
|
||||
# - Copyright (c) 2001-2007, Fernando Perez <fernando.perez@colorado.edu>
|
||||
# - Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
|
||||
# - Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
|
||||
#
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Code modified from IPython (BSD license)
|
||||
# Source: https://github.com/ipython/ipython/blob/master/IPython/utils/syspathcontext.py#L42
|
||||
class modified_sys_path:
|
||||
"""A context for prepending a directory to sys.path for a second."""
|
||||
|
||||
def __init__(self, script_path: str):
|
||||
self._script_path = script_path
|
||||
self._added_path = False
|
||||
|
||||
def __enter__(self):
|
||||
if self._script_path not in sys.path:
|
||||
sys.path.insert(0, self._script_path)
|
||||
self._added_path = True
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self._added_path:
|
||||
try:
|
||||
sys.path.remove(self._script_path)
|
||||
except ValueError:
|
||||
# It's already removed.
|
||||
pass
|
||||
|
||||
# Returning False causes any exceptions to be re-raised.
|
||||
return False
|
||||
|
||||
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
def _new_module(name: str) -> types.ModuleType:
|
||||
"""Create a new module with the given name."""
|
||||
return types.ModuleType(name)
|
||||
|
||||
|
||||
def _run_script(script_path: str) -> None:
|
||||
# This function is based on the following code from Streamlit:
|
||||
# https://github.com/streamlit/streamlit/blob/1.24.0/lib/streamlit/runtime/scriptrunner/script_runner.py#L519-L554
|
||||
|
||||
with tokenize.open(script_path) as f:
|
||||
filebody = f.read()
|
||||
|
||||
# NOTE: In Streamlit, the bytecode caching mechanism has been introduced.
|
||||
# However, we skipped it here for simplicity and because Gradio doesn't need to rerun the script so frequently,
|
||||
# while we may do it in the future.
|
||||
bytecode = compile( # type: ignore
|
||||
filebody,
|
||||
# Pass in the file path so it can show up in exceptions.
|
||||
script_path,
|
||||
# We're compiling entire blocks of Python, so we need "exec"
|
||||
# mode (as opposed to "eval" or "single").
|
||||
mode="exec",
|
||||
# Don't inherit any flags or "future" statements.
|
||||
flags=0,
|
||||
dont_inherit=1,
|
||||
# Use the default optimization options.
|
||||
optimize=-1,
|
||||
)
|
||||
|
||||
module = _new_module("__main__")
|
||||
|
||||
# Install the fake module as the __main__ module. This allows
|
||||
# the pickle module to work inside the user's code, since it now
|
||||
# can know the module where the pickled objects stem from.
|
||||
# IMPORTANT: This means we can't use "if __name__ == '__main__'" in
|
||||
# our code, as it will point to the wrong module!!!
|
||||
sys.modules["__main__"] = module
|
||||
|
||||
# Add special variables to the module's globals dict.
|
||||
module.__dict__["__file__"] = script_path
|
||||
|
||||
with modified_sys_path(script_path):
|
||||
exec(bytecode, module.__dict__)
|
@ -33,22 +33,31 @@ export class WorkerProxy {
|
||||
gradioWheelUrl: options.gradioWheelUrl,
|
||||
gradioClientWheelUrl: options.gradioClientWheelUrl,
|
||||
files: options.files,
|
||||
requirements: options.requirements
|
||||
requirements: options.requirements,
|
||||
}
|
||||
}).then(() => {
|
||||
console.debug("WorkerProxy.constructor(): Initialization is done.");
|
||||
});
|
||||
}
|
||||
|
||||
public async runPythonAsync(code: string): Promise<void> {
|
||||
public async runPythonCode(code: string): Promise<void> {
|
||||
await this.postMessageAsync({
|
||||
type: "run-python",
|
||||
type: "run-python-code",
|
||||
data: {
|
||||
code
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async runPythonFile(path: string): Promise<void> {
|
||||
await this.postMessageAsync({
|
||||
type: "run-python-file",
|
||||
data: {
|
||||
path,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// A wrapper for this.worker.postMessage(). Unlike that function, which
|
||||
// returns void immediately, this function returns a promise, which resolves
|
||||
// when a ReplyMessage is received from the worker.
|
||||
|
Loading…
Reference in New Issue
Block a user