Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programmatically launch reload to allow factories and arguments to be passed to the app #4119

Merged
merged 15 commits into from
May 15, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## New Features:

- 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)
abidlabs marked this conversation as resolved.
Show resolved Hide resolved
- Added `format` argument to `Audio` component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4178](https://github.com/gradio-app/gradio/pull/4178)
- Add JS client code snippets to use via api page by [@aliabd](https://github.com/aliabd) in [PR 3927](https://github.com/gradio-app/gradio/pull/3927).

Expand Down
51 changes: 42 additions & 9 deletions gradio/reload.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
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"
abidlabs marked this conversation as resolved.
Show resolved Hide resolved
"""
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]
if "." not in demo_name:
print("WARNING: As of Gradio 3.31, the parameter after the file path must be the name of the FastAPI app, not the Gradio demo. In most cases, this just means you should add '.app' after the name of your demo, e.g. 'demo' -> 'demo.app'.")

original_path = args[0]
abs_original_path = utils.abspath(original_path)
Expand All @@ -36,21 +44,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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be using the server class defined in networking.py

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah let me make the change and confirm everything still works

sock = config.bind_socket()
ChangeReload(config, target=server.run, sockets=[sock]).run()


if __name__ == "__main__":
main()
57 changes: 50 additions & 7 deletions guides/07_other-tutorials/developing-faster-with-reload-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This short Guide will cover both of these methods, so no matter how you write Py

## Python IDE Reload 🔥

If you are building Gradio Blocks using a Python IDE, your file of code (let's name it `app.py`) might look something like this:
If you are building Gradio Blocks using a Python IDE, your file of code (let's name it `run.py`) might look something like this:

```python
import gradio as gr
Expand All @@ -32,11 +32,11 @@ if __name__ == "__main__":
demo.launch()
```

The problem is that anytime that you want to make a change to your layout, events, or components, you have to close and rerun your app by writing `python app.py`.
The problem is that anytime that you want to make a change to your layout, events, or components, you have to close and rerun your app by writing `python run.py`.

Instead of doing this, you can run your code in **reload mode** by changing 1 word: `python` to `gradio`:

In the terminal, run `gradio app.py`. That's it!
In the terminal, run `gradio run.py`. That's it!

Now, you'll see that after you'll see something like this:

Expand All @@ -48,13 +48,56 @@ Watching...
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 🥳
The important part here is the line that says `Watching...` What's happening here is that Gradio will be observing the directory where `run.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`
⚠️ Warning: the `gradio` command does not detect the parameters passed to the `launch()` methods because the `launch()` method is never called in reload mode. For example, setting `auth`, or `show_error` in `launch()` will not be reflected in the 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) ✅
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 will need to pass in the name of your demo's FastAPI app as the 2nd parameter in your code. For Gradio demos, the FastAPI app can be accessed using the `.app` attribute. So if your `run.py` file looked like this:

⚠️ The `gradio` command will not detect the parameters passed to the `launch()` methods. For example, setting `auth`, or `show_error` in `launch()` will not be reflected in the app.
```python
import gradio as gr

with gr.Blocks() as my_demo:
gr.Markdown("# Greetings from Gradio!")
inp = gr.Textbox(placeholder="What is your name?")
out = gr.Textbox()

inp.change(fn=lambda x: f"Welcome, {x}!",
inputs=inp,
outputs=out)

if __name__ == "__main__":
my_demo.launch()
```

Then you would launch it in reload mode like this: `gradio run.py my_demo.app`.

🔥 If your application accepts command line arguments, you can pass them in as well. Here's an example:

```python
import gradio as gr
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--name", type=str, default="User")
args, unknown = parser.parse_known_args()

with gr.Blocks() as demo:
gr.Markdown(f"# Greetings {args.name}!")
inp = gr.Textbox()
out = gr.Textbox()

inp.change(fn=lambda x: x, inputs=inp, outputs=out)

app = demo.app
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this line?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed we do not


if __name__ == "__main__":
demo.launch()
```

Which you could run like this: `gradio run.py --name Gretel`

As a small aside, this auto-reloading happens if you change your `run.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) ✅

## Jupyter Notebook Magic 🔮

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