def test_calls_widget_callbacks_error(self, patched_call_callbacks, patched_st_exception): """If an exception is raised from a callback function, it should result in a call to `streamlit.exception`. """ patched_call_callbacks.side_effect = RuntimeError("Random Error") scriptrunner = TestScriptRunner("widgets_script.py") scriptrunner.request_rerun(RerunData()) scriptrunner.start() # Default widget values require_widgets_deltas([scriptrunner]) self._assert_text_deltas( scriptrunner, ["False", "ahoy!", "0", "False", "loop_forever"]) patched_call_callbacks.assert_not_called() # Update widgets states = WidgetStates() w1_id = scriptrunner.get_widget_id("checkbox", "checkbox") _create_widget(w1_id, states).bool_value = True w2_id = scriptrunner.get_widget_id("text_area", "text_area") _create_widget(w2_id, states).string_value = "matey!" w3_id = scriptrunner.get_widget_id("radio", "radio") _create_widget(w3_id, states).int_value = 2 w4_id = scriptrunner.get_widget_id("button", "button") _create_widget(w4_id, states).trigger_value = True # Explicitly clear deltas before re-running, to prevent a race # condition. (The ScriptRunner will clear the deltas when it # starts the re-run, but if that doesn't happen before # require_widgets_deltas() starts polling the ScriptRunner's deltas, # it will see stale deltas from the last run.) scriptrunner.clear_forward_msgs() scriptrunner.request_rerun(RerunData(widget_states=states)) scriptrunner.join() patched_call_callbacks.assert_called_once() self._assert_control_events( scriptrunner, [ ScriptRunnerEvent.SCRIPT_STARTED, ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS, ScriptRunnerEvent.SCRIPT_STARTED, # We use the SCRIPT_STOPPED_WITH_SUCCESS event even if the # script runs into an error during execution. The user is # informed of the error by an `st.exception` box that we check # for below. ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS, ScriptRunnerEvent.SHUTDOWN, ], ) patched_st_exception.assert_called_once()
def off_test_multiple_scriptrunners(self): """Tests that multiple scriptrunners can run simultaneously.""" # This scriptrunner will run before the other 3. It's used to retrieve # the widget id before initializing deltas on other runners. scriptrunner = TestScriptRunner("widgets_script.py") scriptrunner.enqueue_rerun() scriptrunner.start() # Get the widget ID of a radio button and shut down the first runner. require_widgets_deltas([scriptrunner]) radio_widget_id = scriptrunner.get_widget_id("radio", "radio") scriptrunner.enqueue_shutdown() scriptrunner.join() self._assert_no_exceptions(scriptrunner) # Build several runners. Each will set a different int value for # its radio button. runners = [] for ii in range(3): runner = TestScriptRunner("widgets_script.py") runners.append(runner) states = WidgetStates() _create_widget(radio_widget_id, states).int_value = ii runner.enqueue_rerun(widget_state=states) # Start the runners and wait a beat. for runner in runners: runner.start() require_widgets_deltas(runners) # Ensure that each runner's radio value is as expected. for ii, runner in enumerate(runners): self._assert_text_deltas( runner, ["False", "ahoy!", "%s" % ii, "False", "loop_forever"]) runner.enqueue_shutdown() time.sleep(0.1) # Shut 'em all down! for runner in runners: runner.join() for runner in runners: self._assert_no_exceptions(runner) self._assert_events( runner, [ ScriptRunnerEvent.SCRIPT_STARTED, ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS, ScriptRunnerEvent.SHUTDOWN, ], )
def test_get_keyed_widget_values(self): states = WidgetStates() _create_widget("trigger", states).trigger_value = True _create_widget("trigger2", states).trigger_value = True session_state = SessionState() session_state.set_widgets_from_proto(states) session_state._set_widget_metadata( create_metadata("trigger", "trigger_value", True)) session_state._set_widget_metadata( create_metadata("trigger2", "trigger_value")) self.assertEqual(dict(session_state.values()), {"trigger": True})
def test_marshall_excludes_widgets_without_state(self): widget_states = WidgetStates() _create_widget("trigger", widget_states).trigger_value = True session_state = SessionState() session_state.set_widgets_from_proto(widget_states) session_state._set_widget_metadata( WidgetMetadata("other_widget", lambda x, s: x, None, "trigger_value", True)) widgets = session_state.get_widget_states() self.assertEqual(len(widgets), 1) self.assertEqual(widgets[0].id, "trigger")
def test_widgets(self): """Tests that widget values behave as expected.""" scriptrunner = TestScriptRunner("widgets_script.py") scriptrunner.enqueue_rerun() scriptrunner.start() # Default widget values require_widgets_deltas([scriptrunner]) self._assert_text_deltas( scriptrunner, ["False", "ahoy!", "0", "False", "loop_forever"] ) # Update widgets states = WidgetStates() w1_id = scriptrunner.get_widget_id("checkbox", "checkbox") _create_widget(w1_id, states).bool_value = True w2_id = scriptrunner.get_widget_id("text_area", "text_area") _create_widget(w2_id, states).string_value = "matey!" w3_id = scriptrunner.get_widget_id("radio", "radio") _create_widget(w3_id, states).int_value = 2 w4_id = scriptrunner.get_widget_id("button", "button") _create_widget(w4_id, states).trigger_value = True # Explicitly clear deltas before re-running, to prevent a race # condition. (The ScriptRunner will clear the deltas when it # starts the re-run, but if that doesn't happen before # require_widgets_deltas() starts polling the ScriptRunner's deltas, # it will see stale deltas from the last run.) scriptrunner.clear_deltas() scriptrunner.enqueue_rerun(widget_states=states) require_widgets_deltas([scriptrunner]) self._assert_text_deltas( scriptrunner, ["True", "matey!", "2", "True", "loop_forever"] ) # Rerun with previous values. Our button should be reset; # everything else should be the same. scriptrunner.clear_deltas() scriptrunner.enqueue_rerun() require_widgets_deltas([scriptrunner]) self._assert_text_deltas( scriptrunner, ["True", "matey!", "2", "False", "loop_forever"] ) scriptrunner.enqueue_shutdown() scriptrunner.join() self._assert_no_exceptions(scriptrunner)
def test_reset_triggers(self): states = WidgetStates() widgets = Widgets() _create_widget("trigger", states).trigger_value = True _create_widget("int", states).int_value = 123 widgets.set_state(states) self.assertEqual(True, widgets.get_widget_value("trigger")) self.assertEqual(123, widgets.get_widget_value("int")) widgets.reset_triggers() self.assertEqual(None, widgets.get_widget_value("trigger")) self.assertEqual(123, widgets.get_widget_value("int"))
def test_remove_nonexistent_elements(self): """Tests that nonexistent elements are removed from widget cache after script run.""" widget_id = "nonexistent_widget_id" # Run script, sending in a WidgetStates containing our fake widget ID. scriptrunner = TestScriptRunner("good_script.py") states = WidgetStates() _create_widget(widget_id, states).string_value = "streamlit" scriptrunner.enqueue_rerun(widget_states=states) scriptrunner.start() # At this point, scriptrunner should have finished running, detected # that our widget_id wasn't in the list of widgets found this run, and # culled it. Ensure widget cache no longer holds our widget ID. self.assertIsNone(scriptrunner._widgets.get_widget_value(widget_id))
def test_values(self): states = WidgetStates() _create_widget("trigger", states).trigger_value = True _create_widget("bool", states).bool_value = True _create_widget("float", states).double_value = 0.5 _create_widget("int", states).int_value = 123 _create_widget("string", states).string_value = "howdy!" widgets = Widgets() widgets.set_state(states) self.assertEqual(True, widgets.get_widget_value("trigger")) self.assertEqual(True, widgets.get_widget_value("bool")) self.assertAlmostEqual(0.5, widgets.get_widget_value("float")) self.assertEqual(123, widgets.get_widget_value("int")) self.assertEqual("howdy!", widgets.get_widget_value("string"))
def test_reset_triggers(self): states = WidgetStates() session_state = SessionState() _create_widget("trigger", states).trigger_value = True _create_widget("int", states).int_value = 123 session_state.set_widgets_from_proto(states) session_state._set_widget_metadata( WidgetMetadata("trigger", lambda x, s: x, None, "trigger_value")) session_state._set_widget_metadata( WidgetMetadata("int", lambda x, s: x, None, "int_value")) self.assertTrue(session_state["trigger"]) self.assertEqual(123, session_state["int"]) session_state._reset_triggers() self.assertFalse(session_state["trigger"]) self.assertEqual(123, session_state["int"])
def coalesce_widget_states(old_states, new_states): """Coalesces an older WidgetStates into a newer one, and returns a new WidgetState containing the result. For most widget values, we just take the latest version. However, any trigger_values (the state set by buttons) that are True in the older WidgetStates will be set to True in the coalesced result, so that button presses don't go missing. Parameters ---------- old_states : WidgetStates The older WidgetStates protobuf new_states : WidgetStates The newer WidgetStates protobuf Returns ------- WidgetStates The resulting coalesced protobuf """ states_by_id = {} for new_state in new_states.widgets: states_by_id[new_state.id] = new_state for old_state in old_states.widgets: if old_state.WhichOneof("value") == "trigger_value" and old_state.trigger_value: # Ensure the corresponding new_state is also a trigger; # otherwise, a widget that was previously a button but no longer is # could get a bad value. new_state = states_by_id.get(old_state.id) if new_state and new_state.WhichOneof("value") == "trigger_value": states_by_id[old_state.id] = old_state coalesced = WidgetStates() coalesced.widgets.extend(states_by_id.values()) return coalesced
def test_calls_widget_callbacks(self, patched_call_callbacks): """Before a script is rerun, we call callbacks for any widgets whose value has changed. """ scriptrunner = TestScriptRunner("widgets_script.py") scriptrunner.request_rerun(RerunData()) scriptrunner.start() # Default widget values require_widgets_deltas([scriptrunner]) self._assert_text_deltas( scriptrunner, ["False", "ahoy!", "0", "False", "loop_forever"]) patched_call_callbacks.assert_not_called() # Update widgets states = WidgetStates() w1_id = scriptrunner.get_widget_id("checkbox", "checkbox") _create_widget(w1_id, states).bool_value = True w2_id = scriptrunner.get_widget_id("text_area", "text_area") _create_widget(w2_id, states).string_value = "matey!" w3_id = scriptrunner.get_widget_id("radio", "radio") _create_widget(w3_id, states).int_value = 2 w4_id = scriptrunner.get_widget_id("button", "button") _create_widget(w4_id, states).trigger_value = True # Explicitly clear deltas before re-running, to prevent a race # condition. (The ScriptRunner will clear the deltas when it # starts the re-run, but if that doesn't happen before # require_widgets_deltas() starts polling the ScriptRunner's deltas, # it will see stale deltas from the last run.) scriptrunner.clear_forward_msgs() scriptrunner.request_rerun(RerunData(widget_states=states)) require_widgets_deltas([scriptrunner]) patched_call_callbacks.assert_called_once() self._assert_text_deltas( scriptrunner, ["True", "matey!", "2", "True", "loop_forever"]) scriptrunner.request_stop() scriptrunner.join()
def test_rerun_coalesce_none_and_widgets(self): """Coalesce a null-WidgetStates rerun request with a non-null-WidgetStates request. """ reqs = ScriptRequests() # Request a rerun with null WidgetStates. success = reqs.request_rerun(RerunData(widget_states=None)) self.assertTrue(success) # Request a rerun with non-null WidgetStates. states = WidgetStates() _create_widget("trigger", states).trigger_value = True _create_widget("int", states).int_value = 123 success = reqs.request_rerun(RerunData(widget_states=states)) self.assertTrue(success) # The null WidgetStates request will be overwritten. result_states = reqs._rerun_data.widget_states self.assertEqual(True, _get_widget("trigger", result_states).trigger_value) self.assertEqual(123, _get_widget("int", result_states).int_value)
def test_get(self): states = WidgetStates() _create_widget("trigger", states).trigger_value = True _create_widget("bool", states).bool_value = True _create_widget("float", states).double_value = 0.5 _create_widget("int", states).int_value = 123 _create_widget("string", states).string_value = "howdy!" session_state = SessionState() session_state.set_widgets_from_proto(states) session_state.set_metadata(create_metadata("trigger", "trigger_value")) session_state.set_metadata(create_metadata("bool", "bool_value")) session_state.set_metadata(create_metadata("float", "double_value")) session_state.set_metadata(create_metadata("int", "int_value")) session_state.set_metadata(create_metadata("string", "string_value")) self.assertEqual(True, session_state.get("trigger")) self.assertEqual(True, session_state.get("bool")) self.assertAlmostEqual(0.5, session_state.get("float")) self.assertEqual(123, session_state.get("int")) self.assertEqual("howdy!", session_state.get("string"))
def test_rerun_data_coalescing(self): """Test that multiple RERUN requests get coalesced with expected values. (This is similar to widgets_test.test_coalesce_widget_states - it's testing the same thing, but through the ScriptEventQueue interface.) """ queue = ScriptRequestQueue() session_state = SessionState() states = WidgetStates() _create_widget("trigger", states).trigger_value = True _create_widget("int", states).int_value = 123 queue.enqueue(ScriptRequest.RERUN, RerunData(widget_states=states)) states = WidgetStates() _create_widget("trigger", states).trigger_value = False _create_widget("int", states).int_value = 456 session_state.set_metadata( WidgetMetadata("trigger", lambda x, s: x, None, "trigger_value")) session_state.set_metadata( WidgetMetadata("int", lambda x, s: x, lambda x: x, "int_value")) queue.enqueue(ScriptRequest.RERUN, RerunData(widget_states=states)) event, data = queue.dequeue() self.assertEqual(event, ScriptRequest.RERUN) session_state.set_widgets_from_proto(data.widget_states) # Coalesced triggers should be True if either the old or # new value was True self.assertEqual(True, session_state.get("trigger")) # Other widgets should have their newest value self.assertEqual(456, session_state.get("int")) # We should have no more events self.assertEqual((None, None), queue.dequeue(), "Expected empty event queue") # Test that we can coalesce if previous widget state is None queue.enqueue(ScriptRequest.RERUN, RerunData(widget_states=None)) queue.enqueue(ScriptRequest.RERUN, RerunData(widget_states=None)) states = WidgetStates() _create_widget("int", states).int_value = 789 queue.enqueue(ScriptRequest.RERUN, RerunData(widget_states=states)) event, data = queue.dequeue() session_state.set_widgets_from_proto(data.widget_states) self.assertEqual(event, ScriptRequest.RERUN) self.assertEqual(789, session_state.get("int")) # We should have no more events self.assertEqual((None, None), queue.dequeue(), "Expected empty event queue") # Test that we can coalesce if our *new* widget state is None states = WidgetStates() _create_widget("int", states).int_value = 101112 queue.enqueue(ScriptRequest.RERUN, RerunData(widget_states=states)) queue.enqueue(ScriptRequest.RERUN, RerunData(widget_states=None)) event, data = queue.dequeue() session_state.set_widgets_from_proto(data.widget_states) self.assertEqual(event, ScriptRequest.RERUN) self.assertEqual(101112, session_state.get("int")) # We should have no more events self.assertEqual((None, None), queue.dequeue(), "Expected empty event queue")
def test_call_callbacks(self): """Test the call_callbacks method in 6 possible cases: 1. A widget does not have a callback 2. A widget's old and new values are equal, so the callback is not called. 3. A widget's callback has no args provided. 4. A widget's callback has just args provided. 5. A widget's callback has just kwargs provided. 6. A widget's callback has both args and kwargs provided. """ prev_states = WidgetStates() _create_widget("trigger", prev_states).trigger_value = True _create_widget("bool", prev_states).bool_value = True _create_widget("bool2", prev_states).bool_value = True _create_widget("float", prev_states).double_value = 0.5 _create_widget("int", prev_states).int_value = 123 _create_widget("string", prev_states).string_value = "howdy!" session_state = SessionState() session_state.set_widgets_from_proto(prev_states) mock_callback = MagicMock() deserializer = lambda x, s: x callback_cases = [ ("trigger", "trigger_value", None, None, None), ("bool", "bool_value", mock_callback, None, None), ("bool2", "bool_value", mock_callback, None, None), ("float", "double_value", mock_callback, (1, ), None), ("int", "int_value", mock_callback, None, { "x": 2 }), ("string", "string_value", mock_callback, (1, ), { "x": 2 }), ] for widget_id, value_type, callback, args, kwargs in callback_cases: session_state._set_widget_metadata( WidgetMetadata( widget_id, deserializer, lambda x: x, value_type=value_type, callback=callback, callback_args=args, callback_kwargs=kwargs, )) states = WidgetStates() _create_widget("trigger", states).trigger_value = True _create_widget("bool", states).bool_value = True _create_widget("bool2", states).bool_value = False _create_widget("float", states).double_value = 1.5 _create_widget("int", states).int_value = 321 _create_widget("string", states).string_value = "!ydwoh" session_state.on_script_will_rerun(states) mock_callback.assert_has_calls( [call(), call(1), call(x=2), call(1, x=2)])