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/*
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")
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
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)
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()
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=[])
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)
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,
)
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
)
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 *
# 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
)
Top comments (3)
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.
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 =)
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.
on same run, few minutes later.