TaiPy GUI - Some quirks with a neat Python GUI library
Tags: taipy, python, frontend, ui
Published: 2023-08-10 10:06:30

Recently I have taken on the task of creating an application that required a GUI. Now that is an horror story of its own for someone like me, who knows the basic of Front End development but hates it and wishes it was easier.

I have used python GUI libraries in the past, like Remi, PyQt, Tkinter, but what I always wanted was something as simple as

> I WANT THIS THING HERE
> THIS THING SHOWS THIS AND DOES THIS

Enter TaiPy, which I also recently found out that it's a very recent library so there's almost _NOTHING_ about it on the internet, and it's a great tool. That's why I decided to write about it.

I am not going to write a tutorial on how to use it, [their website](https://www.taipy.io/) has a decent documentation on how to get started. I will be documenting the difficulties and little quirks that I had to figure out about TaiPy when applying it to a bigger project.

# Your code is always exposed if you don't do something about it

TaiPy exposes every single file of your implementation in the webserver by default. The only way around this is to create a `.taipyignore` file that will hide them. But bear in mind that if you need files to be exposed, like images, this will filter them out too! The file follows the same syntax as a `.gitignore` file.

An example of a `.taipyignore` file that I'd normally use is one that will hide everything but the images folder:

```shell
*
!/gui/images/*
```

More info [here](https://docs.taipy.io/en/release-2.3/manuals/gui/configuration/#protect-your-application-files)

# Live updating elements

Some of this information can be found on [their FAQ](https://www.taipy.io/faq/)

I've found that a change made to a variable is not reflected unless you manually refresh it. That seems to apply to tables even when they have the `rebuild` option set to to `True`. I imagine it can be a bug as of `TaiPy 2.4.0`, but unsure.

Here's an example:
```python
def on_click_add_button(state):
    HOME_PAGE_TABLE.add_item(INPUT)
    state.refresh("HOME_PAGE_TABLE")
```

if `state.refresh` is not called, the changes in the table are only reflected on a full page refresh or if the user forces an update of the table somehow (by reordering items for example).

## Update as of 25/09/2023
Another method, stated by their FAQ and some members of the TaiPy team I came in contact with, is using the `state` variable. So instead of using `refresh`, we would do:
```python
def on_click_add_button(state):
    HOME_PAGE_TABLE.add_item(INPUT)
    state.HOME_PAGE_TABLE = HOME_PAGE_TABLE
```
Which is also basically what refresh does.
The advantage of this is that if you have something that returns the object itself, it's more straighforward to live update it.

However the big big downside of this is that you don't get linting support. The linter won't be able to detect that `state.HOME_PAGE_TABLE` is an actual thing, as those are defined at run time.

# Threading

Using the Threading library with TaiPy is not straight forward and it can get complicated depending on how you structured your code.

I'll start with the more straight forward ones. Let's say we have some sort of application that tracks metrics over a timer. So when you hit `Start`, you'll want to have a loop running in the background, upgrading the timer and whatever metrics you want during that time.

```python
from taipy.gui import State
# STATS_TABLE is a Python object that TaiPy can interpret as a Table
# STATS_TABLE.tick() will update the values in the variable
def _timer_loop(state:State):
    STATS_TABLE.reset()
    while TIMER.is_running:
        STATS_TABLE.tick()
        state.refresh(STATS_TABLE)
```

`state.refresh` is needed so the update is reflected on the GUI. It shouldn't be needed according to the documentation if you have the `rebuild` option set for tables, but that doesn't work very reliably as of TaiPy v2.4.0.

So if you're just approaching this for the first time you might think of doing something like

```python
import threading

def timer_start(state:State):
    """THIS WILL NOT WORK"""
    thr = threading.Thread(target=_timer_loop, args=[state])
    thr.start()
```

However, doing so will give you an exception `RuntimeError: Working outside of application context`. This seems to happen with everything that tries to start a thread. The way to go about this can be found [here](https://docs.taipy.io/en/release-2.1/manuals/gui/callbacks/#long-running-callbacks-in-a-thread), which is using the `invoke_long_callback` function **IF** you don't need the state variable

### Case 1: I don't need the `state` variable

If you don't need it just go ahead and do
```python
invoke_long_callback(state, _timer_loop, user_function_args=[])
```

But bear in mind that the `state` variable still needs to be provided to the `invoke_long_callback` function. However, if you need to pass `state` to your function, adding it to the `user_function_args` parameter does not work!

### Case 2: I need the `state` variable

As mentioned in Case 1, `state` can not be passed to `user_function_args`. So how to go about it?

We'll need the `get_state_id` function from `taipy.gui`. With this we can get the `state` variable indirectly and pass it down to the `invoke_callback` function, but there's one drawback that I will talk about afterwards.

```python
import threading
from taipy.gui import get_state_id, State, Gui

# Later in the code, pages can be added to the Gui with
# GUI_OBJ.add_page
# Then at the end we can do
# GUI_OBJ.run
GUI_OBJ = Gui()

def timer_start(state:State):
    thr = threading.Thread(target=_start_timer_thread, args=[get_state_id(state)])
    thr.start()

# You can probably merge these two functions into one but I haven't tested it ;) Might do in the near future
def _start_timer_thread(state_id: str):
    TIMER.start_timer()
    invoke_callback(GUI_OBJ, state_id, _timer_loop, args=[])

def _timer_loop(state:State):
    STATS_TABLE.reset()
    while TIMER.is_running:
        STATS_TABLE.tick()
        state.refresh(STATS_TABLE)
```

The drawback of this is that we need the Gui object, forcing us to have a more complicated structure, because usually `GUI_OBJ` is declared after everything else is declared, due to the way the variable scope of TaiPy works.

With this, however, you're forced to declare the Gui before everything else and will have to work around the variable scoping, which I'll talk about next.


# Variable scope - an incomplete guide

When it comes to this, I am still not 100% sure how TaiPy works. Hopefully in the future I can edit this post and put more complete information. But here's my experience:

Usually, your TaiPy programs will be written something like this (a VERY simplified version):

```python
import threading
from taipy.gui import Gui, get_state_id, invoke_callback, State
from components import STATS_TABLE, TIMER

root_page="""
<|{STATS_TABLE.table}|table|on_edit=on_edit_func|>
<|button|label=Start Timer|on_action=timer_start
"""

def on_edit_func(state):
     print("The table was edited!")

def timer_start(state:State):
    thr = threading.Thread(target=_start_timer_thread, args=[get_state_id(state)])
    thr.start()

def _start_timer_thread(state_id: str):
    TIMER.start_timer()
    invoke_callback(GUI_OBJ, state_id, _timer_loop, args=[])

def _timer_loop(state:State):
    STATS_TABLE.reset()
    while TIMER.is_running:
        STATS_TABLE.tick()
        state.refresh(STATS_TABLE)

GUI_OBJ = Gui(page=root_page)

GUI_OBJ.run(
        host="127.0.0.1",
        port=1234,
        dark_mode=False,
        debug=True,
        use_reloader=True,
)
```

It's noticeable that it is already getting pretty loaded with just a single button and a table, so we'll need to split this file. But how?

Normally a straight forward way to do it is to do something like this maybe?

```python
""" This won't work! """
from taipy.gui import Gui

from pages import root_page
from callbacks import on_edit_func, timer_start
from components import STATS_TABLE, TIMER

GUI_OBJ = Gui(page=root_page)

GUI_OBJ.run(
        host="127.0.0.1",
        port=1234,
        dark_mode=False,
        debug=True,
        use_reloader=True
)
```
Maybe not the best division but should work, no?
Not really, because if you remember the previous section, `timer_start` needs `GUI_OBJ` to work!

If it weren't for that this code would most likely work.

Now here's the thing, _TaiPy NEEDS everything that the Gui will use to be on the same scope of when you first declare the Gui object_. That also means the `timer_start` function, which needs the Gui itself.

It becomes a circular situation. The best way I found around this is to have:

-  A file with ALL the necessary imports and the initialisation of the Gui all in the same place. 
- Then the assignment of pages to the Gui and running is made on another file.

So for example a `gui_functional.py` file and a `main.py` respectively.

```python
# gui_functional.py
from taipy.gui import Gui

GUI_OBJ = Gui()

from components import STATS_TABLE, TIMER
from callbacks import on_edit_func, timer_start
from pages import *
```

```python
# main.py
from gui_functional import GUI_OBJ
from callbacks import root_page

GUI_OBJ.add_page("home", page=root_page)

GUI_OBJ.run(
        host="127.0.0.1",
        port=1234,
        dark_mode=False,
        debug=True,
        use_reloader=True
)
```