Repository: streamlit/streamlit

Issue:
Support `st.sidebar.write()`
How: move `write` away from `__init__.py` and into the `DeltaGenerator` class.


Discussion:
Linked: #722 

Resolving patch:
diff --git a/lib/streamlit/__init__.py b/lib/streamlit/__init__.py
--- a/lib/streamlit/__init__.py
+++ b/lib/streamlit/__init__.py
@@ -90,16 +90,12 @@
 import textwrap as _textwrap
 import threading as _threading
 import traceback as _traceback
-import types as _types
-import json as _json
-import numpy as _np
 import urllib.parse as _parse
 
 from streamlit import code_util as _code_util
 from streamlit import env_util as _env_util
 from streamlit import source_util as _source_util
 from streamlit import string_util as _string_util
-from streamlit import type_util as _type_util
 from streamlit.delta_generator import DeltaGenerator as _DeltaGenerator
 from streamlit.report_thread import add_report_ctx as _add_report_ctx
 from streamlit.report_thread import get_report_ctx as _get_report_ctx
@@ -182,6 +178,7 @@ def _update_logger():
 vega_lite_chart = _main.vega_lite_chart  # noqa: E221
 video = _main.video  # noqa: E221
 warning = _main.warning  # noqa: E221
+write = _main.write  # noqa: E221
 beta_color_picker = _main.beta_color_picker  # noqa: E221
 
 # Config
@@ -224,198 +221,6 @@ def set_option(key, value):
     )
 
 
