smhk

NiceGUI with Click, Poetry, auto-reload and classes

It’s very easy to get up and running with NiceGUI, and the development cycle is fast because it has an auto-reload feature, where the web page automatically updates whenever you save a change to your project’s Python code.

However, almost all the official NiceGUI examples show UI code being written at the top-level, outside of a class, and outside of a main guard.

For example, following is the ui.button example code:

from nicegui import ui

ui.button('Click me!', on_click=lambda: ui.notify('You clicked me!'))

ui.run()

I like the auto-reload feature for development, but want to wrap up my GUI code in a class. Additionally, I want to provide command line options using Click, and expose the GUI application as a Poetry script. It turns out doing all of these together is a little fiddly.

Solution §

These notes cover how to do all of the following:

  • Use the NiceGUI auto-reload feature for development.
  • Use Poetry to provide a script for users to run the GUI.
  • Use Click to allow specifying options when launching the GUI.
  • Wrap your NiceGUI code in a class rather than having it at the top-level.
  • And, as an optional bonus step: configure an explicit index page.

Auto-reload and Poetry §

The auto-reload feature is enabled by default, but can be set explicitly with ui.run(reload=True). However, auto-reload only works if the script is called directly, e.g.:

python my_app/my_cli.py

If you call it indirectly, e.g. via Poetry…

poetry run my-cli

…then auto-reload will not work, and you will get this warning:

WARNING:root:auto-reloading is only supported when running from a file

For this problem, the best solution I have found is to have two entry points to your GUI:

  1. An if __name__ in {"__main__", "__mp_main__"}: entry point for calling directly. (We’ll get to the details of this line next).
  2. A def main(): entry point for calling it via Poetry.

The first entry point uses ui.run(reload=True), and the second uses ui.run(reload=False). This prevents getting any warning when launching via Poetry

Avoid double run during auto-reload §

You may have been wondering why this is the first entry point:

if __name__ in {"__main__", "__mp_main__"}:

The reason is that with auto-reload, NiceGUI uses two processes1:

  • The main process (__main__) starts the server.
  • The child process (__mp_main__) is restarted each time the code is changed.

Each process evalutes the script. So any top-level code (e.g. code outside a class, function or main guard) will be executed whenever the script is evaluated.2

There are several solutions1, but since I want to put my GUI code in a class, my preferred way is to use a main guard as follows:

if __name__ in {"__main__", "__mp_main__"}:
    if __name__ == "__mp_main__":
        MyGUI()

    ui.run(port=8081, reload=True)

This way the GUI code (contained in the MyGUI class) only gets created by the child process. So we avoid the “double run”. However the ui.run function gets called each time, which is necessary for the main and child process to work.

Click §

The second entry point can be further extended with Click to allow passing in options. For example, adding a --port option to specify which port the GUI runs on:

poetry run my-cli --port=8090

I haven’t dug into the details, but in my experience I found that Click did not work auto-reload. Attempting to do so caused NiceGUI to hang when launching. Hence why I only use Click with the Poetry entry point, which has reload=False.

Putting it all together §

Following is a full example of using NiceGUI, Click and Poetry scripts:

my_app/my_cli.py
import click
from nicegui import ui

# Very basic example GUI in a class.
class MyGUI:
    def __init__(self):
        ui.label("Hello, world!")


# Wrapper around the call to `ui.run`.
def my_run(port: int, reload: bool):
    ui.run(port=port, reload=reload, title="My GUI")


# Entrypoint for when GUI is launched by the CLI.
# e.g.: poetry run my-cli
@click.command()
@click.option(
    "--port",
    default=8081,
    help="Port for GUI",
)
def main(port):
    print("GUI launched by CLI")
    MyGUI()
    my_run(port=port, reload=False)


# Entrypoint for when GUI is launched by the CLI.
# e.g.: python my_app/my_cli.py
if __name__ in {"__main__", "__mp_main__"}:
    if __name__ == "__mp_main__":
        print("GUI launched by auto-reload")
        MyGUI()

    my_run(port=8081, reload=True)

To enable using poetry run my-cli to launch the GUI, we must add this to our pyproject.toml:

pyproject.toml
[tool.poetry.scripts]
my-cli = "my_app.my_cli:main"

Now you can do the following:

  • As a user:
    • Use poetry run my-cli to launch the GUI.
    • Use poetry run my-cli --port=8090 to specify the port.
  • As a developer:
    • Use poetry run python my_app/my_cli.py to launch the GUI with auto-reload enabled.

Bonus: explicit index page §

If you’ve got this far, you might be interested in this next step. It isn’t necessary, but can help simplify the code a little.

Almost all the NiceGUI documentation uses an implicit index page, whereby UI elements defined outside of a @ui.page decorator are part of the auto-index page at /.

By instead using an explicit index page, we can remove the need to instantiate MyGUI() in two separate places. Though beware this also means data within the index page is “private to the user and not shared with others”, so if you are relying on that feature to allow multiple users to share the same index page, using an explicit index page will break that functionality.

my_app/my_cli.py
import click
from nicegui import ui

# Very basic example GUI in a class.
class MyGUI:
    def __init__(self):
        ui.label("Hello, world!")


# Explicit index page.
@ui.page("/")
def index():
    MyGUI()


# Wrapper around the call to `ui.run`.
def my_run(port: int, reload: bool):
    ui.run(port=port, reload=reload, title="My GUI")


# Entrypoint for when GUI is launched by the CLI.
# e.g.: poetry run my-cli
@click.command()
@click.option(
    "--port",
    default=8081,
    help="Port for GUI",
)
def main(port):
    print("GUI launched by CLI")
    # MyGUI()  # Can delete this line
    my_run(port=port, reload=False)


# Entrypoint for when GUI is launched by the CLI.
# e.g.: python my_app/my_cli.py
if __name__ in {"__main__", "__mp_main__"}:
    if __name__ == "__mp_main__":
        print("GUI launched by auto-reload")
        # MyGUI()  # Can delete this line

    my_run(port=8081, reload=True)

I prefer the explicit index page, especially for more complex GUIs, since it makes it clearer how the pages are defined, especially once you start adding more than one page.