How do I add a "native looking rendering method" to streamlit?

Summary

I have written some python code that renders data in a way that makes sense in our organization. I have several recipes that I use over and over and would like them to fit cleanly into streamlit.

To grossly simplify:

import streamlit as st

def dump_data(region,title, df):
    """ Display formatted information """
    region.markdown(title)
    region.dataframe(df)

def make_page():
    # This gets called by making a container and passing it to my function
    df = get_df()
    c = st.container()
    dump_data(c,title,df)  # Call recipe here...not congruent with streamlit style

    # I want my code to work the same as streamlit code where I just take the
    # region.method to get data on the page
    c = st.container()
    c.dump_data(title,df)

    # Or perhaps better
   c = st.container()
   c.my_package.dump_data(title,df)

What is the recommended way to add my method so it works like other streamlit ‘renderables’ for lack of a better term? I have dug a bit through the code and it isn’t obvious how st a module, is interchangeable with DeltaGenerator and if there are constraints on just patching in new methods into the st package, or a DeltaGenerator.

I am afraid the recommended method is just calling the function. Monkey patching seems to work as expected, though, and it feels safe if it is just a bunch of well known functions and you don’t get too fancy.

PS.

It is actually quite simple. There is a package-level, private instance of DeltaGenerator

Then what looks like package-level functions are actually aliases for the methods of that instance.

I can take that path, but it feels wrong that the underlying streamlit code is “special” and prevents users from extending streamlit cleanly, by cleanly I mean in a way that is not discernable from the baseline API. That way users just need to write pages in the style of streamlit (which is fantastic, simple and clean).

Would an API method registering a method be feasible?

st.register_function(dump_data)

or a decorator

@st_streamlit_extension
def dump_data(region,title, df):
    """ Display formatted information """
    region.markdown(title)
    region.dataframe(df)

both of these would monkey patch the module and the DeltaGenarator class.

How about you create a module with your class and functions. Include the streamlit in the constructor, so you have all the power to use it inside your methods.

Module

"""
my_module.py
"""

import pandas as pd

class MyClass:
    def __init__(self, st):
        self.st = st

    def dump_data(self, df, title=''):
        """ Display formatted information """
        with self.st.container():
            self.st.markdown(title)
            self.st.dataframe(df)


def get_df(fn):
    return pd.read_csv(fn)

Main

import streamlit as st
from my_module import MyClass, get_df


def make_page():
    my = MyClass(st)

    fn = 'shop.csv'
    df = get_df(fn)

    my.dump_data(df, title='First')
    my.dump_data(df, title='Second')


if __name__ == '__main__':
    make_page()
1 Like

It is not so special, most python libraries I know don’t provide a way to extend their API other than monkey patching. And those that does use monkey patching or even more evil tricks under the hood. Which is fine, python gives you a lot of power and there are times when using that power is the right thing to do.

But streamlit favors a functional style, I guess because they think it is more familiar to the target user. That’s why they encourage you to call the package-level functions and use the DeltaGenerator objects as context managers. If you go with that flow, you don’t even need to pass a region argument. Your dump_data is used the same way as the streamlit functions, it is just defined in another module.

import streamlit as st

def dump_data(title, df):
    """Display formatted information."""
    st.markdown(title)
    st.dataframe(df)

def make_page():
    df = get_df()
    c = st.container()
    with c:
        dump_data(title, df)  # Called the same way as a streamlit function, but defined somewhere else.

Now, if you prefer a more OO approach, you can do it, but you’ll have to do extra work. If you want to go the monkey-patching way, a decorator can hide the ugly stuff from the caller and it feels more readable and pythonic than a register function (to me at least).

1 Like

That makes sense.

Let me ask a follow up.

I’ve created a “dropin” alternative for the metric object that allows me to display the metric centered with colors and more size/font control. By drop I don’t mean API is exact, I mean, it is a widget for nicely displaying a numeric value.

In the current state I can never do this:

   value1,value2,value3,value4 = get_values()
   col1,col2,col3,col4 = st.columns(4)
   col1.img_metric("Label1",value1,min=0,max=100,text_color='red',bk_color='green')
   col2.img_metric("Label2",value2,min=0,max=100,text_color='red',bk_color='green')
   col3.img_metric("Label3",value3,min=0,max=100,text_color='red',bk_color='green')
   col4.img_metric("Label4",value4,min=0,max=100,text_color='red',bk_color='green')

  # or perhaps better...or much worse

  for col,label,value in zip(st.columns(4),get_values(),get_labels()):
      col.img_metric(label,value,min=0,max=100,text_color='red',bk_color='green')

Instead I need to say

   value1,value2 = get_values()
   col1,col2 = st.columns(2)
   with col1:
       img_metric("Label1",value1,min=0,max=100,text_color='red',bk_color='green')
   with col2:
       img_metric("Label2",value2,min=0,max=100,text_color='red',bk_color='green')
   with col3:
       img_metric("Label4",value3,min=0,max=100,text_color='red',bk_color='green')
   with col4:
       img_metric("Label4",value4,min=0,max=100,text_color='red',bk_color='green')

This is situation where code that looks like it is doing “streamlit” stuff looks different than the baseline streamlit code and the paradigm col1.img_metric is not possible. You either ALWAYS use a context manager or you pass the DeltaGenerator to a function as I did above. This isn’t that big of a deal…but with streamlit being open source, you want (I think you want) as many people writing extensions as possible with a consistent look and feel. Having them look and feel like the built in seems critical to a clean code experience (low wtf/code-review). Typing in ‘pip install streamlit_imgmetric’ and having new features available in a consistent programming model seems like a very useful thing.

You already know how to arrange things so that you can call col1.img_metric so you can go either way.