class SessionData:
    """
    Contains parameters related to running a script, and also houses
    the ForwardMsgQueue that is used to deliver messages to a connected browser.
    """

    script_path: str
    script_folder: str
    name: str
    command_line: str
    script_run_id: str
    _browser_queue: ForwardMsgQueue

    def __init__(self, script_path: str, command_line: str):
        """Constructor.

        Parameters
        ----------
        script_path : str
            Path of the Python file from which this app is generated.

        command_line : string
            Command line as input by the user

        """
        basename = os.path.basename(script_path)

        self.script_path = os.path.abspath(script_path)
        self.script_folder = os.path.dirname(self.script_path)
        self.name = str(os.path.splitext(basename)[0])

        # The browser queue contains messages that haven't yet been
        # delivered to the browser. Periodically, the server flushes
        # this queue and delivers its contents to the browser.
        self._browser_queue = ForwardMsgQueue()

        self.script_run_id = generate_new_id()

        self.command_line = command_line

    def enqueue(self, msg):
        self._browser_queue.enqueue(msg)

    def clear(self):
        self._browser_queue.clear()

    def flush_browser_queue(self):
        """Clears our browser queue and returns the messages it contained.

        The Server calls this periodically to deliver new messages
        to the browser associated with this session.

        Returns
        -------
        list[ForwardMsg]
            The messages that were removed from the queue and should
            be delivered to the browser.

        """
        return self._browser_queue.flush()
Example #2
0
    def __init__(self, script_name):
        """Initializes the ScriptRunner for the given script_name"""
        # DeltaGenerator deltas will be enqueued into self.forward_msg_queue.
        self.forward_msg_queue = ForwardMsgQueue()

        def enqueue_fn(msg):
            self.forward_msg_queue.enqueue(msg)
            self.maybe_handle_execution_control_request()

        self.script_request_queue = ScriptRequestQueue()
        script_path = os.path.join(os.path.dirname(__file__), "test_data",
                                   script_name)

        super(TestScriptRunner, self).__init__(
            session_id="test session id",
            session_data=SessionData(script_path, "test command line"),
            enqueue_forward_msg=enqueue_fn,
            client_state=ClientState(),
            session_state=SessionState(),
            request_queue=self.script_request_queue,
            uploaded_file_mgr=UploadedFileManager(),
        )

        # Accumulates uncaught exceptions thrown by our run thread.
        self.script_thread_exceptions = []

        # Accumulates all ScriptRunnerEvents emitted by us.
        self.events = []
        self.event_data = []

        def record_event(event, **kwargs):
            self.events.append(event)
            self.event_data.append(kwargs)

        self.on_event.connect(record_event, weak=False)
    def __init__(self, script_path: str, command_line: str):
        """Constructor.

        Parameters
        ----------
        script_path : str
            Path of the Python file from which this app is generated.

        command_line : string
            Command line as input by the user

        """
        basename = os.path.basename(script_path)

        self.script_path = os.path.abspath(script_path)
        self.script_folder = os.path.dirname(self.script_path)
        self.name = str(os.path.splitext(basename)[0])

        # The browser queue contains messages that haven't yet been
        # delivered to the browser. Periodically, the server flushes
        # this queue and delivers its contents to the browser.
        self._browser_queue = ForwardMsgQueue()

        self.script_run_id = generate_new_id()

        self.command_line = command_line
Example #4
0
class DeltaGeneratorTestCase(unittest.TestCase):
    def setUp(self, override_root=True):
        self.forward_msg_queue = ForwardMsgQueue()
        self.override_root = override_root
        self.orig_report_ctx = None
        self.new_script_run_ctx = ScriptRunContext(
            session_id="test session id",
            enqueue=self.forward_msg_queue.enqueue,
            query_string="",
            session_state=SessionState(),
            uploaded_file_mgr=UploadedFileManager(),
        )

        if self.override_root:
            self.orig_report_ctx = get_script_run_ctx()
            add_script_run_ctx(threading.current_thread(),
                               self.new_script_run_ctx)

        self.app_session = FakeAppSession()

    def tearDown(self):
        self.clear_queue()
        if self.override_root:
            add_script_run_ctx(threading.current_thread(),
                               self.orig_report_ctx)

    def get_message_from_queue(self, index=-1):
        """Get a ForwardMsg proto from the queue, by index.

        Returns
        -------
        ForwardMsg
        """
        return self.forward_msg_queue._queue[index]

    def get_delta_from_queue(self, index=-1):
        """Get a Delta proto from the queue, by index.

        Returns
        -------
        Delta
        """
        deltas = self.get_all_deltas_from_queue()
        return deltas[index]

    def get_all_deltas_from_queue(self):
        """Return all the delta messages in our ForwardMsgQueue"""
        return [
            msg.delta for msg in self.forward_msg_queue._queue
            if msg.HasField("delta")
        ]

    def clear_queue(self):
        self.forward_msg_queue._clear()
