Why Callbacks instead of Streamlit-Like "if"-statements? #21
-
Hi, I like very much what you did with nicegui and how you used justpy! Very nice. I also looked at streamlit. Could you expound a little about why that was not a good solution for you in the end? One feature of streamlit that I find intriguing is the ability to assign the value of a component to a variable without the need of callbacks. Of course, the callbacks happen but are transparent to the user of the framework. I am trying to think how this could be implemented in JustPy. It would be a great feature for nicegui, making it even simpler to use. What is your view on this and how do you think it could be implemented? If you prefer to have the discussion by email instead of here, my email is eli.mintz@gmail.com or you can also open an issue in the JustPy repository. Eli |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments
-
Hi Eli, Our problem with Streamlit was mainly due to the way it handles user interaction. Writing State If the script starts with loading some non-constant initial state, e.g. from an external source, this state is reset whenever the script is processed again. The following script already shows inconsistent behavior. Initializing import streamlit as st
import random
state = random.randint(0, 5) # simulate external source
new_state = st.radio('State', ['A', 'B', 'C', 'D', 'E', 'F'], state)
st.text('State: ' + new_state) It is hard to let Streamlit load the state once and let the user update it repeatedly. It gets worse when multiple widgets interact with each other, like a button resetting the radio selection to some value. There are workarounds introducing session state. But they are not trivial and require quite some additional code. You can look into one of our projects to see how we mapped machine state onto session state to make it accessible for Streamlit: Loops Always reloading the script makes it also difficult to introduce own event loops, e.g. reading data and updating a plot repeatedly. In the above-mentioned project we found a workaround using threads that need to get stopped and restarted every time the script restarts due to user interaction: Today (on the main branch) we avoid this struggle using NiceGUI: Handling user interaction without callbacks seems indeed intriguing. But we doubt it holds for slightly more demanding projects. It’s just not like the language works: A (non-blocking) if-statement evaluates the conditional expression to decide which path to take, before the user gets the chance to interact. Doing it otherwise requires Streamlit to perform a significant amount of "magic", causing trouble in other respects. |
Beta Was this translation helpful? Give feedback.
-
Thank you very much for the detailed answer Falko. It does indeed look that Streamlit has a serious design flaw. They tried making things very simple, but that ended limiting the utility of their framework. |
Beta Was this translation helpful? Give feedback.
-
I renamed the issue from Improvements in JustPy to Callbacks vs Streamlit-Like "if"-statements for better clarification of the topic. |
Beta Was this translation helpful? Give feedback.
-
@elimintz I was curious to see whether I could come up with Streamlit-like if-statements for NiceGUI (or JustPy) UI elements that postpone code execution until an event occurs, but don't require re-running the whole script. Although I don't think it's super reliable and it could break in more complicated scenarios, it's interesting to see what is possible. #!/usr/bin/env python3
from nicegui import ui
import inspect
import ast
def magic_button(text):
class NodeVisitor(ast.NodeVisitor):
def __init__(self, lineno):
self.lineno = lineno
self.node = None
def visit_If(self, node):
if node.lineno == self.lineno:
self.node = node
self.generic_visit(node)
frame = inspect.currentframe().f_back
with open(inspect.getsourcefile(frame)) as f:
code = f.read()
tree = ast.parse(code)
visitor = NodeVisitor(inspect.currentframe().f_back.f_lineno)
visitor.visit(tree)
block = code.splitlines()[visitor.node.lineno:visitor.node.end_lineno]
indentation = min(len(line) - len(line.lstrip()) for line in block)
block = '\n'.join(line[indentation:] for line in block)
ui.button(text, on_click=lambda _: exec(block, frame.f_globals, frame.f_locals))
ui.label('This script demonstrates postponed code execution with streamlit-like if-statements.')
count_label = ui.label('count = 0')
count = 0
if magic_button('Click me!'):
count += 1
print(f'count = {count}')
count_label.text = f'count = {count}' When creating a "magic button", the source code is parsed into a tree and traversed looking for if-statements on the same line from which The question remains, how "nice" and pythonic it is to bend the normal flow of code execution. As Streamlit shows, it's easy to find examples where such an approach leads to unexpected behavior. |
Beta Was this translation helpful? Give feedback.
-
Coming from streamlit I prefer nicegui callbacks and that's why I will not going back to streamlit. |
Beta Was this translation helpful? Give feedback.
Hi Eli,
Our problem with Streamlit was mainly due to the way it handles user interaction. Writing
if st.button('Click'): st.text('Hey!')
is super cool. But it hides the underlying event loop by implicitly re-evaluating the script. That works in simple cases, but it gets quickly rather complicated to achieve apparently "normal" behavior.State
If the script starts with loading some non-constant initial state, e.g. from an external source, this state is reset whenever the script is processed again. The following script already shows inconsistent behavior. Initializing
state
with a constant works, but assigning a non-constant random value overwrites the user-selected state every time.