Repository: streamlit/streamlit

Issue:
Page Config check is problematic when deployed.
# Summary
The error that is thrown if page config is run multiple times or is not the first run shows up on rerun. For some reason, this only seems to happen when deployed.

# Steps to reproduce
1. Deploy this code sample to [heroku](https://calm-wave-95641.herokuapp.com/)
```
import streamlit as st
import pandas as pd

st.beta_set_page_config(
    page_title="{0}",
    page_icon="{1}",
    layout="wide",
    initial_sidebar_state="collapsed"
)

options = pd.DataFrame({'Options': ['radio', 'multiselect', 'selectbox']})

st.radio("Dataframe as input for radio buttons", options)
```
2. Switch between radios a variable amount of times to cause reruns.

## Expected behavior:
Nothing

## Actual behavior:
![image](https://user-images.githubusercontent.com/24946400/90166542-6d669c80-dd68-11ea-83a6-e928b4598d61.png)



Discussion:

Resolving patch:
diff --git a/lib/streamlit/report_session.py b/lib/streamlit/report_session.py
--- a/lib/streamlit/report_session.py
+++ b/lib/streamlit/report_session.py
@@ -41,7 +41,6 @@
 from streamlit.storage.s3_storage import S3Storage
 from streamlit.watcher.local_sources_watcher import LocalSourcesWatcher
 import streamlit.elements.exception_proto as exception_proto
-from streamlit.errors import StreamlitAPIException
 
 LOGGER = get_logger(__name__)
 
@@ -108,9 +107,6 @@ def __init__(self, ioloop, script_path, command_line, uploaded_file_manager):
 
         self._scriptrunner = None
 
-        # set_page_config is allowed at most once, as the very first st.command
-        self._set_page_config_allowed = True
-
         LOGGER.debug("ReportSession initialized (id=%s)", self.id)
 
     def flush_browser_queue(self):
@@ -175,17 +171,6 @@ def enqueue(self, msg):
             if scriptrunner is not None:
                 scriptrunner.maybe_handle_execution_control_request()
 
-        if msg.HasField("page_config_changed") and not self._set_page_config_allowed:
-            raise StreamlitAPIException(
-                "`beta_set_page_config()` can only be called once per app, "
-                + "and must be called as the first Streamlit command in your script.\n\n"
-                + "For more information refer to the [docs]"
-                + "(https://docs.streamlit.io/en/stable/api.html#streamlit.beta_set_page_config)."
-            )
-
-        if msg.HasField("delta") or msg.HasField("page_config_changed"):
-            self._set_page_config_allowed = False
-
         self._report.enqueue(msg)
 
     def enqueue_exception(self, e):
diff --git a/lib/streamlit/report_thread.py b/lib/streamlit/report_thread.py
--- a/lib/streamlit/report_thread.py
+++ b/lib/streamlit/report_thread.py
@@ -15,6 +15,7 @@
 import threading
 
 from streamlit.logger import get_logger
+from streamlit.errors import StreamlitAPIException
 
 LOGGER = get_logger(__name__)
 
@@ -52,17 +53,34 @@ def __init__(
         # cursor (type AbstractCursor).
         self.cursors = {}
         self.session_id = session_id
-        self.enqueue = enqueue
+        self._enqueue = enqueue
         self.query_string = query_string
         self.widgets = widgets
         self.widget_ids_this_run = widget_ids_this_run
         self.uploaded_file_mgr = uploaded_file_mgr
 
+        # set_page_config is allowed at most once, as the very first st.command
+        self._set_page_config_allowed = True
+
     def reset(self, query_string=""):
         self.cursors = {}
         self.widget_ids_this_run.clear()
         self.query_string = query_string
 
+    def enqueue(self, msg):
+        if msg.HasField("page_config_changed") and not self._set_page_config_allowed:
+            raise StreamlitAPIException(
+                "`beta_set_page_config()` can only be called once per app, "
+                + "and must be called as the first Streamlit command in your script.\n\n"
+                + "For more information refer to the [docs]"
+                + "(https://docs.streamlit.io/en/stable/api.html#streamlit.beta_set_page_config)."
+            )
+
+        if msg.HasField("delta") or msg.HasField("page_config_changed"):
+            self._set_page_config_allowed = False
+
+        self._enqueue(msg)
+
 
 class _WidgetIDSet(object):
     """Stores a set of widget IDs. Safe to mutate from multiple threads."""


Test patch:
diff --git a/lib/tests/streamlit/report_context_test.py b/lib/tests/streamlit/report_context_test.py
new file mode 100644
--- /dev/null
+++ b/lib/tests/streamlit/report_context_test.py
@@ -0,0 +1,37 @@
+from unittest.mock import MagicMock, patch
+import unittest
+
+from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
+from streamlit.errors import StreamlitAPIException
+from streamlit.report_thread import ReportContext
+
+
+class ReportContextTest(unittest.TestCase):
+    def test_set_page_config_immutable(self):
+        """st.set_page_config must be called at most once"""
+
+        fake_enqueue = lambda msg: None
+        ctx = ReportContext("TestSessionID", fake_enqueue, "", None, None, None)
+
+        msg = ForwardMsg()
+        msg.page_config_changed.title = "foo"
+
+        ctx.enqueue(msg)
+        with self.assertRaises(StreamlitAPIException):
+            ctx.enqueue(msg)
+
+    def test_set_page_config_first(self):
+        """st.set_page_config must be called before other st commands"""
+
+        fake_enqueue = lambda msg: None
+        ctx = ReportContext("TestSessionID", fake_enqueue, "", None, None, None)
+
+        markdown_msg = ForwardMsg()
+        markdown_msg.delta.new_element.markdown.body = "foo"
+
+        msg = ForwardMsg()
+        msg.page_config_changed.title = "foo"
+
+        ctx.enqueue(markdown_msg)
+        with self.assertRaises(StreamlitAPIException):
+            ctx.enqueue(msg)
diff --git a/lib/tests/streamlit/report_session_test.py b/lib/tests/streamlit/report_session_test.py
--- a/lib/tests/streamlit/report_session_test.py
+++ b/lib/tests/streamlit/report_session_test.py
@@ -106,35 +106,6 @@ def get_option(name):
         # skip func when installTracer is on).
         func.assert_not_called()
 
-    @patch("streamlit.report_session.LocalSourcesWatcher")
-    def test_set_page_config_immutable(self, _1):
-        """st.set_page_config must be called at most once"""
-        file_mgr = MagicMock(spec=UploadedFileManager)
-        rs = ReportSession(None, "", "", file_mgr)
-
-        msg = ForwardMsg()
-        msg.page_config_changed.title = "foo"
-
-        rs.enqueue(msg)
-        with self.assertRaises(StreamlitAPIException):
-            rs.enqueue(msg)
-
-    @patch("streamlit.report_session.LocalSourcesWatcher")
-    def test_set_page_config_first(self, _1):
-        """st.set_page_config must be called before other st commands"""
-        file_mgr = MagicMock(spec=UploadedFileManager)
-        rs = ReportSession(None, "", "", file_mgr)
-
-        markdown_msg = ForwardMsg()
-        markdown_msg.delta.new_element.markdown.body = "foo"
-
-        msg = ForwardMsg()
-        msg.page_config_changed.title = "foo"
-
-        rs.enqueue(markdown_msg)
-        with self.assertRaises(StreamlitAPIException):
-            rs.enqueue(msg)
-
     @patch("streamlit.report_session.LocalSourcesWatcher")
     def test_shutdown(self, _1):
         """Test that ReportSession.shutdown behaves sanely."""