Skip to content

Commit

Permalink
Programmatically lauch reload to allow factories and arguments to be …
Browse files Browse the repository at this point in the history
…passed to the app
  • Loading branch information
micky2be committed May 9, 2023
1 parent bebfb72 commit 7261ec1
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## New Features:

- Returning language agnostic types in the `/info` route by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4039](https://github.com/gradio-app/gradio/pull/4039)
- The reloader command (`gradio app.py`) can now accept argumnets to be passed to your application by [@micky2be](https://github.com/micky2be) in [PR 4119](https://github.com/gradio-app/gradio/pull/4119)

## Bug Fixes:

Expand Down
49 changes: 40 additions & 9 deletions gradio/reload.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@
Contains the functions that run when `gradio` is called from the command line. Specifically, allows
$ gradio app.py, to run app.py in reload mode where any changes in the app.py file or Gradio library reloads the demo.
$ gradio app.py my_demo, to use variable names other than "demo"
$ gradio app.py my_demo.app, to use variable names other than "demo"
"""
import inspect
import os
import sys
from pathlib import Path

from uvicorn import Config, Server
from uvicorn.supervisors import ChangeReload

import gradio
from gradio import networking, utils


def run_in_reload_mode():
def _setup_config():
args = sys.argv[1:]
if len(args) == 0:
raise ValueError("No file specified.")
demo_name = "demo" if len(args) == 1 else args[1]
if len(args) == 1 or args[1].startswith("--"):
demo_name = "demo.app"
else:
demo_name = args[1]

original_path = args[0]
abs_original_path = utils.abspath(original_path)
Expand All @@ -36,21 +42,46 @@ def run_in_reload_mode():
print(
f"\nLaunching in *reload mode* on: http://{networking.LOCALHOST_NAME}:{port} (Press CTRL+C to quit)\n"
)
command = f"uvicorn {filename}:{demo_name}.app --reload --port {port} --log-level warning "
message = "Watching:"

gradio_app = f"{filename}:{demo_name}"
message = "Watching:"
message_change_count = 0

watching_dirs = []
if str(gradio_folder).strip():
command += f'--reload-dir "{gradio_folder}" '
watching_dirs.append(gradio_folder)
message += f" '{gradio_folder}'"
message_change_count += 1

abs_parent = abs_original_path.parent
if str(abs_parent).strip():
command += f'--reload-dir "{abs_parent}"'
watching_dirs.append(abs_parent)
if message_change_count == 1:
message += ","
message += f" '{abs_parent}'"

print(f"{message}\n")
os.system(command)
print(message + "\n")

# guaranty access to the module of an app
sys.path.insert(0, os.getcwd())

# uvicorn.run blocks the execution (looping) which makes it hard to test
return Config(
gradio_app,
reload=True,
port=port,
log_level="warning",
reload_dirs=watching_dirs,
)


def main():
# default execution pattern to start the server and watch changes
config = _setup_config()
server = Server(config)
sock = config.bind_socket()
ChangeReload(config, target=server.run, sockets=[sock]).run()


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ WARNING: The --reload flag should not be used in production on Windows.

The important part here is the line that says `Watching...` What's happening here is that Gradio will be observing the directory where `app.py` file lives, and if the file changes, it will automatically rerun the file for you. So you can focus on writing your code, and your Gradio demo will refresh automatically 🥳

⚠️ Now, there is one important thing to keep in mind when using the reload mode: Gradio specifically looks for a Gradio Blocks/Interface demo called `demo` in your code. If you have named your demo something else, you can pass that as the 2nd parameter in your code, like this: `gradio app.py my_demo`
⚠️ Now, there is one important thing to keep in mind when using the reload mode: Gradio specifically looks for a Gradio Blocks/Interface demo called `demo` in your code. If you have named your demo something else, you can pass that as the 2nd parameter in your code, like this: `gradio app.py my_demo.app`

⚠️ If your application accepts cli arguments using a function/factory you can now run `gradio app.py create_app --option1 --option2`, assuming `create_app` will return the `app` object of your Gradio app

As a small aside, this auto-reloading happens if you change your `app.py` source code or the Gradio source code. Meaning that this can be useful if you decide to [contribute to Gradio itself](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ authors = [
keywords = ["machine learning", "reproducibility", "visualization"]

[project.scripts]
gradio = "gradio.reload:run_in_reload_mode"
gradio = "gradio.reload:main"
upload_theme = "gradio.themes.upload_theme:main"

[project.urls]
Expand Down
65 changes: 53 additions & 12 deletions test/test_reload.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,61 @@
from pathlib import Path
from unittest.mock import patch
import asyncio
import pytest

import gradio
from gradio.reload import run_in_reload_mode
import gradio as gr
from gradio.reload import _setup_config, Server


@patch("gradio.reload.os.system")
@patch("gradio.reload.sys")
def test_run_in_reload_mode(mock_sys, mock_system_call):
def build_demo():
with gr.Blocks() as demo:
gr.Textbox("")

mock_sys.argv = ["gradio", "demo/calculator/run.py"]
run_in_reload_mode()
reload_command = mock_system_call.call_args[0][0]
gradio_dir = Path(gradio.__file__).parent
demo_dir = Path("demo/calculator/run.py").resolve().parent
return demo

assert "uvicorn demo.calculator.run:demo.app" in reload_command
assert f'--reload-dir "{gradio_dir}"' in reload_command
assert f'--reload-dir "{demo_dir}"' in reload_command

class TestReload:
@pytest.fixture(autouse=True)
def argv(self):
return ["demo/calculator/run.py"]

@pytest.fixture
def config(self, monkeypatch, argv):
monkeypatch.setattr("sys.argv", ["gradio"] + argv)
return _setup_config()

@pytest.fixture(params=[{}])
def reloader(self, config):
reloader = Server(config)
reloader.should_exit = True
yield reloader
reloader.handle_exit(2, None)

def test_config_default_app(self, config):
assert "demo.calculator.run:demo.app" == config.app

@pytest.mark.parametrize("argv", [["demo/calculator/run.py", "test.app"]])
def test_config_custom_app(self, config):
assert "demo.calculator.run:test.app" == config.app

def test_config_watch_gradio(self, config):
gradio_dir = Path(gradio.__file__).parent
assert gradio_dir in config.reload_dirs

def test_config_watch_app(self, config):
demo_dir = Path("demo/calculator/run.py").resolve().parent
assert demo_dir in config.reload_dirs

def test_config_load_default(self, config):
config.load()
assert config.loaded == True

@pytest.mark.parametrize("argv", [["test/test_reload.py", "build_demo"]])
def test_config_load_factory(self, config):
config.load()
assert config.loaded == True

def test_reload_run_default(self, reloader):
asyncio.run(reloader.serve())
assert reloader.started == True

0 comments on commit 7261ec1

Please sign in to comment.