Remove Ruff and Uvicorn in Wasm env (#7744)

* Exclude `ruff` from the requirements list for Wasm env

* Exclude `uvicorn` from the requirements list for Wasm env and fix the related modules not to try to import it when not used

* add changeset

* Fix tests

* add changeset

* Apply formatter

* Remove a test case which became unnecessary in https://github.com/gradio-app/gradio/pull/5267, ref: https://github.com/gradio-app/gradio/pull/7744#issuecomment-2011332287

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Yuichiro Tachibana (Tsuchiya) 2024-03-22 17:32:59 +09:00 committed by GitHub
parent 2efb05ed99
commit d831040032
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 214 additions and 282 deletions

View File

@ -0,0 +1,6 @@
---
"@gradio/wasm": minor
"gradio": minor
---
feat:Remove Ruff and Uvicorn in Wasm env

View File

@ -16,7 +16,7 @@ import huggingface_hub
import pytest
import uvicorn
from fastapi import FastAPI
from gradio.networking import Server
from gradio.http_server import Server
from huggingface_hub import HfFolder
from huggingface_hub.utils import RepositoryNotFoundError

View File

@ -2086,13 +2086,15 @@ Received outputs:
)
wasm_utils.register_app(app)
else:
from gradio import http_server
(
server_name,
server_port,
local_url,
app,
server,
) = networking.start_server(
) = http_server.start_server(
self,
server_name,
server_port,

170
gradio/http_server.py Normal file
View File

@ -0,0 +1,170 @@
from __future__ import annotations
import os
import socket
import threading
import time
from functools import partial
from typing import TYPE_CHECKING
import uvicorn
from uvicorn.config import Config
from gradio.exceptions import ServerFailedToStartError
from gradio.routes import App
from gradio.utils import SourceFileReloader, watchfn
if TYPE_CHECKING: # Only import for type checking (to avoid circular imports).
from gradio.blocks import Blocks
# By default, the local server will try to open on localhost, port 7860.
# If that is not available, then it will try 7861, 7862, ... 7959.
INITIAL_PORT_VALUE = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
TRY_NUM_PORTS = int(os.getenv("GRADIO_NUM_PORTS", "100"))
LOCALHOST_NAME = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1")
should_watch = bool(os.getenv("GRADIO_WATCH_DIRS", ""))
GRADIO_WATCH_DIRS = (
os.getenv("GRADIO_WATCH_DIRS", "").split(",") if should_watch else []
)
GRADIO_WATCH_MODULE_NAME = os.getenv("GRADIO_WATCH_MODULE_NAME", "app")
GRADIO_WATCH_DEMO_NAME = os.getenv("GRADIO_WATCH_DEMO_NAME", "demo")
GRADIO_WATCH_DEMO_PATH = os.getenv("GRADIO_WATCH_DEMO_PATH", "")
class Server(uvicorn.Server):
def __init__(
self, config: Config, reloader: SourceFileReloader | None = None
) -> None:
self.running_app = config.app
super().__init__(config)
self.reloader = reloader
if self.reloader:
self.event = threading.Event()
self.watch = partial(watchfn, self.reloader)
def install_signal_handlers(self):
pass
def run_in_thread(self):
self.thread = threading.Thread(target=self.run, daemon=True)
if self.reloader:
self.watch_thread = threading.Thread(target=self.watch, daemon=True)
self.watch_thread.start()
self.thread.start()
start = time.time()
while not self.started:
time.sleep(1e-3)
if time.time() - start > 5:
raise ServerFailedToStartError(
"Server failed to start. Please check that the port is available."
)
def close(self):
self.should_exit = True
if self.reloader:
self.reloader.stop()
self.watch_thread.join()
self.thread.join()
def start_server(
blocks: Blocks,
server_name: str | None = None,
server_port: int | None = None,
ssl_keyfile: str | None = None,
ssl_certfile: str | None = None,
ssl_keyfile_password: str | None = None,
app_kwargs: dict | None = None,
) -> tuple[str, int, str, App, Server]:
"""Launches a local server running the provided Interface
Parameters:
blocks: The Blocks object to run on the server
server_name: to make app accessible on local network, set this to "0.0.0.0". Can be set by environment variable GRADIO_SERVER_NAME.
server_port: will start gradio app on this port (if available). Can be set by environment variable GRADIO_SERVER_PORT.
auth: If provided, username and password (or list of username-password tuples) required to access the Blocks. Can also provide function that takes username and password and returns True if valid login.
ssl_keyfile: If a path to a file is provided, will use this as the private key file to create a local server running on https.
ssl_certfile: If a path to a file is provided, will use this as the signed certificate for https. Needs to be provided if ssl_keyfile is provided.
ssl_keyfile_password: If a password is provided, will use this with the ssl certificate for https.
app_kwargs: Additional keyword arguments to pass to the gradio.routes.App constructor.
Returns:
port: the port number the server is running on
path_to_local_server: the complete address that the local server can be accessed at
app: the FastAPI app object
server: the server object that is a subclass of uvicorn.Server (used to close the server)
"""
if ssl_keyfile is not None and ssl_certfile is None:
raise ValueError("ssl_certfile must be provided if ssl_keyfile is provided.")
server_name = server_name or LOCALHOST_NAME
url_host_name = "localhost" if server_name == "0.0.0.0" else server_name
# Strip IPv6 brackets from the address if they exist.
# This is needed as http://[::1]:port/ is a valid browser address,
# but not a valid IPv6 address, so asyncio will throw an exception.
if server_name.startswith("[") and server_name.endswith("]"):
host = server_name[1:-1]
else:
host = server_name
app = App.create_app(blocks, app_kwargs=app_kwargs)
server_ports = (
[server_port]
if server_port is not None
else range(INITIAL_PORT_VALUE, INITIAL_PORT_VALUE + TRY_NUM_PORTS)
)
for port in server_ports:
try:
# The fastest way to check if a port is available is to try to bind to it with socket.
# If the port is not available, socket will throw an OSError.
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Really, we should be checking if (server_name, server_port) is available, but
# socket.bind() doesn't seem to throw an OSError with ipv6 addresses, based on my testing.
# Instead, we just check if the port is available on localhost.
s.bind((LOCALHOST_NAME, port))
s.close()
# To avoid race conditions, so we also check if the port by trying to start the uvicorn server.
# If the port is not available, this will throw a ServerFailedToStartError.
config = uvicorn.Config(
app=app,
port=port,
host=host,
log_level="warning",
ssl_keyfile=ssl_keyfile,
ssl_certfile=ssl_certfile,
ssl_keyfile_password=ssl_keyfile_password,
)
reloader = None
if GRADIO_WATCH_DIRS:
change_event = threading.Event()
app.change_event = change_event
reloader = SourceFileReloader(
app=app,
watch_dirs=GRADIO_WATCH_DIRS,
watch_module_name=GRADIO_WATCH_MODULE_NAME,
demo_name=GRADIO_WATCH_DEMO_NAME,
stop_event=threading.Event(),
change_event=change_event,
demo_file=GRADIO_WATCH_DEMO_PATH,
)
server = Server(config=config, reloader=reloader)
server.run_in_thread()
break
except (OSError, ServerFailedToStartError):
pass
else:
raise OSError(
f"Cannot find empty port in range: {min(server_ports)}-{max(server_ports)}. You can specify a different port by setting the GRADIO_SERVER_PORT environment variable or passing the `server_port` parameter to `launch()`."
)
if ssl_keyfile is not None:
path_to_local_server = f"https://{url_host_name}:{port}/"
else:
path_to_local_server = f"http://{url_host_name}:{port}/"
return server_name, port, path_to_local_server, app, server

View File

@ -8,7 +8,7 @@ except ImportError:
pass
import gradio as gr
from gradio.networking import App
from gradio.routes import App
from gradio.utils import BaseReloader

View File

@ -5,218 +5,17 @@ creating tunnels.
from __future__ import annotations
import os
import socket
import threading
import time
import warnings
from functools import partial
from typing import TYPE_CHECKING
import httpx
import uvicorn
from uvicorn.config import Config
from gradio.exceptions import ServerFailedToStartError
from gradio.routes import App
from gradio.routes import App # HACK: to avoid circular import # noqa: F401
from gradio.tunneling import Tunnel
from gradio.utils import SourceFileReloader, watchfn
if TYPE_CHECKING: # Only import for type checking (to avoid circular imports).
from gradio.blocks import Blocks
# By default, the local server will try to open on localhost, port 7860.
# If that is not available, then it will try 7861, 7862, ... 7959.
INITIAL_PORT_VALUE = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
TRY_NUM_PORTS = int(os.getenv("GRADIO_NUM_PORTS", "100"))
LOCALHOST_NAME = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1")
GRADIO_API_SERVER = "https://api.gradio.app/v2/tunnel-request"
GRADIO_SHARE_SERVER_ADDRESS = os.getenv("GRADIO_SHARE_SERVER_ADDRESS")
should_watch = bool(os.getenv("GRADIO_WATCH_DIRS", ""))
GRADIO_WATCH_DIRS = (
os.getenv("GRADIO_WATCH_DIRS", "").split(",") if should_watch else []
)
GRADIO_WATCH_MODULE_NAME = os.getenv("GRADIO_WATCH_MODULE_NAME", "app")
GRADIO_WATCH_DEMO_NAME = os.getenv("GRADIO_WATCH_DEMO_NAME", "demo")
GRADIO_WATCH_DEMO_PATH = os.getenv("GRADIO_WATCH_DEMO_PATH", "")
class Server(uvicorn.Server):
def __init__(
self, config: Config, reloader: SourceFileReloader | None = None
) -> None:
self.running_app = config.app
super().__init__(config)
self.reloader = reloader
if self.reloader:
self.event = threading.Event()
self.watch = partial(watchfn, self.reloader)
def install_signal_handlers(self):
pass
def run_in_thread(self):
self.thread = threading.Thread(target=self.run, daemon=True)
if self.reloader:
self.watch_thread = threading.Thread(target=self.watch, daemon=True)
self.watch_thread.start()
self.thread.start()
start = time.time()
while not self.started:
time.sleep(1e-3)
if time.time() - start > 5:
raise ServerFailedToStartError(
"Server failed to start. Please check that the port is available."
)
def close(self):
self.should_exit = True
if self.reloader:
self.reloader.stop()
self.watch_thread.join()
self.thread.join()
def get_first_available_port(initial: int, final: int) -> int:
"""
Gets the first open port in a specified range of port numbers
Parameters:
initial: the initial value in the range of port numbers
final: final (exclusive) value in the range of port numbers, should be greater than `initial`
Returns:
port: the first open port in the range
"""
for port in range(initial, final):
try:
s = socket.socket() # create a socket object
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((LOCALHOST_NAME, port)) # Bind to the port
s.close()
return port
except OSError:
pass
raise OSError(
f"All ports from {initial} to {final - 1} are in use. Please close a port."
)
def configure_app(app: App, blocks: Blocks) -> App:
auth = blocks.auth
if auth is not None:
if not callable(auth):
app.auth = {account[0]: account[1] for account in auth}
else:
app.auth = auth
else:
app.auth = None
app.blocks = blocks
app.cwd = os.getcwd()
app.favicon_path = blocks.favicon_path
app.tokens = {}
return app
def start_server(
blocks: Blocks,
server_name: str | None = None,
server_port: int | None = None,
ssl_keyfile: str | None = None,
ssl_certfile: str | None = None,
ssl_keyfile_password: str | None = None,
app_kwargs: dict | None = None,
) -> tuple[str, int, str, App, Server]:
"""Launches a local server running the provided Interface
Parameters:
blocks: The Blocks object to run on the server
server_name: to make app accessible on local network, set this to "0.0.0.0". Can be set by environment variable GRADIO_SERVER_NAME.
server_port: will start gradio app on this port (if available). Can be set by environment variable GRADIO_SERVER_PORT.
auth: If provided, username and password (or list of username-password tuples) required to access the Blocks. Can also provide function that takes username and password and returns True if valid login.
ssl_keyfile: If a path to a file is provided, will use this as the private key file to create a local server running on https.
ssl_certfile: If a path to a file is provided, will use this as the signed certificate for https. Needs to be provided if ssl_keyfile is provided.
ssl_keyfile_password: If a password is provided, will use this with the ssl certificate for https.
app_kwargs: Additional keyword arguments to pass to the gradio.routes.App constructor.
Returns:
port: the port number the server is running on
path_to_local_server: the complete address that the local server can be accessed at
app: the FastAPI app object
server: the server object that is a subclass of uvicorn.Server (used to close the server)
"""
if ssl_keyfile is not None and ssl_certfile is None:
raise ValueError("ssl_certfile must be provided if ssl_keyfile is provided.")
server_name = server_name or LOCALHOST_NAME
url_host_name = "localhost" if server_name == "0.0.0.0" else server_name
# Strip IPv6 brackets from the address if they exist.
# This is needed as http://[::1]:port/ is a valid browser address,
# but not a valid IPv6 address, so asyncio will throw an exception.
if server_name.startswith("[") and server_name.endswith("]"):
host = server_name[1:-1]
else:
host = server_name
app = App.create_app(blocks, app_kwargs=app_kwargs)
server_ports = (
[server_port]
if server_port is not None
else range(INITIAL_PORT_VALUE, INITIAL_PORT_VALUE + TRY_NUM_PORTS)
)
for port in server_ports:
try:
# The fastest way to check if a port is available is to try to bind to it with socket.
# If the port is not available, socket will throw an OSError.
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Really, we should be checking if (server_name, server_port) is available, but
# socket.bind() doesn't seem to throw an OSError with ipv6 addresses, based on my testing.
# Instead, we just check if the port is available on localhost.
s.bind((LOCALHOST_NAME, port))
s.close()
# To avoid race conditions, so we also check if the port by trying to start the uvicorn server.
# If the port is not available, this will throw a ServerFailedToStartError.
config = uvicorn.Config(
app=app,
port=port,
host=host,
log_level="warning",
ssl_keyfile=ssl_keyfile,
ssl_certfile=ssl_certfile,
ssl_keyfile_password=ssl_keyfile_password,
)
reloader = None
if GRADIO_WATCH_DIRS:
change_event = threading.Event()
app.change_event = change_event
reloader = SourceFileReloader(
app=app,
watch_dirs=GRADIO_WATCH_DIRS,
watch_module_name=GRADIO_WATCH_MODULE_NAME,
demo_name=GRADIO_WATCH_DEMO_NAME,
stop_event=threading.Event(),
change_event=change_event,
demo_file=GRADIO_WATCH_DEMO_PATH,
)
server = Server(config=config, reloader=reloader)
server.run_in_thread()
break
except (OSError, ServerFailedToStartError):
pass
else:
raise OSError(
f"Cannot find empty port in range: {min(server_ports)}-{max(server_ports)}. You can specify a different port by setting the GRADIO_SERVER_PORT environment variable or passing the `server_port` parameter to `launch()`."
)
if ssl_keyfile is not None:
path_to_local_server = f"https://{url_host_name}:{port}/"
else:
path_to_local_server = f"http://{url_host_name}:{port}/"
return server_name, port, path_to_local_server, app, server
def setup_tunnel(
local_host: str, local_port: int, share_token: str, share_server_address: str | None

View File

@ -80,7 +80,6 @@ async function initializeEnvironment(
await micropip.install(["markdown-it-py[linkify]~=2.2.0"]); // On 3rd June 2023, markdown-it-py 3.0.0 has been released. The `gradio` package depends on its `>=2.0.0` version so its 3.x will be resolved. However, it conflicts with `mdit-py-plugins`'s dependency `markdown-it-py >=1.0.0,<3.0.0` and micropip currently can't resolve it. So we explicitly install the compatible version of the library here.
await micropip.install(["anyio==3.*"]); // `fastapi` depends on `anyio>=3.4.0,<5` so its 4.* can be installed, but it conflicts with the anyio version `httpx` depends on, `==3.*`. Seems like micropip can't resolve it for now, so we explicitly install the compatible version of the library here.
await micropip.add_mock_package("pydantic", "2.4.2"); // PydanticV2 is not supported on Pyodide yet. Mock it here for installing the `gradio` package to pass the version check. Then, install PydanticV1 below.
await micropip.add_mock_package("ruff", "0.2.2"); // `ruff` was added to the requirements of `gradio` for the custom components (https://github.com/gradio-app/gradio/pull/7030), but it's not working on PYodide yet. Also Lite doesn't need it, so mock it here for installing the `gradio` package to pass the version check.
await micropip.install.callKwargs(gradioWheelUrls, {
keep_going: true
});

View File

@ -20,7 +20,7 @@ pydub
pyyaml>=5.0,<7.0
semantic_version~=2.0
typing_extensions~=4.0
uvicorn>=0.14.0
uvicorn>=0.14.0; sys.platform != 'emscripten'
typer[all]>=0.9,<1.0
tomlkit==0.12.0
ruff>=0.2.2
ruff>=0.2.2; sys.platform != 'emscripten'

View File

@ -16,7 +16,6 @@ from unittest.mock import patch
import gradio_client as grc
import numpy as np
import pytest
import uvicorn
from fastapi.testclient import TestClient
from gradio_client import Client, media_data
from PIL import Image
@ -25,7 +24,6 @@ import gradio as gr
from gradio.data_classes import GradioModel, GradioRootModel
from gradio.events import SelectData
from gradio.exceptions import DuplicateBlockError
from gradio.networking import Server, get_first_available_port
from gradio.utils import assert_configs_are_equivalent_besides_ids
pytest_plugins = ("pytest_asyncio",)
@ -345,29 +343,6 @@ class TestBlocksMethods:
if i == 2:
assert dependency["types"] == {"continuous": True, "generator": True}
@pytest.mark.asyncio
async def test_run_without_launching(self):
"""Test that we can start the app and use queue without calling .launch().
This is essentially what the 'gradio' reload mode does
"""
port = get_first_available_port(7860, 7870)
io = gr.Interface(lambda s: s, gr.Textbox(), gr.Textbox()).queue()
config = uvicorn.Config(app=io.app, port=port, log_level="warning")
server = Server(config=config)
server.run_in_thread()
try:
client = grc.Client(f"http://localhost:{port}")
result = client.predict("Victor", api_name="/predict")
assert result == "Victor"
finally:
server.close()
@patch(
"gradio.themes.ThemeClass.from_hub",
side_effect=ValueError("Something went wrong!"),

29
test/test_http_server.py Normal file
View File

@ -0,0 +1,29 @@
import urllib.parse
import pytest
import gradio as gr
from gradio import http_server
class TestStartServer:
# Test IPv4 and IPv6 hostnames as they would be passed from --server-name.
@pytest.mark.parametrize("host", ["127.0.0.1", "[::1]"])
def test_start_server(self, host):
io = gr.Interface(lambda x: x, "number", "number")
io.favicon_path = None
io.config = io.get_config_file()
io.show_error = True
io.flagging_callback.setup(gr.Number(), io.flagging_dir)
io.auth = None
_, _, local_path, _, server = http_server.start_server(io)
url = urllib.parse.urlparse(local_path)
assert url.scheme == "http"
assert url.port is not None
assert (
http_server.INITIAL_PORT_VALUE
<= url.port
<= http_server.INITIAL_PORT_VALUE + http_server.TRY_NUM_PORTS
)
server.close()

View File

@ -1,39 +1,14 @@
"""Contains tests for networking.py and app.py"""
import os
import urllib
import warnings
import pytest
from fastapi.testclient import TestClient
import gradio as gr
from gradio import Interface, networking
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
class TestPort:
def test_port_is_in_range(self):
start = 7860
end = 7960
try:
port = networking.get_first_available_port(start, end)
assert start <= port <= end
except OSError:
warnings.warn("Unable to test, no ports available")
def test_same_port_is_returned(self):
start = 7860
end = 7960
try:
port1 = networking.get_first_available_port(start, end)
port2 = networking.get_first_available_port(start, end)
assert port1 == port2
except OSError:
warnings.warn("Unable to test, no ports available")
class TestInterfaceErrors:
def test_processing_error(self):
io = Interface(lambda x: 1 / x, "number", "number")
@ -53,29 +28,6 @@ class TestInterfaceErrors:
io.close()
class TestStartServer:
# Test IPv4 and IPv6 hostnames as they would be passed from --server-name.
@pytest.mark.parametrize("host", ["127.0.0.1", "[::1]"])
def test_start_server(self, host):
io = Interface(lambda x: x, "number", "number")
io.favicon_path = None
io.config = io.get_config_file()
io.show_error = True
io.flagging_callback.setup(gr.Number(), io.flagging_dir)
io.auth = None
io.host = host
port = networking.get_first_available_port(
networking.INITIAL_PORT_VALUE,
networking.INITIAL_PORT_VALUE + networking.TRY_NUM_PORTS,
)
_, _, local_path, _, server = networking.start_server(io, server_port=port)
url = urllib.parse.urlparse(local_path)
assert url.scheme == "http"
assert url.port == port
server.close()
class TestURLs:
def test_url_ok(self):
res = networking.url_ok("https://www.gradio.app")

View File

@ -7,7 +7,7 @@ import pytest
import gradio
import gradio as gr
from gradio.cli.commands.reload import _setup_config
from gradio.networking import Server
from gradio.http_server import Server
def build_demo():