Example #5
0
    def setUp(self, override_root=True):
        self.forward_msg_queue = ForwardMsgQueue()
        self.override_root = override_root
        self.orig_report_ctx = None
        self.new_script_run_ctx = ScriptRunContext(
            session_id="test session id",
            enqueue=self.forward_msg_queue.enqueue,
            query_string="",
            session_state=SessionState(),
            uploaded_file_mgr=UploadedFileManager(),
        )

        if self.override_root:
            self.orig_report_ctx = get_script_run_ctx()
            add_script_run_ctx(threading.current_thread(),
                               self.new_script_run_ctx)

        self.app_session = FakeAppSession()
Example #6
0
    def test_simple_enqueue(self):
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        self.assertFalse(rq.is_empty())
        queue = rq.flush()
        self.assertTrue(rq.is_empty())
        self.assertEqual(len(queue), 1)
        self.assertTrue(queue[0].new_session.config.allow_run_on_save)
Example #7
0
    def test_replace_element(self):
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)
        rq.enqueue(TEXT_DELTA_MSG1)

        TEXT_DELTA_MSG2.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)
        rq.enqueue(TEXT_DELTA_MSG2)

        queue = rq.flush()
        self.assertEqual(len(queue), 2)
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 0),
                         queue[1].metadata.delta_path)
        self.assertEqual(queue[1].delta.new_element.text.body, "text2")
    def __init__(self, script_name: str):
        """Initializes the ScriptRunner for the given script_name"""
        # DeltaGenerator deltas will be enqueued into self.forward_msg_queue.
        self.forward_msg_queue = ForwardMsgQueue()

        main_script_path = os.path.join(os.path.dirname(__file__), "test_data",
                                        script_name)

        super().__init__(
            session_id="test session id",
            session_data=SessionData(main_script_path, "test command line"),
            client_state=ClientState(),
            session_state=SessionState(),
            uploaded_file_mgr=UploadedFileManager(),
            initial_rerun_data=RerunData(),
        )

        # Accumulates uncaught exceptions thrown by our run thread.
        self.script_thread_exceptions: List[BaseException] = []

        # Accumulates all ScriptRunnerEvents emitted by us.
        self.events: List[ScriptRunnerEvent] = []
        self.event_data: List[Any] = []

        def record_event(sender: Optional[ScriptRunner],
                         event: ScriptRunnerEvent, **kwargs) -> None:
            # Assert that we're not getting unexpected `sender` params
            # from ScriptRunner.on_event
            assert (sender is None
                    or sender == self), "Unexpected ScriptRunnerEvent sender!"

            self.events.append(event)
            self.event_data.append(kwargs)

            # Send ENQUEUE_FORWARD_MSGs to our queue
            if event == ScriptRunnerEvent.ENQUEUE_FORWARD_MSG:
                forward_msg = kwargs["forward_msg"]
                self.forward_msg_queue.enqueue(forward_msg)

        self.on_event.connect(record_event, weak=False)
Example #9
0
    def test_replace_element(self):
        """Enqueuing an element with the same delta_path as another element
        already in the queue should replace the original element.
        """
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)
        rq.enqueue(TEXT_DELTA_MSG1)

        TEXT_DELTA_MSG2.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)
        rq.enqueue(TEXT_DELTA_MSG2)

        queue = rq.flush()
        self.assertEqual(2, len(queue))
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 0),
                         queue[1].metadata.delta_path)
        self.assertEqual("text2", queue[1].delta.new_element.text.body)
Example #10
0
    def test_enqueue_three(self):
        """Enqueue 3 ForwardMsgs."""
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)
        rq.enqueue(TEXT_DELTA_MSG1)

        TEXT_DELTA_MSG2.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 1)
        rq.enqueue(TEXT_DELTA_MSG2)

        queue = rq.flush()
        self.assertEqual(3, len(queue))
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 0),
                         queue[1].metadata.delta_path)
        self.assertEqual("text1", queue[1].delta.new_element.text.body)
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 1),
                         queue[2].metadata.delta_path)
        self.assertEqual("text2", queue[2].delta.new_element.text.body)
