Multi-page app with session state

@fdubinski, @AteBitHavoc,

Are you still encountering this error after the fix I’ve made recently? Could you post your traceback error to check where the exception is raised?


Hi! Thanks for your precious work. I have this problem: inside this for loop it seems that the session state is not working because it cannot save the values if i turn back on this page after i move to another page.
Do you know why?

Hello @davins90,

Does it raise any exception, or it just fails to save your values silently?

Hey @okld,

thanks for the reply. It just fails to save the results, unfortunately. It works on a single “number_input/selectboc/ecc…” but in this for cycle, it doesn’t. If i move forward on the page of the apps i see the values insert, but if i turning back to the page in which I’ve inserted the values, they disappear. I hope to have explained in a good way my situation.
Thanks

Thanks @okld for the nice demo!

I am running into issues when having different state variables depend on each other. Maybe it’s just a logic error. What I am trying to do is to not have state.input depend on state.selectbox if text is written into the text_input. If a different choice is selected, then state.input should depend on it again. Additionally, a function call should update state.input every time it is called.

My example is the same as your gist, but I changed the main() and page_settings() functions. Here is my code:

import streamlit as st
from streamlit.hashing import _CodeHasher

try:
    # Before Streamlit 0.65
    from streamlit.ReportThread import get_report_ctx
    from streamlit.server.Server import Server
except ModuleNotFoundError:
    # After Streamlit 0.65
    from streamlit.report_thread import get_report_ctx
    from streamlit.server.server import Server


def main():
    state = _get_state()
    pages = {
        "Dashboard": page_dashboard,
        "Settings": page_settings,
    }

    st.sidebar.title(":floppy_disk: Page states")
    page = st.sidebar.radio("Select your page", tuple(pages.keys()))

    # Display the selected page with the session state
    pages[page](state)

    # Mandatory to avoid rollbacks with widgets, must be called at the end of your app
    state.sync()


def page_dashboard(state):
    st.title(":chart_with_upwards_trend: Dashboard page")
    display_state_values(state)


def page_settings(state):
    st.title(":wrench: Settings")
    display_state_values(state)

    st.write("---")
    options = ["", "Hello", "World", "Goodbye"]
    state.selectbox = st.selectbox("Select value.", options, options.index(state.selectbox) if state.selectbox else 0)
    state.input = st.text_input("Set input value.", state.input or "")
    state.input = state.selectbox

    state.function = st.button("Function")
    if state.function:
        state.input = state.input + " function"
        print("Press function")


def display_state_values(state):
    st.write("Selectbox state:", state.selectbox)
    st.write("Input state:", state.input)

    if st.button("Clear state"):
        state.clear()


class _SessionState:

    def __init__(self, session, hash_funcs):
        """Initialize SessionState instance."""
        self.__dict__["_state"] = {
            "data": {},
            "hash": None,
            "hasher": _CodeHasher(hash_funcs),
            "is_rerun": False,
            "session": session,
        }

    def __call__(self, **kwargs):
        """Initialize state data once."""
        for item, value in kwargs.items():
            if item not in self._state["data"]:
                self._state["data"][item] = value

    def __getitem__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)
        
    def __getattr__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)

    def __setitem__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value

    def __setattr__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value
    
    def clear(self):
        """Clear session state and request a rerun."""
        self._state["data"].clear()
        self._state["session"].request_rerun()
    
    def sync(self):
        """Rerun the app with all state values up to date from the beginning to fix rollbacks."""

        # Ensure to rerun only once to avoid infinite loops
        # caused by a constantly changing state value at each run.
        #
        # Example: state.value += 1
        if self._state["is_rerun"]:
            self._state["is_rerun"] = False
        
        elif self._state["hash"] is not None:
            if self._state["hash"] != self._state["hasher"].to_bytes(self._state["data"], None):
                self._state["is_rerun"] = True
                self._state["session"].request_rerun()

        self._state["hash"] = self._state["hasher"].to_bytes(self._state["data"], None)


def _get_session():
    session_id = get_report_ctx().session_id
    session_info = Server.get_current()._get_session_info(session_id)

    if session_info is None:
        raise RuntimeError("Couldn't get your Streamlit Session object.")
    
    return session_info.session


def _get_state(hash_funcs=None):
    session = _get_session()

    if not hasattr(session, "_custom_session_state"):
        session._custom_session_state = _SessionState(session, hash_funcs)

    return session._custom_session_state


if __name__ == "__main__":
    main()
1 Like

Is it possible to implement session state with st.button? It seems to be the only widget that is not supported with this framework at the moment.

1 Like

Hello @e-tony, welcome to the forum!

Indeed, this is just a matter of logic. Are you expecting something like this? (I’ve removed pages)

from streamlit.hashing import _CodeHasher
from streamlit.report_thread import get_report_ctx
from streamlit.server.server import Server
from random import choices
from string import ascii_letters
import streamlit as st


DATES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]


def main():
    state = _get_state()

    st.write("**Current state:**", state.input)

    predef = st.selectbox("Predefined input", options=DATES)
    custom = st.text_input("Custom input")
    random = "".join(choices(ascii_letters, k=10))

    # This can be a function which changes state.input
    if st.button("Random input"):
        state.input = random

    # Here we check if text input is empty or not
    elif custom:
        state.input = custom

    # If we didn't call our function, and text input is empty, we use predefined choices
    else:
        state.input = predef

    # Mandatory to avoid rollbacks with widgets, must be called at the end of your app
    state.sync()


