def _read_from_cache(mem_cache, key, persist, allow_output_mutation, func_or_code, hash_funcs=None): """Read a value from the cache. Our goal is to read from memory if possible. If the data was mutated (hash changed), we show a warning. If reading from memory fails, we either read from disk or rerun the code. """ try: return _read_from_mem_cache(mem_cache, key, allow_output_mutation, func_or_code, hash_funcs) except CachedObjectMutationError as e: handle_uncaught_app_exception(CachedObjectMutationWarning(e)) return e.cached_value except CacheKeyNotFoundError as e: if persist: value = _read_from_disk_cache(key) _write_to_mem_cache(mem_cache, key, value, allow_output_mutation, func_or_code, hash_funcs) return value raise e
def test_handle_uncaught_app_exception_with_rich(self): """Test if the exception is logged with rich enabled and disabled.""" exc = Exception("boom!") with testutil.patch_config_options({"logger.enableRich": True}): with io.StringIO() as buf: # Capture stdout logs (rich logs to stdout) with contextlib.redirect_stdout(buf): handle_uncaught_app_exception(exc) # Capture the stdout output captured_output = buf.getvalue() assert "Exception:" in captured_output assert "boom!" in captured_output # Uncaught app exception is only used by the non-rich exception logging assert "Uncaught app exception" not in captured_output with testutil.patch_config_options({"logger.enableRich": False}): with io.StringIO() as buf: # Capture stdout logs with contextlib.redirect_stdout(buf): handle_uncaught_app_exception(exc) # Capture the stdout output captured_output = buf.getvalue() # With rich deactivated, the exception is not logged to stdout assert "Exception:" not in captured_output assert "boom!" not in captured_output
def test_uncaught_exception_no_details(self, mock_st_error, mock_st_exception): """If client.showErrorDetails is false, uncaught app errors are logged, and a generic error message is printed to the frontend.""" with testutil.patch_config_options({"client.showErrorDetails": False}): exc = RuntimeError("boom!") handle_uncaught_app_exception(exc) mock_st_error.assert_not_called() mock_st_exception.assert_called_once()
def test_uncaught_exception_show_details(self, mock_st_error, mock_st_exception): """If client.showErrorDetails is true, uncaught app errors print to the frontend.""" with testutil.patch_config_options({"client.showErrorDetails": True}): exc = RuntimeError("boom!") handle_uncaught_app_exception(exc) mock_st_error.assert_not_called() mock_st_exception.assert_called_once_with(exc)
def test_uncaught_exception_no_tracebacks(self, mock_st_error, mock_st_exception): """If client.showTracebacks is false, uncaught app errors are logged, and a generic error message is printed to the frontend.""" with testutil.patch_config_options({"client.showTracebacks": False}): exc = RuntimeError("boom!") handle_uncaught_app_exception(exc) mock_st_exception.assert_not_called() mock_st_error.assert_called_once_with( _GENERIC_UNCAUGHT_EXCEPTION_TEXT)
def _run_script(self, rerun_data): """Run our script. Parameters ---------- rerun_data: RerunData The RerunData to use. """ assert self._is_in_script_thread() LOGGER.debug("Running script %s", rerun_data) # Reset DeltaGenerators, widgets, media files. media_file_manager.clear_session_files() ctx = get_report_ctx() if ctx is None: # This should never be possible on the script_runner thread. raise RuntimeError( "ScriptRunner thread has a null ReportContext. Something has gone very wrong!" ) ctx.reset(query_string=rerun_data.query_string) self.on_event.send(ScriptRunnerEvent.SCRIPT_STARTED) # Compile the script. Any errors thrown here will be surfaced # to the user via a modal dialog in the frontend, and won't result # in their previous report disappearing. try: with source_util.open_python_file(self._report.script_path) as f: filebody = f.read() if config.get_option("runner.magicEnabled"): filebody = magic.add_magic(filebody, self._report.script_path) code = compile( filebody, # Pass in the file path so it can show up in exceptions. self._report.script_path, # We're compiling entire blocks of Python, so we need "exec" # mode (as opposed to "eval" or "single"). mode="exec", # Don't inherit any flags or "future" statements. flags=0, dont_inherit=1, # Use the default optimization options. optimize=-1, ) except BaseException as e: # We got a compile error. Send an error event and bail immediately. LOGGER.debug("Fatal script error: %s" % e) self.on_event.send( ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR, exception=e) return # If we get here, we've successfully compiled our script. The next step # is to run it. Errors thrown during execution will be shown to the # user as ExceptionElements. # Update the Widget object with the new widget_states. # (The ReportContext has a reference to this object, so we just update it in-place) if rerun_data.widget_states is not None: self._widgets.set_state(rerun_data.widget_states) if config.get_option("runner.installTracer"): self._install_tracer() # This will be set to a RerunData instance if our execution # is interrupted by a RerunException. rerun_with_data = None try: # Create fake module. This gives us a name global namespace to # execute the code in. module = _new_module("__main__") # Install the fake module as the __main__ module. This allows # the pickle module to work inside the user's code, since it now # can know the module where the pickled objects stem from. # IMPORTANT: This means we can't use "if __name__ == '__main__'" in # our code, as it will point to the wrong module!!! sys.modules["__main__"] = module # Add special variables to the module's globals dict. # Note: The following is a requirement for the CodeHasher to # work correctly. The CodeHasher is scoped to # files contained in the directory of __main__.__file__, which we # assume is the main script directory. module.__dict__["__file__"] = self._report.script_path with modified_sys_path(self._report), self._set_execing_flag(): exec(code, module.__dict__) except RerunException as e: rerun_with_data = e.rerun_data except StopException: pass except BaseException as e: handle_uncaught_app_exception(e) finally: self._widgets.reset_triggers() self._widgets.cull_nonexistent(ctx.widget_ids_this_run.items()) self.on_event.send(ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS) # delete expired files now that the script has run and files in use # are marked as active media_file_manager.del_expired_files() # Use _log_if_error() to make sure we never ever ever stop running the # script without meaning to. _log_if_error(_clean_problem_modules) if rerun_with_data is not None: self._run_script(rerun_with_data)
def _run_script(self, rerun_data: RerunData) -> None: """Run our script. Parameters ---------- rerun_data: RerunData The RerunData to use. """ assert self._is_in_script_thread() LOGGER.debug("Running script %s", rerun_data) # Reset DeltaGenerators, widgets, media files. in_memory_file_manager.clear_session_files() ctx = self._get_script_run_ctx() ctx.reset(query_string=rerun_data.query_string) self.on_event.send(ScriptRunnerEvent.SCRIPT_STARTED) # Compile the script. Any errors thrown here will be surfaced # to the user via a modal dialog in the frontend, and won't result # in their previous script elements disappearing. try: with source_util.open_python_file( self._session_data.script_path) as f: filebody = f.read() if config.get_option("runner.magicEnabled"): filebody = magic.add_magic(filebody, self._session_data.script_path) code = compile( filebody, # Pass in the file path so it can show up in exceptions. self._session_data.script_path, # We're compiling entire blocks of Python, so we need "exec" # mode (as opposed to "eval" or "single"). mode="exec", # Don't inherit any flags or "future" statements. flags=0, dont_inherit=1, # Use the default optimization options. optimize=-1, ) except BaseException as e: # We got a compile error. Send an error event and bail immediately. LOGGER.debug("Fatal script error: %s" % e) self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = False self.on_event.send( ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR, exception=e) return # If we get here, we've successfully compiled our script. The next step # is to run it. Errors thrown during execution will be shown to the # user as ExceptionElements. if config.get_option("runner.installTracer"): self._install_tracer() # This will be set to a RerunData instance if our execution # is interrupted by a RerunException. rerun_with_data = None try: # Create fake module. This gives us a name global namespace to # execute the code in. module = _new_module("__main__") # Install the fake module as the __main__ module. This allows # the pickle module to work inside the user's code, since it now # can know the module where the pickled objects stem from. # IMPORTANT: This means we can't use "if __name__ == '__main__'" in # our code, as it will point to the wrong module!!! sys.modules["__main__"] = module # Add special variables to the module's globals dict. # Note: The following is a requirement for the CodeHasher to # work correctly. The CodeHasher is scoped to # files contained in the directory of __main__.__file__, which we # assume is the main script directory. module.__dict__["__file__"] = self._session_data.script_path with modified_sys_path( self._session_data), self._set_execing_flag(): # Run callbacks for widgets whose values have changed. if rerun_data.widget_states is not None: # Update the WidgetManager with the new widget_states. # The old states, used to skip callbacks if values # haven't changed, are also preserved in the # WidgetManager. self._session_state.compact_state() self._session_state.set_widgets_from_proto( rerun_data.widget_states) self._session_state.call_callbacks() ctx.on_script_start() exec(code, module.__dict__) self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = True except RerunException as e: rerun_with_data = e.rerun_data except StopException: pass except BaseException as e: self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = False handle_uncaught_app_exception(e) finally: self._on_script_finished(ctx) # Use _log_if_error() to make sure we never ever ever stop running the # script without meaning to. _log_if_error(_clean_problem_modules) if rerun_with_data is not None: self._run_script(rerun_with_data)