-# Special methods:
-
-_HELP_TYPES = (
-    _types.BuiltinFunctionType,
-    _types.BuiltinMethodType,
-    _types.FunctionType,
-    _types.MethodType,
-    _types.ModuleType,
-)  # type: Tuple[Type[Any], ...]
-
-
-def write(*args, **kwargs):
-    """Write arguments to the app.
-
-    This is the Swiss Army knife of Streamlit commands: it does different
-    things depending on what you throw at it. Unlike other Streamlit commands,
-    write() has some unique properties:
-
-    1. You can pass in multiple arguments, all of which will be written.
-    2. Its behavior depends on the input types as follows.
-    3. It returns None, so it's "slot" in the App cannot be reused.
-
-    Parameters
-    ----------
-    *args : any
-        One or many objects to print to the App.
-
-        Arguments are handled as follows:
-
-        - write(string)     : Prints the formatted Markdown string, with
-            support for LaTeX expression and emoji shortcodes.
-            See docs for st.markdown for more.
-        - write(data_frame) : Displays the DataFrame as a table.
-        - write(error)      : Prints an exception specially.
-        - write(func)       : Displays information about a function.
-        - write(module)     : Displays information about the module.
-        - write(dict)       : Displays dict in an interactive widget.
-        - write(obj)        : The default is to print str(obj).
-        - write(mpl_fig)    : Displays a Matplotlib figure.
-        - write(altair)     : Displays an Altair chart.
-        - write(keras)      : Displays a Keras model.
-        - write(graphviz)   : Displays a Graphviz graph.
-        - write(plotly_fig) : Displays a Plotly figure.
-        - write(bokeh_fig)  : Displays a Bokeh figure.
-        - write(sympy_expr) : Prints SymPy expression using LaTeX.
-
-    unsafe_allow_html : bool
-        This is a keyword-only argument that defaults to False.
-
-        By default, any HTML tags found in strings will be escaped and
-        therefore treated as pure text. This behavior may be turned off by
-        setting this argument to True.
-
-        That said, *we strongly advise* against it*. It is hard to write secure
-        HTML, so by using this argument you may be compromising your users'
-        security. For more information, see:
-
-        https://github.com/streamlit/streamlit/issues/152
-
-        **Also note that `unsafe_allow_html` is a temporary measure and may be
-        removed from Streamlit at any time.**
-
-        If you decide to turn on HTML anyway, we ask you to please tell us your
-        exact use case here:
-        https://discuss.streamlit.io/t/96 .
-
-        This will help us come up with safe APIs that allow you to do what you
-        want.
-
-    Example
-    -------
-
-    Its simplest use case is to draw Markdown-formatted text, whenever the
-    input is a string:
-
-    >>> write('Hello, *World!* :sunglasses:')
-
-    .. output::
-       https://share.streamlit.io/0.50.2-ZWk9/index.html?id=Pn5sjhgNs4a8ZbiUoSTRxE
-       height: 50px
-
-    As mentioned earlier, `st.write()` also accepts other data formats, such as
-    numbers, data frames, styled data frames, and assorted objects:
-
-    >>> st.write(1234)
-    >>> st.write(pd.DataFrame({
-    ...     'first column': [1, 2, 3, 4],
-    ...     'second column': [10, 20, 30, 40],
-    ... }))
-
-    .. output::
-       https://share.streamlit.io/0.25.0-2JkNY/index.html?id=FCp9AMJHwHRsWSiqMgUZGD
-       height: 250px
-
-    Finally, you can pass in multiple arguments to do things like:
-
-    >>> st.write('1 + 1 = ', 2)
-    >>> st.write('Below is a DataFrame:', data_frame, 'Above is a dataframe.')
-
-    .. output::
-       https://share.streamlit.io/0.25.0-2JkNY/index.html?id=DHkcU72sxYcGarkFbf4kK1
-       height: 300px
-
-    Oh, one more thing: `st.write` accepts chart objects too! For example:
-
-    >>> import pandas as pd
-    >>> import numpy as np
-    >>> import altair as alt
-    >>>
-    >>> df = pd.DataFrame(
-    ...     np.random.randn(200, 3),
-    ...     columns=['a', 'b', 'c'])
-    ...
-    >>> c = alt.Chart(df).mark_circle().encode(
-    ...     x='a', y='b', size='c', color='c', tooltip=['a', 'b', 'c'])
-    >>>
-    >>> st.write(c)
-
-    .. output::
-       https://share.streamlit.io/0.25.0-2JkNY/index.html?id=8jmmXR8iKoZGV4kXaKGYV5
-       height: 200px
-
-    """
-    try:
-        string_buffer = []  # type: List[str]
-        unsafe_allow_html = kwargs.get("unsafe_allow_html", False)
-
-        def flush_buffer():
-            if string_buffer:
-                markdown(
-                    " ".join(string_buffer), unsafe_allow_html=unsafe_allow_html,
-                )  # noqa: F821
-                string_buffer[:] = []
-
-        for arg in args:
-            # Order matters!
-            if isinstance(arg, str):
-                string_buffer.append(arg)
-            elif _type_util.is_dataframe_like(arg):
-                flush_buffer()
-                if len(_np.shape(arg)) > 2:
-                    text(arg)
-                else:
-                    dataframe(arg)  # noqa: F821
-            elif isinstance(arg, Exception):
-                flush_buffer()
-                exception(arg)  # noqa: F821
-            elif isinstance(arg, _HELP_TYPES):
-                flush_buffer()
-                help(arg)
-            elif _type_util.is_altair_chart(arg):
-                flush_buffer()
-                altair_chart(arg)
-            elif _type_util.is_type(arg, "matplotlib.figure.Figure"):
-                flush_buffer()
-                pyplot(arg)
-            elif _type_util.is_plotly_chart(arg):
-                flush_buffer()
-                plotly_chart(arg)
-            elif _type_util.is_type(arg, "bokeh.plotting.figure.Figure"):
-                flush_buffer()
-                bokeh_chart(arg)
-            elif _type_util.is_graphviz_chart(arg):
-                flush_buffer()
-                graphviz_chart(arg)
-            elif _type_util.is_sympy_expession(arg):
-                flush_buffer()
-                latex(arg)
-            elif _type_util.is_keras_model(arg):
-                from tensorflow.python.keras.utils import vis_utils
-
-                flush_buffer()
-                dot = vis_utils.model_to_dot(arg)
-                graphviz_chart(dot.to_string())
-            elif isinstance(arg, (dict, list)):
-                flush_buffer()
-                json(arg)
-            elif _type_util.is_namedtuple(arg):
-                flush_buffer()
-                json(_json.dumps(arg._asdict()))
-            elif _type_util.is_pydeck(arg):
-                flush_buffer()
-                pydeck_chart(arg)
-            else:
-                string_buffer.append("`%s`" % str(arg).replace("`", "\\`"))
-
-        flush_buffer()
-
-    except Exception as exc:
-        exception(exc)
-
-
 def experimental_show(*args):
     """Write arguments and *argument names* to your app for debugging purposes.
 
diff --git a/lib/streamlit/delta_generator.py b/lib/streamlit/delta_generator.py
--- a/lib/streamlit/delta_generator.py
+++ b/lib/streamlit/delta_generator.py
@@ -62,6 +62,7 @@
 from streamlit.elements.slider import SliderMixin
 from streamlit.elements.image_proto import ImageMixin
 from streamlit.elements.pyplot import PyplotMixin
+from streamlit.elements.write import WriteMixin
 
 LOGGER = get_logger(__name__)
 
@@ -110,6 +111,7 @@ class DeltaGenerator(
     TextWidgetsMixin,
     TimeWidgetsMixin,
     VegaLiteMixin,
+    WriteMixin,
 ):
     """Creator of Delta protobuf messages.
 