Example #11
0
    def test_multiple_containers(self):
        """Deltas should only be coalesced if they're in the same container"""
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        def enqueue_deltas(container: int, path: Tuple[int, ...]):
            # We deep-copy the protos because we mutate each one
            # multiple times.
            msg = copy.deepcopy(TEXT_DELTA_MSG1)
            msg.metadata.delta_path[:] = make_delta_path(container, path, 0)
            rq.enqueue(msg)

            msg = copy.deepcopy(DF_DELTA_MSG)
            msg.metadata.delta_path[:] = make_delta_path(container, path, 1)
            rq.enqueue(msg)

            msg = copy.deepcopy(ADD_ROWS_MSG)
            msg.metadata.delta_path[:] = make_delta_path(container, path, 1)
            rq.enqueue(msg)

        enqueue_deltas(RootContainer.MAIN, ())
        enqueue_deltas(RootContainer.SIDEBAR, (0, 0, 1))

        def assert_deltas(container: int, path: Tuple[int, ...], idx: int):
            # Text delta
            self.assertEqual(make_delta_path(container, path, 0),
                             queue[idx].metadata.delta_path)
            self.assertEqual("text1", queue[idx].delta.new_element.text.body)

            # Dataframe delta
            self.assertEqual(make_delta_path(container, path, 1),
                             queue[idx + 1].metadata.delta_path)
            col0 = queue[
                idx + 1].delta.new_element.data_frame.data.cols[0].int64s.data
            col1 = queue[
                idx + 1].delta.new_element.data_frame.data.cols[1].int64s.data
            self.assertEqual([0, 1, 2], col0)
            self.assertEqual([10, 11, 12], col1)

            # add_rows delta
            self.assertEqual(make_delta_path(container, path, 1),
                             queue[idx + 2].metadata.delta_path)
            ar_col0 = queue[idx +
                            2].delta.add_rows.data.data.cols[0].int64s.data
            ar_col1 = queue[idx +
                            2].delta.add_rows.data.data.cols[1].int64s.data
            self.assertEqual([3, 4, 5], ar_col0)
            self.assertEqual([13, 14, 15], ar_col1)

        queue = rq.flush()
        self.assertEqual(7, len(queue))

        assert_deltas(RootContainer.MAIN, (), 1)
        assert_deltas(RootContainer.SIDEBAR, (0, 0, 1), 4)
Example #12
0
    def test_dont_replace_block(self, other_msg: ForwardMsg):
        """add_block deltas should never be replaced/composed because they can
        have dependent deltas later in the queue."""
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        ADD_BLOCK_MSG.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)

        other_msg.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)

        # Delta messages should not replace `add_block` deltas with the
        # same delta_path.
        rq.enqueue(ADD_BLOCK_MSG)
        rq.enqueue(other_msg)
        queue = rq.flush()
        self.assertEqual(len(queue), 2)
        self.assertEqual(queue[0], ADD_BLOCK_MSG)
        self.assertEqual(queue[1], other_msg)
Example #13
0
    def test_cached_st_function_warning(self, _, cache_decorator, call_stack):
        """Ensure we properly warn when st.foo functions are called
        inside a cached function.
        """
        forward_msg_queue = ForwardMsgQueue()
        orig_report_ctx = get_script_run_ctx()
        add_script_run_ctx(
            threading.current_thread(),
            ScriptRunContext(
                session_id="test session id",
                enqueue=forward_msg_queue.enqueue,
                query_string="",
                session_state=SessionState(),
                uploaded_file_mgr=None,
            ),
        )
        with patch.object(call_stack,
                          "_show_cached_st_function_warning") as warning:
            st.text("foo")
            warning.assert_not_called()

            @cache_decorator
            def cached_func():
                st.text("Inside cached func")

            cached_func()
            warning.assert_called_once()

            warning.reset_mock()

            # Make sure everything got reset properly
            st.text("foo")
            warning.assert_not_called()

            # Test warning suppression
            @cache_decorator(suppress_st_warning=True)
            def suppressed_cached_func():
                st.text("No warnings here!")

            suppressed_cached_func()

            warning.assert_not_called()

            # Test nested st.cache functions
            @cache_decorator
            def outer():
                @cache_decorator
                def inner():
                    st.text("Inside nested cached func")

                return inner()

            outer()
            warning.assert_called_once()

            warning.reset_mock()

            # Test st.cache functions that raise errors
            with self.assertRaises(RuntimeError):

                @cache_decorator
                def cached_raise_error():
                    st.text("About to throw")
                    raise RuntimeError("avast!")

                cached_raise_error()

            warning.assert_called_once()
            warning.reset_mock()

            # Make sure everything got reset properly
            st.text("foo")
            warning.assert_not_called()

            # Test st.cache functions with widgets
            @cache_decorator
            def cached_widget():
                st.button("Press me!")

            cached_widget()

            warning.assert_called_once()
            warning.reset_mock()

            # Make sure everything got reset properly
            st.text("foo")
            warning.assert_not_called()

            add_script_run_ctx(threading.current_thread(), orig_report_ctx)
