166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from typing import TYPE_CHECKING, Any, Callable, Literal
|
|
|
|
from streamlit import util
|
|
from streamlit.delta_generator_singletons import (
|
|
context_dg_stack,
|
|
get_default_dg_stack_value,
|
|
)
|
|
from streamlit.error_util import handle_uncaught_app_exception
|
|
from streamlit.errors import FragmentHandledException
|
|
from streamlit.runtime.scriptrunner_utils.exceptions import (
|
|
RerunException,
|
|
StopException,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from types import TracebackType
|
|
|
|
from streamlit.runtime.scriptrunner_utils.script_requests import RerunData
|
|
from streamlit.runtime.scriptrunner_utils.script_run_context import ScriptRunContext
|
|
|
|
|
|
class modified_sys_path: # noqa: N801
|
|
"""A context for prepending a directory to sys.path for a second.
|
|
|
|
Code inspired by IPython:
|
|
Source: https://github.com/ipython/ipython/blob/master/IPython/utils/syspathcontext.py#L42
|
|
"""
|
|
|
|
def __init__(self, main_script_path: str) -> None:
|
|
self._main_script_path = main_script_path
|
|
self._added_path = False
|
|
|
|
def __repr__(self) -> str:
|
|
return util.repr_(self)
|
|
|
|
def __enter__(self) -> None:
|
|
if self._main_script_path not in sys.path:
|
|
sys.path.insert(0, self._main_script_path)
|
|
self._added_path = True
|
|
|
|
def __exit__(
|
|
self,
|
|
typ: type[BaseException] | None,
|
|
exc: BaseException | None,
|
|
tb: TracebackType | None,
|
|
) -> Literal[False]:
|
|
if self._added_path:
|
|
try:
|
|
sys.path.remove(self._main_script_path)
|
|
except ValueError:
|
|
# It's already removed.
|
|
pass
|
|
|
|
# Returning False causes any exceptions to be re-raised.
|
|
return False
|
|
|
|
|
|
def exec_func_with_error_handling(
|
|
func: Callable[[], Any], ctx: ScriptRunContext
|
|
) -> tuple[
|
|
Any | None,
|
|
bool,
|
|
RerunData | None,
|
|
bool,
|
|
Exception | None,
|
|
]:
|
|
"""Execute the passed function wrapped in a try/except block.
|
|
|
|
This function is called by the script runner to execute the user's script or
|
|
fragment reruns, but also for the execution of fragment code in context of a normal
|
|
app run. This wrapper ensures that handle_uncaught_exception messages show up in the
|
|
correct context.
|
|
|
|
Parameters
|
|
----------
|
|
func : callable
|
|
The function to execute wrapped in the try/except block.
|
|
ctx : ScriptRunContext
|
|
The context in which the script is being run.
|
|
|
|
Returns
|
|
-------
|
|
tuple
|
|
A tuple containing:
|
|
- The result of the passed function.
|
|
- A boolean indicating whether the script ran without errors (RerunException and
|
|
StopException don't count as errors).
|
|
- The RerunData instance belonging to a RerunException if the script was
|
|
interrupted by a RerunException.
|
|
- A boolean indicating whether the script was stopped prematurely (False for
|
|
RerunExceptions, True for all other exceptions).
|
|
- The uncaught exception if one occurred, None otherwise
|
|
"""
|
|
run_without_errors = True
|
|
|
|
# This will be set to a RerunData instance if our execution
|
|
# is interrupted by a RerunException.
|
|
rerun_exception_data: RerunData | None = None
|
|
|
|
# If the script stops early, we don't want to remove unseen widgets,
|
|
# so we track this to potentially skip session state cleanup later.
|
|
premature_stop: bool = False
|
|
|
|
# The result of the passed function
|
|
result: Any | None = None
|
|
|
|
# The uncaught exception if one occurred, None otherwise
|
|
uncaught_exception: Exception | None = None
|
|
|
|
try:
|
|
result = func()
|
|
except RerunException as e:
|
|
rerun_exception_data = e.rerun_data
|
|
|
|
# Since the script is about to rerun, we may need to reset our cursors/dg_stack
|
|
# so that we write to the right place in the app. For full script runs, this
|
|
# needs to happen in case the same thread reruns our script (a different thread
|
|
# would automatically come with fresh cursors/dg_stack values). For fragments,
|
|
# it doesn't matter either way since the fragment resets these values from its
|
|
# snapshot before execution.
|
|
ctx.cursors.clear()
|
|
context_dg_stack.set(get_default_dg_stack_value())
|
|
|
|
# Interruption due to a rerun is usually from `st.rerun()`, which
|
|
# we want to count as a script completion so triggers reset.
|
|
# It is also possible for this to happen if fast reruns is off,
|
|
# but this is very rare.
|
|
premature_stop = False
|
|
|
|
except StopException:
|
|
# This is thrown when the script executes `st.stop()`.
|
|
# We don't have to do anything here.
|
|
premature_stop = True
|
|
except FragmentHandledException:
|
|
run_without_errors = False
|
|
premature_stop = True
|
|
except Exception as ex:
|
|
run_without_errors = False
|
|
premature_stop = True
|
|
handle_uncaught_app_exception(ex)
|
|
uncaught_exception = ex
|
|
|
|
return (
|
|
result,
|
|
run_without_errors,
|
|
rerun_exception_data,
|
|
premature_stop,
|
|
uncaught_exception,
|
|
)
|