class _SessionState:

    def __init__(self, session, hash_funcs):
        """Initialize SessionState instance."""
        self.__dict__["_state"] = {
            "data": {},
            "hash": None,
            "hasher": _CodeHasher(hash_funcs),
            "is_rerun": False,
            "session": session,
        }

    def __call__(self, **kwargs):
        """Initialize state data once."""
        for item, value in kwargs.items():
            if item not in self._state["data"]:
                self._state["data"][item] = value

    def __getitem__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)
        
    def __getattr__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)

    def __setitem__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value

    def __setattr__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value
    
    def clear(self):
        """Clear session state and request a rerun."""
        self._state["data"].clear()
        self._state["session"].request_rerun()
    
    def sync(self):
        """Rerun the app with all state values up to date from the beginning to fix rollbacks."""

        # Ensure to rerun only once to avoid infinite loops
        # caused by a constantly changing state value at each run.
        #
        # Example: state.value += 1
        if self._state["is_rerun"]:
            self._state["is_rerun"] = False
        
        elif self._state["hash"] is not None:
            if self._state["hash"] != self._state["hasher"].to_bytes(self._state["data"], None):
                self._state["is_rerun"] = True
                self._state["session"].request_rerun()

        self._state["hash"] = self._state["hasher"].to_bytes(self._state["data"], None)


def _get_session():
    session_id = get_report_ctx().session_id
    session_info = Server.get_current()._get_session_info(session_id)

    if session_info is None:
        raise RuntimeError("Couldn't get your Streamlit Session object.")
    
    return session_info.session


def _get_state(hash_funcs=None):
    session = _get_session()

    if not hasattr(session, "_custom_session_state"):
        session._custom_session_state = _SessionState(session, hash_funcs)

    return session._custom_session_state


if __name__ == "__main__":
    main()
1 Like

Hello @mkhorasani,

The persisting equivalent to buttons are checkboxes IMO. And buttons doesn’t take a value parameter.
How do you imagine (or plan to use) session state with buttons?

Thanks a lot for the example @okld!

I will play around with that and see if I run into any issues.

I hacked together a more type-safe example for us enterprise-y types :stuck_out_tongue:

8 Likes

Thanks for the idea of assigning default values from variables stored session! The SessionState file works great too. I can you explain the sync() function a bit.

Thanks again!

It’s a requirement to sync back the state at the end of each page refresh, so as to tell the session “keep these values when Streamlit reruns the page”. Otherwise they would be lost like all other values, as there’s not really a traditional main loop otherwise to keep track of state.

In my posted example, I’ve just wrapped it as a decorator to show how you can bury it a bit.

2 Likes

Nice work @okld with this Multi-page app with session states. Really helpful.
I have some questions:
1.- Is there any way to make a file containing all data from the active SessionState ?
2.- Can you load that file in another session to reuse the same data ?

Thank you! :herb:

1 Like

Hi @an_pas,
I’ve used dill library to save and load the states.

To save:
dill.dump(state._state[‘data’], filename)

To load:
state._state[‘data’] = dill.load(filename)

I hope it helps you!
Wilber

1 Like

Hi @delbrison thanks for the answer. I understand the idea, but never worked with the dill library before, so I’m encountering some errors. I will leave all the procedure here, maybe it can help someone.
I will use 2 buttons, for saving and loading the states.

1.- Saving button
TypeError: file must have a ‘write’ attribute
following this page: Fix Python Pickle TypeError: file must have a 'write' attribute Error - Python Tutorial

I wrote something like this and seems to be working fine:

if st.button("Save State File"):
        with open("binary_list.bin", "wb") as filename:
            dill.dump(state._state['data'], filename)

2.- Loading button
TypeError: file must have a ‘read’ and ‘readline’ attributes
following this page: Fix Python Pickle Load TypeError: file must have 'read' and 'readline' attributes Error - Python Tutorial

I wrote something like this:

if st.button("Load State File"):
        with open("binary_list.bin", "rb") as pickle_file:
            state._state['data'] = dill.load(pickle_file)

- Working Version
I have a working version with the last save state working almost properly, I can save the current session and later on load that same data.
Have some problems with the csv file uploader, when I try uploading a new csv file the data doesn’t change.

- Some improvements that could be made
Using a file uploader system to save and load the binary file with whatever name you want.

@delbrison if you could share more information about your implementation I will be grateful.

My project consist of a csv file uploader to make some personalized graphics.
I made a simplified version of the file:
1.- You can ulpoad a csv file.
2.- Using pandas library to read it.
3.- Print dataframe with Streamlit.

Is working properly but the new buttons added aren’t (in the code those lines are commented 58-63). If someone could give me a hand to figure what I’m doing wrong. Here is the link to the code in github: StreamlitCSVgraphicMaker/g00.py at main · apad0998/StreamlitCSVgraphicMaker · GitHub

Suggestions are welcomed.

1 Like

Thanks for this work around @okld. This saved my day.

3 Likes

This is awesome, thanks a lot!

1 Like

I have a problem with this code as such that a programatically changed input value is not updated. In the excample code below the text_input widget is only updated to ‘xxx’ on the first run. After this, the text_input shows whatever is entered into the widget. The st.write(state.test) stills gives ‘xxx’ . Any ideas?
‘session_module’ contains the code from the gist discussed here.

import streamlit as st
import session_module as sm

def main():
    
    state = sm._get_state()

    st.title('session test')    
    st.write(state.test)
    state.test = st.text_input('enter something',state.test or 'default')
    state.test = 'xxx'
    st.write(state.test)

    state.sync()


main()

Or ignore the sample code and simply add this to the main() function in the orginal code before state.sync():
state.input = ‘changed’

With the save states, you hash your data, but how can you hash this:

UnhashableTypeError : Cannot hash object of type cx_Oracle.Connection , found in something.

Is there any way the data dictionary can be divided into values that can be cleared and values that one wants to persist through different states? @okld ? @andfanilo The page you mentioned does not exist anymore : -(