Example #14
0
    def test_add_rows_rerun(self):
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        # Simulate rerun
        for i in range(2):
            TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
                RootContainer.MAIN, (), 0)
            rq.enqueue(TEXT_DELTA_MSG1)

            DF_DELTA_MSG.metadata.delta_path[:] = make_delta_path(
                RootContainer.MAIN, (), 1)
            rq.enqueue(DF_DELTA_MSG)

            ADD_ROWS_MSG.metadata.delta_path[:] = make_delta_path(
                RootContainer.MAIN, (), 1)
            rq.enqueue(ADD_ROWS_MSG)

        queue = rq.flush()
        self.assertEqual(5, len(queue))

        # Text delta
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 0),
                         queue[1].metadata.delta_path)
        self.assertEqual("text1", queue[1].delta.new_element.text.body)

        # Dataframe delta
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 1),
                         queue[2].metadata.delta_path)
        col0 = queue[2].delta.new_element.data_frame.data.cols[0].int64s.data
        col1 = queue[2].delta.new_element.data_frame.data.cols[1].int64s.data
        self.assertEqual([0, 1, 2], col0)
        self.assertEqual([10, 11, 12], col1)

        # First add_rows delta
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 1),
                         queue[3].metadata.delta_path)
        ar_col0 = queue[3].delta.add_rows.data.data.cols[0].int64s.data
        ar_col1 = queue[3].delta.add_rows.data.data.cols[1].int64s.data
        self.assertEqual([3, 4, 5], ar_col0)
        self.assertEqual([13, 14, 15], ar_col1)

        # Second add_rows delta
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 1),
                         queue[4].metadata.delta_path)
        ar_col0 = queue[4].delta.add_rows.data.data.cols[0].int64s.data
        ar_col1 = queue[4].delta.add_rows.data.data.cols[1].int64s.data
        self.assertEqual([3, 4, 5], ar_col0)
        self.assertEqual([13, 14, 15], ar_col1)
Example #15
0
class TestScriptRunner(ScriptRunner):
    """Subclasses ScriptRunner to provide some testing features."""
    def __init__(self, script_name):
        """Initializes the ScriptRunner for the given script_name"""
        # DeltaGenerator deltas will be enqueued into self.forward_msg_queue.
        self.forward_msg_queue = ForwardMsgQueue()

        self.script_request_queue = ScriptRequestQueue()
        main_script_path = os.path.join(os.path.dirname(__file__), "test_data",
                                        script_name)

        super(TestScriptRunner, self).__init__(
            session_id="test session id",
            session_data=SessionData(main_script_path, "test command line"),
            enqueue_forward_msg=self.forward_msg_queue.enqueue,
            client_state=ClientState(),
            session_state=SessionState(),
            request_queue=self.script_request_queue,
            uploaded_file_mgr=UploadedFileManager(),
        )

        # Accumulates uncaught exceptions thrown by our run thread.
        self.script_thread_exceptions = []

        # Accumulates all ScriptRunnerEvents emitted by us.
        self.events: List[ScriptRunnerEvent] = []
        self.event_data: List[Any] = []

        def record_event(sender: Optional[ScriptRunner],
                         event: ScriptRunnerEvent, **kwargs):
            # Assert that we're not getting unexpected `sender` params
            # from ScriptRunner.on_event
            assert (sender is None
                    or sender == self), "Unexpected ScriptRunnerEvent sender!"
            self.events.append(event)
            self.event_data.append(kwargs)

        self.on_event.connect(record_event, weak=False)

    def enqueue_rerun(self, argv=None, widget_states=None, query_string=""):
        self.script_request_queue.enqueue(
            ScriptRequest.RERUN,
            RerunData(widget_states=widget_states, query_string=query_string),
        )

    def enqueue_stop(self):
        self.script_request_queue.enqueue(ScriptRequest.STOP)

    def enqueue_shutdown(self):
        self.script_request_queue.enqueue(ScriptRequest.SHUTDOWN)

    def _run_script_thread(self):
        try:
            super()._run_script_thread()
        except BaseException as e:
            self.script_thread_exceptions.append(e)

    def _run_script(self, rerun_data):
        self.forward_msg_queue.clear()
        super()._run_script(rerun_data)

    def join(self):
        """Joins the run thread, if it was started"""
        if self._script_thread is not None:
            self._script_thread.join()

    def clear_deltas(self):
        """Clear all delta messages from our ForwardMsgQueue"""
        self.forward_msg_queue.clear()

    def deltas(self) -> List[Delta]:
        """Return the delta messages in our ForwardMsgQueue"""
        return [
            msg.delta for msg in self.forward_msg_queue._queue
            if msg.HasField("delta")
        ]

    def elements(self) -> List[Element]:
        """Return the delta.new_element messages in our ForwardMsgQueue."""
        return [delta.new_element for delta in self.deltas()]

    def text_deltas(self) -> List[str]:
        """Return the string contents of text deltas in our ForwardMsgQueue"""
        return [
            element.text.body for element in self.elements()
            if element.WhichOneof("type") == "text"
        ]

    def get_widget_id(self, widget_type, label):
        """Returns the id of the widget with the specified type and label"""
        for delta in self.deltas():
            new_element = getattr(delta, "new_element", None)
            widget = getattr(new_element, widget_type, None)
            widget_label = getattr(widget, "label", None)
            if widget_label == label:
                return widget.id
        return None
Example #16
0
    def test_add_rows_rerun(self):
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        # Simulate rerun
        for i in range(2):
            TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
                RootContainer.MAIN, (), 0)
            rq.enqueue(TEXT_DELTA_MSG1)

            DF_DELTA_MSG.metadata.delta_path[:] = make_delta_path(
                RootContainer.MAIN, (), 1)
            rq.enqueue(DF_DELTA_MSG)

            ADD_ROWS_MSG.metadata.delta_path[:] = make_delta_path(
                RootContainer.MAIN, (), 1)
            rq.enqueue(ADD_ROWS_MSG)

        queue = rq.flush()
        self.assertEqual(len(queue), 3)
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 0),
                         queue[1].metadata.delta_path)
        self.assertEqual(queue[1].delta.new_element.text.body, "text1")
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 1),
                         queue[2].metadata.delta_path)
        col0 = queue[2].delta.new_element.data_frame.data.cols[0].int64s.data
        col1 = queue[2].delta.new_element.data_frame.data.cols[1].int64s.data
        self.assertEqual(col0, [0, 1, 2, 3, 4, 5])
        self.assertEqual(col1, [10, 11, 12, 13, 14, 15])
Example #17
0
    def test_simple_add_rows(self):
        """'add_rows' messages should behave as expected."""
        rq = ForwardMsgQueue()
        self.assertTrue(rq.is_empty())

        rq.enqueue(NEW_SESSION_MSG)

        TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 0)
        rq.enqueue(TEXT_DELTA_MSG1)

        DF_DELTA_MSG.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 1)
        rq.enqueue(DF_DELTA_MSG)

        ADD_ROWS_MSG.metadata.delta_path[:] = make_delta_path(
            RootContainer.MAIN, (), 1)
        rq.enqueue(ADD_ROWS_MSG)

        queue = rq.flush()
        self.assertEqual(4, len(queue))

        # Text delta
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 0),
                         queue[1].metadata.delta_path)
        self.assertEqual("text1", queue[1].delta.new_element.text.body)

        # Dataframe delta
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 1),
                         queue[2].metadata.delta_path)
        df_col0 = queue[2].delta.new_element.data_frame.data.cols[
            0].int64s.data
        df_col1 = queue[2].delta.new_element.data_frame.data.cols[
            1].int64s.data
        self.assertEqual([0, 1, 2], df_col0)
        self.assertEqual([10, 11, 12], df_col1)

        # AddRows delta
        self.assertEqual(make_delta_path(RootContainer.MAIN, (), 1),
                         queue[3].metadata.delta_path)
        ar_col0 = queue[3].delta.add_rows.data.data.cols[0].int64s.data
        ar_col1 = queue[3].delta.add_rows.data.data.cols[1].int64s.data
        self.assertEqual([3, 4, 5], ar_col0)
        self.assertEqual([13, 14, 15], ar_col1)