@@ -145,7 +147,7 @@ def __init__(self, container=BlockPath_pb2.BlockPath.MAIN, cursor=None):
 
         # This is either:
         # - None: if this is the running DeltaGenerator for a top-level
-        #   container.
+        #   container (MAIN or SIDEBAR)
         # - RunningCursor: if this is the running DeltaGenerator for a
         #   non-top-level container (created with dg._block())
         # - LockedCursor: if this is a locked DeltaGenerator returned by some
@@ -200,6 +202,10 @@ def _cursor(self):
         else:
             return self._provided_cursor
 
+    @property
+    def _is_top_level(self):
+        return self._provided_cursor is None
+
     def _get_coordinates(self):
         """Returns the element's 4-component location as string like "M.(1,2).3".
 
diff --git a/lib/streamlit/elements/write.py b/lib/streamlit/elements/write.py
new file mode 100644
--- /dev/null
+++ b/lib/streamlit/elements/write.py
@@ -0,0 +1,212 @@
+import types
+import json as json
+import numpy as np
+
+from typing import cast, Any, List, Tuple, Type
+
+from streamlit import type_util
+from streamlit.errors import StreamlitAPIException
+
+# Special methods:
+
+HELP_TYPES = (
+    types.BuiltinFunctionType,
+    types.BuiltinMethodType,
+    types.FunctionType,
+    types.MethodType,
+    types.ModuleType,
+)  # type: Tuple[Type[Any], ...]
+
+
+class WriteMixin:
+    def write(self, *args, **kwargs):
+        """Write arguments to the app.
+
+        This is the Swiss Army knife of Streamlit commands: it does different
+        things depending on what you throw at it. Unlike other Streamlit commands,
+        write() has some unique properties:
+
+        1. You can pass in multiple arguments, all of which will be written.
+        2. Its behavior depends on the input types as follows.
+        3. It returns None, so it's "slot" in the App cannot be reused.
+
+        Parameters
+        ----------
+        *args : any
+            One or many objects to print to the App.
+
+            Arguments are handled as follows:
+
+            - write(string)     : Prints the formatted Markdown string, with
+                support for LaTeX expression and emoji shortcodes.
+                See docs for st.markdown for more.
+            - write(data_frame) : Displays the DataFrame as a table.
+            - write(error)      : Prints an exception specially.
+            - write(func)       : Displays information about a function.
+            - write(module)     : Displays information about the module.
+            - write(dict)       : Displays dict in an interactive widget.
+            - write(obj)        : The default is to print str(obj).
+            - write(mpl_fig)    : Displays a Matplotlib figure.
+            - write(altair)     : Displays an Altair chart.
+            - write(keras)      : Displays a Keras model.
+            - write(graphviz)   : Displays a Graphviz graph.
+            - write(plotly_fig) : Displays a Plotly figure.
+            - write(bokeh_fig)  : Displays a Bokeh figure.
+            - write(sympy_expr) : Prints SymPy expression using LaTeX.
+
+        unsafe_allow_html : bool
+            This is a keyword-only argument that defaults to False.
+
+            By default, any HTML tags found in strings will be escaped and
+            therefore treated as pure text. This behavior may be turned off by
+            setting this argument to True.
+
+            That said, *we strongly advise* against it*. It is hard to write secure
+            HTML, so by using this argument you may be compromising your users'
+            security. For more information, see:
+
+            https://github.com/streamlit/streamlit/issues/152
+
+            **Also note that `unsafe_allow_html` is a temporary measure and may be
+            removed from Streamlit at any time.**
+
+            If you decide to turn on HTML anyway, we ask you to please tell us your
+            exact use case here:
+            https://discuss.streamlit.io/t/96 .
+
+            This will help us come up with safe APIs that allow you to do what you
+            want.
+
+        Example
+        -------
+
+        Its simplest use case is to draw Markdown-formatted text, whenever the
+        input is a string:
+
+        >>> write('Hello, *World!* :sunglasses:')
+
+        .. output::
+        https://share.streamlit.io/0.50.2-ZWk9/index.html?id=Pn5sjhgNs4a8ZbiUoSTRxE
+        height: 50px
+
+        As mentioned earlier, `st.write()` also accepts other data formats, such as
+        numbers, data frames, styled data frames, and assorted objects:
+
+        >>> st.write(1234)
+        >>> st.write(pd.DataFrame({
+        ...     'first column': [1, 2, 3, 4],
+        ...     'second column': [10, 20, 30, 40],
+        ... }))
+
+        .. output::
+        https://share.streamlit.io/0.25.0-2JkNY/index.html?id=FCp9AMJHwHRsWSiqMgUZGD
+        height: 250px
+
+        Finally, you can pass in multiple arguments to do things like:
+
+        >>> st.write('1 + 1 = ', 2)
+        >>> st.write('Below is a DataFrame:', data_frame, 'Above is a dataframe.')
+
+        .. output::
+        https://share.streamlit.io/0.25.0-2JkNY/index.html?id=DHkcU72sxYcGarkFbf4kK1
+        height: 300px
+
+        Oh, one more thing: `st.write` accepts chart objects too! For example:
+
+        >>> import pandas as pd
+        >>> import numpy as np
+        >>> import altair as alt
+        >>>
+        >>> df = pd.DataFrame(
+        ...     np.random.randn(200, 3),
+        ...     columns=['a', 'b', 'c'])
+        ...
+        >>> c = alt.Chart(df).mark_circle().encode(
+        ...     x='a', y='b', size='c', color='c', tooltip=['a', 'b', 'c'])
+        >>>
+        >>> st.write(c)
+
+        .. output::
+        https://share.streamlit.io/0.25.0-2JkNY/index.html?id=8jmmXR8iKoZGV4kXaKGYV5
+        height: 200px
+
+        """
+        # Import dynamically, to resolve circular dependency
+        from streamlit.delta_generator import DeltaGenerator
+
+        # Cast self to DeltaGenerator, to resolve mypy type issues
+        dg = cast(DeltaGenerator, self)
+        string_buffer = []  # type: List[str]
+        unsafe_allow_html = kwargs.get("unsafe_allow_html", False)
+
+        # This bans some valid cases like: e = st.empty(); e.write("a", "b").
+        # BUT: 1) such cases are rare, 2) this rule is easy to understand,
+        # and 3) this rule should be removed once we have st.container()
+        if not dg._is_top_level and len(args) > 1:
+            raise StreamlitAPIException(
+                "Cannot replace a single element with multiple elements.\n\n"
+                "The `write()` method only supports multiple elements when "
+                "inserting elements rather than replacing. That is, only "
+                "when called as `st.write()` or `st.sidebar.write()`."
+            )
+
+        def flush_buffer():
+            if string_buffer:
+                dg.markdown(
+                    " ".join(string_buffer), unsafe_allow_html=unsafe_allow_html,
+                )
+                string_buffer[:] = []
+
+        for arg in args:
+            # Order matters!
+            if isinstance(arg, str):
+                string_buffer.append(arg)
+            elif type_util.is_dataframe_like(arg):
+                flush_buffer()
+                if len(np.shape(arg)) > 2:
+                    dg.text(arg)
+                else:
+                    dg.dataframe(arg)
+            elif isinstance(arg, Exception):
+                flush_buffer()
+                dg.exception(arg)
+            elif isinstance(arg, HELP_TYPES):
+                flush_buffer()
+                dg.help(arg)
+            elif type_util.is_altair_chart(arg):
+                flush_buffer()
+                dg.altair_chart(arg)
+            elif type_util.is_type(arg, "matplotlib.figure.Figure"):
+                flush_buffer()
+                dg.pyplot(arg)
+            elif type_util.is_plotly_chart(arg):
+                flush_buffer()
+                dg.plotly_chart(arg)
+            elif type_util.is_type(arg, "bokeh.plotting.figure.Figure"):
+                flush_buffer()
+                dg.bokeh_chart(arg)
+            elif type_util.is_graphviz_chart(arg):
+                flush_buffer()
+                dg.graphviz_chart(arg)
+            elif type_util.is_sympy_expession(arg):
+                flush_buffer()
+                dg.latex(arg)
+            elif type_util.is_keras_model(arg):
+                from tensorflow.python.keras.utils import vis_utils
+
+                flush_buffer()
+                dot = vis_utils.model_to_dot(arg)
+                dg.graphviz_chart(dot.to_string())
+            elif isinstance(arg, (dict, list)):
+                flush_buffer()
+                dg.json(arg)
+            elif type_util.is_namedtuple(arg):
+                flush_buffer()
+                dg.json(json.dumps(arg._asdict()))
+            elif type_util.is_pydeck(arg):
+                flush_buffer()
+                dg.pydeck_chart(arg)
+            else:
+                string_buffer.append("`%s`" % str(arg).replace("`", "\\`"))
+
+        flush_buffer()


Test patch:
diff --git a/lib/tests/streamlit/delta_generator_test.py b/lib/tests/streamlit/delta_generator_test.py
--- a/lib/tests/streamlit/delta_generator_test.py
+++ b/lib/tests/streamlit/delta_generator_test.py
@@ -133,12 +133,12 @@ def test_nonexistent_method(self):
 
     def test_sidebar_nonexistent_method(self):
         with self.assertRaises(Exception) as ctx:
-            st.sidebar.write()
+            st.sidebar.echo()
 
         self.assertEqual(
             str(ctx.exception),
-            "Method `write()` does not exist for `st.sidebar`. "
-            "Did you mean `st.write()`?",
+            "Method `echo()` does not exist for `st.sidebar`. "
+            "Did you mean `st.echo()`?",
         )
 
     @parameterized.expand(
@@ -169,7 +169,7 @@ def test_sidebar_nonexistent_method(self):
                 "add_rows",
                 "(data=None, **kwargs)",
             ),
-            (st.write, "streamlit", "write", "(*args, **kwargs)"),
+            (st.write, "streamlit.delta_generator", "write", "(*args, **kwargs)"),
         ]
     )
     def test_function_signatures(self, func, module, name, sig):
diff --git a/lib/tests/streamlit/help_test.py b/lib/tests/streamlit/help_test.py
--- a/lib/tests/streamlit/help_test.py
+++ b/lib/tests/streamlit/help_test.py
@@ -99,16 +99,16 @@ def test_st_cache(self):
         )
         self.assertTrue(ds.doc_string.startswith("Function decorator to"))
 
-    def test_st_write(self):
-        """Test st.write function (since it's from __init__)."""
-        st.help(st.write)
+    def test_st_echo(self):
+        """Test st.echo function (since it's from __init__)."""
+        st.help(st.echo)
 
         ds = self.get_delta_from_queue().new_element.doc_string
-        self.assertEqual("write", ds.name)
+        self.assertEqual("echo", ds.name)
         self.assertEqual("streamlit", ds.module)
         self.assertEqual("<class 'function'>", ds.type)
-        self.assertEqual("(*args, **kwargs)", ds.signature)
-        self.assertTrue(ds.doc_string.startswith("Write arguments to the"))
+        self.assertEqual("(code_location='above')", ds.signature)
+        self.assertTrue(ds.doc_string.startswith("Use in a `with` block"))
 
     def test_builtin_func(self):
         """Test a built-in function."""
diff --git a/lib/tests/streamlit/write_test.py b/lib/tests/streamlit/write_test.py
--- a/lib/tests/streamlit/write_test.py
+++ b/lib/tests/streamlit/write_test.py
@@ -15,7 +15,7 @@
 """Streamlit Unit test."""
 
 from collections import namedtuple
-from unittest.mock import call, patch, Mock
+from unittest.mock import call, patch, Mock, PropertyMock
 
 import time
 import unittest
@@ -25,6 +25,7 @@
 
 import streamlit as st
 from streamlit import type_util
+from streamlit.errors import StreamlitAPIException
 
 
 class StreamlitWriteTest(unittest.TestCase):
@@ -39,12 +40,12 @@ class StreamlitWriteTest(unittest.TestCase):
 
     def test_string(self):
         """Test st.write with a string."""
-        with patch("streamlit.markdown") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.markdown") as p:
             st.write("some string")
 
             p.assert_called_once()
 
-        with patch("streamlit.markdown") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.markdown") as p:
             st.write("more", "strings", "to", "pass")
 
             p.assert_called_once_with("more strings to pass", unsafe_allow_html=False)
@@ -67,14 +68,14 @@ def test_dataframe(self):
         self.assertEqual(sorted(data.keys()), sorted(type_util._DATAFRAME_LIKE_TYPES))
 
         for df in data.values():
-            with patch("streamlit.dataframe") as p:
+            with patch("streamlit.delta_generator.DeltaGenerator.dataframe") as p:
                 st.write(df)
 
                 p.assert_called_once()
 
     def test_exception_type(self):
         """Test st.write with exception."""
-        with patch("streamlit.exception") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.exception") as p:
             st.write(Exception("some exception"))
 
             p.assert_called_once()
@@ -82,13 +83,13 @@ def test_exception_type(self):
     def test_help(self):
         """Test st.write with help types."""
         # Test module
-        with patch("streamlit.help") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.help") as p:
             st.write(np)
 
             p.assert_called_once()
 
         # Test function
-        with patch("streamlit.help") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.help") as p:
             st.write(st.set_option)
 
             p.assert_called_once()
@@ -101,7 +102,7 @@ def test_altair_chart(self, is_type):
         class FakeChart(object):
             pass
 
-        with patch("streamlit.altair_chart") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.altair_chart") as p:
             st.write(FakeChart())
 
             p.assert_called_once()
@@ -114,7 +115,7 @@ def test_pyplot(self, is_type):
         class FakePyplot(object):
             pass
 
-        with patch("streamlit.pyplot") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.pyplot") as p:
             st.write(FakePyplot())
 
             p.assert_called_once()
@@ -123,36 +124,36 @@ def test_plotly(self):
         import plotly.graph_objs as go
 
         """Test st.write with plotly object."""
-        with patch("streamlit.plotly_chart") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.plotly_chart") as p:
             st.write([go.Scatter(x=[1, 2], y=[10, 20])])
 
             p.assert_called_once()
 
     def test_dict(self):
         """Test st.write with dict."""
-        with patch("streamlit.json") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.json") as p:
             st.write({"a": 1, "b": 2})
 
             p.assert_called_once()
 
     def test_list(self):
         """Test st.write with list."""
-        with patch("streamlit.json") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.json") as p:
             st.write([1, 2, 3])
 
             p.assert_called_once()
 
     def test_namedtuple(self):
         """Test st.write with list."""
-        with patch("streamlit.json") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.json") as p:
             Boy = namedtuple("Boy", ("name", "age"))
             John = Boy("John", 29)
             st.write(John)
 
             p.assert_called_once()
 
-    @patch("streamlit.markdown")
-    @patch("streamlit.json")
+    @patch("streamlit.delta_generator.DeltaGenerator.markdown")
+    @patch("streamlit.delta_generator.DeltaGenerator.json")
     def test_dict_and_string(self, mock_json, mock_markdown):
         """Test st.write with dict."""
         manager = Mock()
@@ -175,7 +176,7 @@ class SomeObject(object):
             def __str__(self):
                 return "1 * 2 - 3 = 4 `ok` !"
 
-        with patch("streamlit.markdown") as p:
+        with patch("streamlit.delta_generator.DeltaGenerator.markdown") as p:
             st.write(SomeObject())
 
             p.assert_called_once_with(
@@ -187,23 +188,54 @@ def test_exception(self):
         # We patch streamlit.exception to observe it, but we also make sure
         # it's still called (via side_effect). This ensures that it's called
         # with the proper arguments.
-        with patch("streamlit.markdown") as m, patch(
-            "streamlit.exception", side_effect=st.exception
+        with patch("streamlit.delta_generator.DeltaGenerator.markdown") as m, patch(
+            "streamlit.delta_generator.DeltaGenerator.exception",
+            side_effect=st.exception,
         ) as e:
             m.side_effect = Exception("some exception")
-            st.write("some text")
 
-            e.assert_called_once()
+            with self.assertRaises(Exception):
+                st.write("some text")
 
     def test_spinner(self):
         """Test st.spinner."""
         # TODO(armando): Test that the message is actually passed to
         # message.warning
-        with patch("streamlit.empty") as e:
+        with patch("streamlit.delta_generator.DeltaGenerator.empty") as e:
             with st.spinner("some message"):
                 time.sleep(0.15)
             e.assert_called_once_with()
 
+    def test_sidebar(self):
+        """Test st.write in the sidebar."""
+        with patch("streamlit.delta_generator.DeltaGenerator.markdown") as m, patch(
+            "streamlit.delta_generator.DeltaGenerator.help"
+        ) as h:
+            st.sidebar.write("markdown", st.help)
+
+            m.assert_called_once()
+            h.assert_called_once()
+
+    def test_empty(self):
+        """Test st.write from a specific element."""
+        placeholder = st.empty()
+
+        with patch("streamlit.delta_generator.DeltaGenerator.markdown") as p:
+            placeholder.write("One argument is okay...")
+
+            p.assert_called_once()
+
+        with self.assertRaises(StreamlitAPIException):
+            # Also override dg._is_top_level for this test.
+            with patch.object(
+                st.delta_generator.DeltaGenerator,
+                "_is_top_level",
+                new_callable=PropertyMock,
+            ) as top_level:
+                top_level.return_value = False
+
+                placeholder.write("But", "multiple", "args", "should", "fail")
+
 
 def make_is_type_mock(true_type_matchers):
     """Return a function that mocks is_type.