DEV Community

Gabriel Uri
Gabriel Uri

Posted on • Edited on

TaiPy GUI - Some quirks with a neat Python GUI library

November 2024 Update I should preface this by saying that this was written with Taipy 2.something, as of today Taipy is already at version 4! So much of what has been written here could be outdated =)

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 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:

*
!/gui/images/*
Enter fullscreen mode Exit fullscreen mode

More info here

Live updating elements

Some of this information can be found on their 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:

def on_click_add_button(state):
    HOME_PAGE_TABLE.add_item(INPUT)
    state.refresh("HOME_PAGE_TABLE")
Enter fullscreen mode Exit fullscreen mode

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:

def on_click_add_button(state):
    HOME_PAGE_TABLE.add_item(INPUT)
    state.HOME_PAGE_TABLE = HOME_PAGE_TABLE
Enter fullscreen mode Exit fullscreen mode

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.

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)
Enter fullscreen mode Exit fullscreen mode

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

import threading

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

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, 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

invoke_long_callback(state, _timer_loop, user_function_args=[])
Enter fullscreen mode Exit fullscreen mode

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.

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)
Enter fullscreen mode Exit fullscreen mode

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):

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,
)
Enter fullscreen mode Exit fullscreen mode

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?

""" 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
)
Enter fullscreen mode Exit fullscreen mode

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.

# 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 *
Enter fullscreen mode Exit fullscreen mode
# 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
)
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
taikedz profile image
Tai Kedzierski

This is a great rundown of the gotchas of using Taipy for some common scenarios.

I do like the concept of it, with the UI and layout being "batteries-included", so as to focus on business logic. Great find.

Collapse
 
gbritoda profile image
Gabriel Uri

Just had a chat with a bunch of people that work in TaiPy because of this article.

Seems some things will be changed on the next release and I'll update the article accordingly =)

Collapse
 
oluwafemi_ebenezerbcx_ profile image
Oluwafemi Ebenezer (BCX)

The Taipy object changes too, so I am not sure it is reliable to put in production, guess I will only use the ETL later for start stop process.


> Finished chain.
Current state.__dict__={'_Bindings__gui': <taipy.gui.gui.Gui object at 0x000002537C5CE1E0>, '_Bindings__scopes': <taipy.gui.data.data_scope._DataScopes object at 0x000002537F6780B0>}
Called with what does the datarepresent, what do you think they re trying to achieved with the collected data?


> Entering new AgentExecutor chain...
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"

Thought: Do I need to use a tool? Yes

Enter fullscreen mode Exit fullscreen mode

on same run, few minutes later.

Current state.__dict__={'_Bindings__gui': <taipy.gui.gui.Gui object at 0x000002CD3D533A40>, '_Bindings__scopes': <taipy.gui.data.data_scope._DataScopes object at 0x000002CD44186B10>}
Called with
Current state.__dict__={'_Bindings__gui': <taipy.gui.gui.Gui object at 0x000002CD3D533A40>, '_Bindings__scopes': <taipy.gui.data.data_scope._DataScopes object at 0x000002CD44186B10>}
Called with
Current state.__dict__={'_Bindings__gui': <taipy.gui.gui.Gui object at 0x000002CD3D533A40>, '_Bindings__scopes': <taipy.gui.data.data_scope._DataScopes object at 0x000002CD44186B10>}
Called with
Enter fullscreen mode Exit fullscreen mode