Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callbacks vs Streamlit-Like "if"-statements #1

Closed
elimintz opened this issue May 24, 2021 · 4 comments
Closed

Callbacks vs Streamlit-Like "if"-statements #1

elimintz opened this issue May 24, 2021 · 4 comments

Comments

@elimintz
Copy link

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 [email protected] or you can also open an issue in the JustPy repository.

Eli

@falkoschindler
Copy link
Contributor

falkoschindler commented May 24, 2021

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.

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:
https://github.com/zauberzeug/odrive-gui/blob/9d8a6d3b46a8e1f200670aa837bff476d98504a5/src/main.py#L21

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:
https://github.com/zauberzeug/odrive-gui/blob/9d8a6d3b46a8e1f200670aa837bff476d98504a5/src/main.py#L40

Today (on the main branch) we avoid this struggle using NiceGUI:
https://github.com/zauberzeug/odrive-gui/blob/main/src/main.py

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.

@elimintz
Copy link
Author

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.

@rodja rodja changed the title Improvements in JustPy Callbacks vs Streamlit-Like "if"-statements May 26, 2021
@rodja
Copy link
Member

rodja commented May 26, 2021

I renamed the issue from Improvements in JustPy to Callbacks vs Streamlit-Like "if"-statements for better clarification of the topic.

@falkoschindler
Copy link
Contributor

falkoschindler commented May 26, 2021

@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 magic_button(...) has been called. The body of the if-statement is extracted and assigned to the click handler of a normal ui.button(...), together with global and local variables of the corresponding execution frame.

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.

@rodja rodja pinned this issue Jan 18, 2022
@zauberzeug zauberzeug locked and limited conversation to collaborators Feb 19, 2022
@rodja rodja converted this issue into discussion #21 Feb 19, 2022
@rodja rodja unpinned this issue Mar 18, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants