def test_unhashable_type(self): @st.experimental_memo def unhashable_type_func(lock: threading.Lock): return str(lock) with self.assertRaises(UnhashableParamError) as cm: unhashable_type_func(threading.Lock()) ep = ExceptionProto() exception.marshall(ep, cm.exception) self.assertEqual(ep.type, "UnhashableParamError") expected_message = """ Cannot hash argument 'lock' (of type `_thread.lock`) in 'unhashable_type_func'. To address this, you can tell Streamlit not to hash this argument by adding a leading underscore to the argument's name in the function signature: ``` @st.experimental_memo def unhashable_type_func(_lock, ...): ... ``` """ self.assertEqual(testutil.normalize_md(expected_message), testutil.normalize_md(ep.message)) # Stack trace doesn't show in test :( # self.assertNotEqual(len(ep.stack_trace), 0) self.assertEqual(ep.message_is_markdown, True) self.assertEqual(ep.is_warning, False)
def serialize_forward_msg(msg): """Serialize a ForwardMsg to send to a client. If the message is too large, it will be converted to an exception message instead. Parameters ---------- msg : ForwardMsg The message to serialize Returns ------- str The serialized byte string to send """ populate_hash_if_needed(msg) msg_str = msg.SerializeToString() if len(msg_str) > MESSAGE_SIZE_LIMIT: import streamlit.elements.exception as exception error = RuntimeError( f"Data of size {len(msg_str)/1e6:.1f}MB exceeds write limit of {MESSAGE_SIZE_LIMIT/1e6}MB" ) # Overwrite the offending ForwardMsg.delta with an error to display. # This assumes that the size limit wasn't exceeded due to metadata. exception.marshall(msg.delta.new_element.exception, error) msg_str = msg.SerializeToString() return msg_str
def handle_backmsg_exception(self, e: BaseException) -> None: """Handle an Exception raised while processing a BackMsg from the browser.""" # This does a few things: # 1) Clears the current app in the browser. # 2) Marks the current app as "stopped" in the browser. # 3) HACK: Resets any script params that may have been broken (e.g. the # command-line when rerunning with wrong argv[0]) self._on_scriptrunner_event(None, ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS) self._on_scriptrunner_event(None, ScriptRunnerEvent.SCRIPT_STARTED) self._on_scriptrunner_event(None, ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS) msg = ForwardMsg() exception_utils.marshall(msg.delta.new_element.exception, e) self.enqueue(msg)
def test_unhashable_type(self): @st.cache def unhashable_type_func(): return threading.Lock() with self.assertRaises(hashing.UnhashableTypeError) as cm: unhashable_type_func() ep = ExceptionProto() exception.marshall(ep, cm.exception) self.assertEqual(ep.type, "UnhashableTypeError") self.assertTrue( normalize_md(ep.message).startswith( normalize_md( """ Cannot hash object of type `_thread.lock`, found in the return value of `unhashable_type_func()`. While caching the return value of `unhashable_type_func()`, Streamlit encountered an object of type `_thread.lock`, which it does not know how to hash. To address this, please try helping Streamlit understand how to hash that type by passing the `hash_funcs` argument into `@st.cache`. For example: ``` @st.cache(hash_funcs={_thread.lock: my_hash_func}) def my_func(...): ... ``` If you don't know where the object of type `_thread.lock` is coming from, try looking at the hash chain below for an object that you do recognize, then pass that to `hash_funcs` instead: ``` Object of type _thread.lock: """ ) ) ) # Stack trace doesn't show in test :( # self.assertNotEqual(len(ep.stack_trace), 0) self.assertEqual(ep.message_is_markdown, True) self.assertEqual(ep.is_warning, False)
def test_user_hash_error(self): class MyObj(object): pass def bad_hash_func(x): x += 10 # Throws a TypeError since x has type MyObj. return x @st.cache(hash_funcs={MyObj: bad_hash_func}) def user_hash_error_func(x): pass with self.assertRaises(hashing.UserHashError) as cm: my_obj = MyObj() user_hash_error_func(my_obj) ep = ExceptionProto() exception.marshall(ep, cm.exception) self.assertEqual(ep.type, "TypeError") self.assertTrue( normalize_md(ep.message).startswith( normalize_md( """ unsupported operand type(s) for +=: 'MyObj' and 'int' This error is likely due to a bug in `bad_hash_func()`, which is a user-defined hash function that was passed into the `@st.cache` decorator of `user_hash_error_func()`. `bad_hash_func()` failed when hashing an object of type `caching_test.CacheErrorsTest.test_user_hash_error.<locals>.MyObj`. If you don't know where that object is coming from, try looking at the hash chain below for an object that you do recognize, then pass that to `hash_funcs` instead: ``` Object of type caching_test.CacheErrorsTest.test_user_hash_error.<locals>.MyObj: <caching_test.CacheErrorsTest.test_user_hash_error.<locals>.MyObj object at """ ) ) ) # Stack trace doesn't show in test :( # self.assertNotEqual(len(ep.stack_trace), 0) self.assertEqual(ep.message_is_markdown, True) self.assertEqual(ep.is_warning, False)
def test_uncaught_app_exception(self): err = None try: st.format("http://not_an_image.png", width=-1) except Exception as e: err = UncaughtAppException(e) self.assertIsNotNone(err) # Marshall it. proto = ExceptionProto() exception.marshall(proto, err) for line in proto.stack_trace: # assert message that could contain secret information in the stack trace assert "module 'streamlit' has no attribute 'format'" not in line assert proto.message == _GENERIC_UNCAUGHT_EXCEPTION_TEXT assert proto.type == "AttributeError"
def serialize_forward_msg(msg: ForwardMsg) -> bytes: """Serialize a ForwardMsg to send to a client. If the message is too large, it will be converted to an exception message instead. """ populate_hash_if_needed(msg) msg_str = msg.SerializeToString() if len(msg_str) > get_max_message_size_bytes(): import streamlit.elements.exception as exception # Overwrite the offending ForwardMsg.delta with an error to display. # This assumes that the size limit wasn't exceeded due to metadata. exception.marshall(msg.delta.new_element.exception, MessageSizeError(msg_str)) msg_str = msg.SerializeToString() return msg_str
def test_strip_streamlit_stack_entries(self): """Test that StreamlitAPIExceptions don't include Streamlit entries in the stack trace. """ # Create a StreamlitAPIException. err = None try: st.image("http://not_an_image.png", width=-1) except StreamlitAPIException as e: err = e self.assertIsNotNone(err) # Marshall it. proto = ExceptionProto() exception.marshall(proto, err) # The streamlit package should not appear in any stack entry. streamlit_dir = os.path.dirname(st.__file__) streamlit_dir = os.path.join(os.path.realpath(streamlit_dir), "") for line in proto.stack_trace: self.assertNotIn(streamlit_dir, line, "Streamlit stack entry not stripped")
def enqueue_exception(self, e): """Enqueue an Exception message. Parameters ---------- e : BaseException """ # This does a few things: # 1) Clears the current report in the browser. # 2) Marks the current report as "stopped" in the browser. # 3) HACK: Resets any script params that may have been broken (e.g. the # command-line when rerunning with wrong argv[0]) self._on_scriptrunner_event( ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS) self._on_scriptrunner_event(ScriptRunnerEvent.SCRIPT_STARTED) self._on_scriptrunner_event( ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS) msg = ForwardMsg() exception.marshall(msg.delta.new_element.exception, e) self.enqueue(msg)
def test_markdown_flag(self): """Test that ExceptionProtos for StreamlitAPIExceptions (and subclasses) have the "message_is_markdown" flag set. """ proto = ExceptionProto() exception.marshall(proto, RuntimeError("oh no!")) self.assertFalse(proto.message_is_markdown) proto = ExceptionProto() exception.marshall(proto, StreamlitAPIException("oh no!")) self.assertTrue(proto.message_is_markdown) proto = ExceptionProto() exception.marshall(proto, errors.DuplicateWidgetID("oh no!")) self.assertTrue(proto.message_is_markdown)
def _on_scriptrunner_event(self, event, exception=None, client_state=None): """Called when our ScriptRunner emits an event. This is *not* called on the main thread. Parameters ---------- event : ScriptRunnerEvent exception : BaseException | None An exception thrown during compilation. Set only for the SCRIPT_STOPPED_WITH_COMPILE_ERROR event. client_state : streamlit.proto.ClientState_pb2.ClientState | None The ScriptRunner's final ClientState. Set only for the SHUTDOWN event. """ LOGGER.debug("OnScriptRunnerEvent: %s", event) prev_state = self._state if event == ScriptRunnerEvent.SCRIPT_STARTED: if self._state != ReportSessionState.SHUTDOWN_REQUESTED: self._state = ReportSessionState.REPORT_IS_RUNNING if config.get_option("server.liveSave"): # Enqueue into the IOLoop so it runs without blocking AND runs # on the main thread. self._ioloop.spawn_callback(self._save_running_report) self._clear_queue() self._enqueue_new_report_message() elif (event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS or event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR): if self._state != ReportSessionState.SHUTDOWN_REQUESTED: self._state = ReportSessionState.REPORT_NOT_RUNNING script_succeeded = event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS self._enqueue_report_finished_message( ForwardMsg.FINISHED_SUCCESSFULLY if script_succeeded else ForwardMsg.FINISHED_WITH_COMPILE_ERROR) if config.get_option("server.liveSave"): # Enqueue into the IOLoop so it runs without blocking AND runs # on the main thread. self._ioloop.spawn_callback(self._save_final_report_and_quit) if script_succeeded: # When a script completes successfully, we update our # LocalSourcesWatcher to account for any source code changes # that change which modules should be watched. (This is run on # the main thread, because LocalSourcesWatcher is not # thread safe.) self._ioloop.spawn_callback( self._local_sources_watcher.update_watched_modules) else: # When a script fails to compile, we send along the exception. import streamlit.elements.exception as exception_utils msg = ForwardMsg() exception_utils.marshall( msg.session_event.script_compilation_exception, exception) self.enqueue(msg) elif event == ScriptRunnerEvent.SHUTDOWN: # When ScriptRunner shuts down, update our local reference to it, # and check to see if we need to spawn a new one. (This is run on # the main thread.) if self._state == ReportSessionState.SHUTDOWN_REQUESTED: # Only clear media files if the script is done running AND the # report session is actually shutting down. media_file_manager.clear_session_files(self.id) def on_shutdown(): self._client_state = client_state self._scriptrunner = None # Because a new ScriptEvent could have been enqueued while the # scriptrunner was shutting down, we check to see if we should # create a new one. (Otherwise, a newly-enqueued ScriptEvent # won't be processed until another event is enqueued.) self._maybe_create_scriptrunner() self._ioloop.spawn_callback(on_shutdown) # Send a message if our run state changed report_was_running = prev_state == ReportSessionState.REPORT_IS_RUNNING report_is_running = self._state == ReportSessionState.REPORT_IS_RUNNING if report_is_running != report_was_running: self._enqueue_session_state_changed_message()
def _on_scriptrunner_event( self, event: ScriptRunnerEvent, exception: Optional[BaseException] = None, client_state: Optional[ClientState] = None, ) -> None: """Called when our ScriptRunner emits an event. This is *not* called on the main thread. Parameters ---------- event : ScriptRunnerEvent exception : BaseException | None An exception thrown during compilation. Set only for the SCRIPT_STOPPED_WITH_COMPILE_ERROR event. client_state : streamlit.proto.ClientState_pb2.ClientState | None The ScriptRunner's final ClientState. Set only for the SHUTDOWN event. """ LOGGER.debug("OnScriptRunnerEvent: %s", event) prev_state = self._state if event == ScriptRunnerEvent.SCRIPT_STARTED: if self._state != AppSessionState.SHUTDOWN_REQUESTED: self._state = AppSessionState.APP_IS_RUNNING self._clear_queue() self._enqueue_new_session_message() elif (event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS or event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR): if self._state != AppSessionState.SHUTDOWN_REQUESTED: self._state = AppSessionState.APP_NOT_RUNNING script_succeeded = event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS self._enqueue_script_finished_message( ForwardMsg.FINISHED_SUCCESSFULLY if script_succeeded else ForwardMsg.FINISHED_WITH_COMPILE_ERROR) if script_succeeded: # When a script completes successfully, we update our # LocalSourcesWatcher to account for any source code changes # that change which modules should be watched. (This is run on # the main thread, because LocalSourcesWatcher is not # thread safe.) self._ioloop.spawn_callback( self._local_sources_watcher.update_watched_modules) else: msg = ForwardMsg() exception_utils.marshall( msg.session_event.script_compilation_exception, exception) self.enqueue(msg) elif event == ScriptRunnerEvent.SHUTDOWN: # When ScriptRunner shuts down, update our local reference to it, # and check to see if we need to spawn a new one. (This is run on # the main thread.) assert ( client_state is not None), "client_state must be set for the SHUTDOWN event" if self._state == AppSessionState.SHUTDOWN_REQUESTED: # Only clear media files if the script is done running AND the # session is actually shutting down. in_memory_file_manager.clear_session_files(self.id) def on_shutdown(): # We assert above that this is non-null self._client_state = cast(ClientState, client_state) self._scriptrunner = None # Because a new ScriptEvent could have been enqueued while the # scriptrunner was shutting down, we check to see if we should # create a new one. (Otherwise, a newly-enqueued ScriptEvent # won't be processed until another event is enqueued.) self._maybe_create_scriptrunner() self._ioloop.spawn_callback(on_shutdown) # Send a message if our run state changed app_was_running = prev_state == AppSessionState.APP_IS_RUNNING app_is_running = self._state == AppSessionState.APP_IS_RUNNING if app_is_running != app_was_running: self._enqueue_session_state_changed_message()
def _create_exception_message(self, e: BaseException) -> ForwardMsg: """Create and return an Exception ForwardMsg.""" msg = ForwardMsg() exception_utils.marshall(msg.delta.new_element.exception, e) return msg
def _handle_scriptrunner_event_on_main_thread( self, sender: Optional[ScriptRunner], event: ScriptRunnerEvent, forward_msg: Optional[ForwardMsg] = None, exception: Optional[BaseException] = None, client_state: Optional[ClientState] = None, ) -> None: """Handle a ScriptRunner event. This function must only be called on the main thread. Parameters ---------- sender : ScriptRunner | None The ScriptRunner that emitted the event. (This may be set to None when called from `handle_backmsg_exception`, if no ScriptRunner was active when the backmsg exception was raised.) event : ScriptRunnerEvent The event type. forward_msg : ForwardMsg | None The ForwardMsg to send to the frontend. Set only for the ENQUEUE_FORWARD_MSG event. exception : BaseException | None An exception thrown during compilation. Set only for the SCRIPT_STOPPED_WITH_COMPILE_ERROR event. client_state : streamlit.proto.ClientState_pb2.ClientState | None The ScriptRunner's final ClientState. Set only for the SHUTDOWN event. """ assert (threading.main_thread() == threading.current_thread() ), "This function must only be called on the main thread" if sender is not self._scriptrunner: # This event was sent by a non-current ScriptRunner; ignore it. # This can happen after sppinng up a new ScriptRunner (to handle a # rerun request, for example) while another ScriptRunner is still # shutting down. The shutting-down ScriptRunner may still # emit events. LOGGER.debug("Ignoring event from non-current ScriptRunner: %s", event) return prev_state = self._state if event == ScriptRunnerEvent.SCRIPT_STARTED: if self._state != AppSessionState.SHUTDOWN_REQUESTED: self._state = AppSessionState.APP_IS_RUNNING self._clear_queue() self._enqueue_forward_msg(self._create_new_session_message()) elif (event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS or event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR): if self._state != AppSessionState.SHUTDOWN_REQUESTED: self._state = AppSessionState.APP_NOT_RUNNING script_succeeded = event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS script_finished_msg = self._create_script_finished_message( ForwardMsg.FINISHED_SUCCESSFULLY if script_succeeded else ForwardMsg.FINISHED_WITH_COMPILE_ERROR) self._enqueue_forward_msg(script_finished_msg) if script_succeeded: # The script completed successfully: update our # LocalSourcesWatcher to account for any source code changes # that change which modules should be watched. self._local_sources_watcher.update_watched_modules() else: # The script didn't complete successfully: send the exception # to the frontend. assert ( exception is not None ), "exception must be set for the SCRIPT_STOPPED_WITH_COMPILE_ERROR event" msg = ForwardMsg() exception_utils.marshall( msg.session_event.script_compilation_exception, exception) self._enqueue_forward_msg(msg) elif event == ScriptRunnerEvent.SHUTDOWN: assert ( client_state is not None), "client_state must be set for the SHUTDOWN event" if self._state == AppSessionState.SHUTDOWN_REQUESTED: # Only clear media files if the script is done running AND the # session is actually shutting down. in_memory_file_manager.clear_session_files(self.id) self._client_state = client_state self._scriptrunner = None elif event == ScriptRunnerEvent.ENQUEUE_FORWARD_MSG: assert ( forward_msg is not None), "null forward_msg in ENQUEUE_FORWARD_MSG event" self._enqueue_forward_msg(forward_msg) # Send a message if our run state changed app_was_running = prev_state == AppSessionState.APP_IS_RUNNING app_is_running = self._state == AppSessionState.APP_IS_RUNNING if app_is_running != app_was_running: self._enqueue_forward_msg( self._create_session_state_changed_message())