class to patch streamlit functions for displaying content in jupyter notebooks
Exported source
class StreamlitPatcher:"""class to patch streamlit functions for displaying content in jupyter notebooks"""def__init__(self):self.is_registered: bool=Falseself.registered_methods: tp.Set[str] =set()def jupyter(self):"""patches streamlit methods to display content in jupyter notebooks"""# patch streamlit methods from MAPPING property dictfor method_name, wrapper inself.MAPPING.items():self._wrap(method_name, wrapper)self.is_registered =True@staticmethoddef _get_streamlit_methods():"""get all streamlit methods"""return [attr for attr indir(st) ifnot attr.startswith("_")]
Exported source
@patch_to(StreamlitPatcher, cls_method=False)def _wrap( cls, method_name: str, wrapper: tp.Callable,) ->None:"""make a streamlit method jupyter friendly Parameters ---------- method_name : str which method to jupyterify wrapper : tp.Callable wrapper function to use """if IN_IPYTHON: # only patch if in jupyter trg =getattr(st, method_name) # get the streamlit methodsetattr(st, method_name, wrapper(trg)) # patch the method cls.registered_methods.add(method_name) # add to registered methods
sp = StreamlitPatcher()assertnot sp.is_registered, "StreamlitPatcher is already registered"
Modifying streamlit
The way we will modify streamlit methods is by putting them through a decorator. This decorator will check if we are in a jupyter notebook, and if so, it will take the input and display it in the notebook.
Else it will use the original streamlit method.
st.write
sp._wrap("write", _st_write)with capture_output() as cap: st.write("hello") got = cap._outputs[0]["data"]expected = {"text/plain": "<IPython.core.display.Markdown object>","text/markdown": "hello",}assert got == expected, "check that the output is correct"st.write("hello")
with capture_output() as cap: st.title("foo") got = cap._outputs[0]["data"]["text/markdown"]test_eq(got, "# foo")
with capture_output() as cap: st.header("foo") got = cap._outputs[0]["data"]["text/markdown"]test_eq(got, "## foo")
with capture_output() as cap: st.subheader("foo") got = cap._outputs[0]["data"]["text/markdown"]test_eq(got, "### foo")
# these should failtest_fail(lambda: st.title(df), contains="Unsupported type")test_fail(lambda: st.header(df), contains="Unsupported type")test_fail(lambda: st.subheader(df), contains="Unsupported type")test_fail(lambda: st.subheader(1), contains="Unsupported type")
st.caption
st.caption("This is a string that explains something above.")st.caption("A caption with _italics_ :blue[colors] and emojis :sunglasses:")st.caption("A caption with \n newlines")
This is a string that explains something above.
A caption with italics :blue[colors] and emojis :sunglasses:
A caption with newlines
patch some methods to simply display the input in jupyter
sp._wrap("markdown", functools.partial(_st_type_check, allowed_types=str))test_fail(lambda: st.markdown(df), contains="Unsupported type")st.markdown("This is **bold** text in markdown")
The streamlitcache method is used to cache the output of a function. This is useful for functions that take a long time to run, and we want to avoid running them every time we run the app.
If we are in a jupyter notebook, we can’t use the streamlitcache method, so we will replace the streamlitcache method with a dummy method that does nothing.
# verify that during patching we didn't change the name or docstringassert st.cache.__name__=="cache"assert"@st.cache"in tp.cast(str, st.cache.__doc__), "check that the docstring is correct"
# test caching@st.cache_data()def get_data(): st.write("Getting data...")for i in tqdm(range(5)): time.sleep(0.1)return pd.DataFrame({"c": [7, 8, 9], "d": [10, 11, 12]})df = get_data()st.write(df)
Getting data…
c
d
0
7
10
1
8
11
2
9
12
# test that the cache in jupyter does not affect get_datadf = get_data()with capture_output() as cap: st.write(df) got = cap._outputs[0]["data"]expected = {"text/plain": " c d\n0 7 10\n1 8 11\n2 9 12","text/html": '<div>\n<style scoped>\n .dataframe tbody tr th:only-of-type {\n vertical-align: middle;\n }\n\n .dataframe tbody tr th {\n vertical-align: top;\n }\n\n .dataframe thead th {\n text-align: right;\n }\n</style>\n<table border="1" class="dataframe">\n <thead>\n <tr style="text-align: right;">\n <th></th>\n <th>c</th>\n <th>d</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>7</td>\n <td>10</td>\n </tr>\n <tr>\n <th>1</th>\n <td>8</td>\n <td>11</td>\n </tr>\n <tr>\n <th>2</th>\n <td>9</td>\n <td>12</td>\n </tr>\n </tbody>\n</table>\n</div>',}assert got == expected, "check that the output is correct"
Getting data…
# test caching@st.cache_resource(ttl=3600)def get_resource(): st.write("Getting resource...")for i in tqdm(range(5)): time.sleep(0.1)return {"foo": "bar","baz": [1, 2, 3],"qux": {"a": 1, "b": 2, "c": 3}, }expected = {"foo": "bar","baz": [1, 2, 3],"qux": {"a": 1, "b": 2, "c": 3},}got = get_resource()assert got == expected, "check that the output is correct"
Getting resource…
# test that the cache in jupyter does not affect get_datarecords = get_resource()with capture_output() as cap: st.write(records) got = cap._outputs[0]["data"]expected = {"text/plain": "{'foo': 'bar', 'baz': [1, 2, 3], 'qux': {'a': 1, 'b': 2, 'c': 3}}"}assert got == expected, "check that the output is correct"
Getting resource…
st.expander
Note that this will be an exception from the usual wrapper logic.
Since st.expander is used as a context manager, we replace it with a dummy class that displays the input in jupyter.
sp._wrap("expander", _st_expander)
with st.expander("Expand me!", expanded=False): st.markdown("""The **#30DaysOfStreamlit** is a coding challenge designed to help you get started in building Streamlit apps.Particularly, you'll be able to:- Set up a coding environment for building Streamlit apps- Build your first Streamlit app- Learn about all the awesome input/output widgets to use for your Streamlit app """ ) st.write("**More text, we can expand as many streamlit elements as we want**")
expander starts: Expand me!
The #30DaysOfStreamlit is a coding challenge designed to help you get started in building Streamlit apps.
Particularly, you’ll be able to: - Set up a coding environment for building Streamlit apps - Build your first Streamlit app - Learn about all the awesome input/output widgets to use for your Streamlit app
More text, we can expand as many streamlit elements as we want
st.multiselect("Multiselect with defaults: ", options=["nbdev", "streamlit", "jupyter", "fastcore"], default=["jupyter", "streamlit"],)
('jupyter', 'streamlit')
st.metric
sp._wrap("metric", _st_metric)
# test that we don't allow invalid values for delta_color and label_visibilitytest_fail(lambda: st.metric("Speed", 300, 210, delta_color="FOOBAR", label_visibility="hidden" ), contains="delta_color",)test_fail(lambda: st.metric("Speed", 300, 210, delta_color="normal", label_visibility="FOOBAR" ), contains="label_visibility",)# display a metricst.metric("Speed", 300, 210, delta_color="normal", label_visibility="hidden")
2023-03-06 17:34:09.265 WARNING __main__: `delta_color` argument is not supported in Jupyter notebooks, but will be applied in Streamlit
2023-03-06 17:34:09.266 WARNING __main__: `label_visibility` argument is not supported in Jupyter notebooks, but will be applied in Streamlit
2023-03-06 17:34:09.267 WARNING __main__: plotly is not installed, falling back to default st.metric implementation
To use plotly, run `pip install plotly`
st.metric widget (this will work as expected in streamlit)
st.columns
ToDo: - [ ] add support for st.columns in jupyter
# logger.warning("Not implemented yet")
StreamlitPatcher.MAPPING
Mapping is a dictionary that maps the streamlit method to the method we want to use instead.
This is used when StreamlitPatcher.jupyter() is called.
sp = StreamlitPatcher()
assertnot sp.registered_methods, "registered methods should be empty at this point"