From 0e12da34b37fd7cfee64e870068c04890820b6f8 Mon Sep 17 00:00:00 2001 From: micky2be Date: Tue, 9 May 2023 11:54:55 +0200 Subject: [PATCH] Programmatically lauch reload to allow factories and arguments to be passed to the app --- CHANGELOG.md | 1 + gradio/reload.py | 38 ++++++++--- .../developing-faster-with-reload-mode.md | 4 +- pyproject.toml | 2 +- test/test_reload.py | 66 +++++++++++++++---- 5 files changed, 88 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 953032b413cdd..3847666984625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 XXXX](https://github.com/gradio-app/gradio/pull/XXXX) ## Bug Fixes: diff --git a/gradio/reload.py b/gradio/reload.py index 1ae802ff32b59..b0640d2ef9d0e 100644 --- a/gradio/reload.py +++ b/gradio/reload.py @@ -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) @@ -36,21 +42,35 @@ 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") + + sys.path.insert(0, os.getcwd()) + return Config(gradio_app, reload=True, port=port, log_level="warning", reload_dirs=watching_dirs) + + +def main(): + config = _setup_config() + server = Server(config) + sock = config.bind_socket() + ChangeReload(config, target=server.run, sockets=[sock]).run() + +if __name__ == "__main__": + main() diff --git a/guides/07_other-tutorials/developing-faster-with-reload-mode.md b/guides/07_other-tutorials/developing-faster-with-reload-mode.md index 0c98054a4e2cf..c4bb3840451d9 100644 --- a/guides/07_other-tutorials/developing-faster-with-reload-mode.md +++ b/guides/07_other-tutorials/developing-faster-with-reload-mode.md @@ -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) ✅ diff --git a/pyproject.toml b/pyproject.toml index b79a15b7bd561..da10bde849cce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/test/test_reload.py b/test/test_reload.py index f8ce88c1ee902..f1660340cda03 100644 --- a/test/test_reload.py +++ b/test/test_reload.py @@ -1,20 +1,62 @@ 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