Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: streamlit/streamlit
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.37.0
Choose a base ref
...
head repository: streamlit/streamlit
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1.37.1
Choose a head ref
  • 5 commits
  • 24 files changed
  • 4 contributors

Commits on Aug 1, 2024

  1. Make ScriptControlException inherit from BaseException again (#9167)

    ## Describe your changes
    
    Closes #9155 
    
    Unfortunately, I changed the base class of `ScriptControlException` in
    485ec8d
    from `BaseException` to `Exception`. This broke a bunch of user apps
    because `st.rerun` started to not work in `try-except` blocks anymore
    when `Exception` was caught. We use `BaseException` again after some
    discussion to avoid the exception being caught in the accidentally.
    
    ## Testing Plan
    
    - E2E Tests
    - Add a test to ensure that the base class won't be changed unnoticeably
    again in the future
    
    ---
    
    **Contribution License Agreement**
    
    By submitting this pull request you agree that all contributions to this
    project are made under the Apache 2.0 license.
    raethlein authored and kmcgrady committed Aug 1, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d711748 View commit details
  2. Support to_pandas method to return a Pandas Series (#9175)

    ## Describe your changes
    
    This is a quick fix for
    #9156. Polars isn't
    officially supported in 1.37, but since some users are already using it
    it is probably worth applying this quick fix. Streamlit 1.38 will come
    with official Polars support + proper testing for this.
    
    ## GitHub Issue Link (if applicable)
    
    - Closes #9156
    
    ## Testing Plan
    
    - Testing will come as part of the official Polars support in 1.38. 
    
    ---
    
    **Contribution License Agreement**
    
    By submitting this pull request you agree that all contributions to this
    project are made under the Apache 2.0 license.
    lukasmasuch authored and kmcgrady committed Aug 1, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2adaeaf View commit details
  3. Revert "Remove fragment_ids_this_run from script run context (#8953)"…

    … and tweaks some types (#9178)
    
    This reverts commit 00cd560.
    
    See the discussion on #9171 for details on this bug.
    
    Closes #9171
    vdonato authored and kmcgrady committed Aug 1, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    6686c3d View commit details
  4. Fix uncaught exception tracking (#9199)

    ## Describe your changes
    
    The metrics tracking for uncaught exceptions broke in 1.37. This PR
    fixes it so that uncaught exceptions are properly tracked via page
    profiling.
    
    ## Testing Plan
    
    - Added a unit test to verify that the metrics gathering works correctly
    when exception is thrown.
    
    ---
    
    **Contribution License Agreement**
    
    By submitting this pull request you agree that all contributions to this
    project are made under the Apache 2.0 license.
    lukasmasuch authored and kmcgrady committed Aug 1, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    b2b7fd6 View commit details
  5. Up version to 1.37.1

    kmcgrady committed Aug 1, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7d8b8d5 View commit details
2 changes: 1 addition & 1 deletion e2e_playwright/shared/app_utils.py
Original file line number Diff line number Diff line change
@@ -225,7 +225,7 @@ def expect_markdown(
locator: Locator | Page,
expected_message: str | Pattern[str],
) -> None:
"""Expect an exception to be displayed in the app.
"""Expect markdown with the given message to be displayed in the app.
Parameters
----------
21 changes: 21 additions & 0 deletions e2e_playwright/st_rerun.py
Original file line number Diff line number Diff line change
@@ -21,6 +21,9 @@

@st.fragment
def my_fragment():
if st.button("rerun whole app (from fragment)"):
st.rerun(scope="app")

if st.button("rerun fragment"):
st.session_state.fragment_count += 1
st.rerun(scope="fragment")
@@ -32,6 +35,15 @@ def my_fragment():
st.rerun(scope="fragment")


@st.fragment
def fragment_with_rerun_in_try_block():
try:
if st.button("rerun try_fragment"):
st.rerun()
except Exception as e:
st.write(f"Caught exception: {e}")


st.session_state.count += 1

if st.session_state.count < 4:
@@ -40,5 +52,14 @@ def my_fragment():
if st.session_state.count >= 4:
st.text("Being able to rerun a session is awesome!")


s = st.selectbox(
"i should retain my state",
["a", "b", "c"],
index=None,
)
st.write(f"selectbox selection: {s}")

my_fragment()
fragment_with_rerun_in_try_block()
st.write(f"app run count: {st.session_state.count}")
52 changes: 46 additions & 6 deletions e2e_playwright/st_rerun_test.py
Original file line number Diff line number Diff line change
@@ -14,26 +14,66 @@

from playwright.sync_api import Page, expect

from e2e_playwright.shared.app_utils import click_button
from e2e_playwright.conftest import wait_for_app_run
from e2e_playwright.shared.app_utils import click_button, expect_markdown


def test_st_rerun_restarts_the_session_when_invoked(app: Page):
def _expect_initial_reruns_finished(app: Page):
expect(app.get_by_test_id("stText")).to_have_text(
"Being able to rerun a session is awesome!"
)


def _expect_initial_reruns_count_text(app: Page):
expect(app.get_by_test_id("stMarkdown").last).to_have_text("app run count: 4")


def test_st_rerun_restarts_the_session_when_invoked(app: Page):
_expect_initial_reruns_finished(app)


def test_fragment_scoped_st_rerun(app: Page):
expect(app.get_by_test_id("stText")).to_have_text(
"Being able to rerun a session is awesome!"
)

click_button(app, "rerun fragment")
expect(app.get_by_test_id("stMarkdown").first).to_have_text("fragment run count: 5")
expect(app.get_by_test_id("stMarkdown").last).to_have_text("app run count: 4")
expect(app.get_by_test_id("stMarkdown").nth(1)).to_have_text(
"fragment run count: 5"
)
_expect_initial_reruns_count_text(app)

click_button(app, "rerun fragment")
expect(app.get_by_test_id("stMarkdown").first).to_have_text(
expect(app.get_by_test_id("stMarkdown").nth(1)).to_have_text(
"fragment run count: 10"
)
expect(app.get_by_test_id("stMarkdown").last).to_have_text("app run count: 4")
# the main apps rerun count should not have been incremented
_expect_initial_reruns_count_text(app)


def test_rerun_works_in_try_except_block(app: Page):
_expect_initial_reruns_finished(app)
_expect_initial_reruns_count_text(app)

click_button(app, "rerun try_fragment")
# the rerun in the try-block worked as expected, so the session_state count
# incremented
expect(app.get_by_test_id("stMarkdown").last).to_have_text("app run count: 5")


def test_state_retained_on_app_scoped_rerun(app: Page):
# Sanity check 1
expect_markdown(app, "selectbox selection: None")

# Click on the selectbox and select the first option.
app.get_by_test_id("stSelectbox").first.locator("input").click()
selection_dropdown = app.locator('[data-baseweb="popover"]').first
selection_dropdown.locator("li").first.click()
wait_for_app_run(app)

# Sanity check 2
expect_markdown(app, "selectbox selection: a")

# Rerun the fragment and verify that the selectbox kept its state
click_button(app, "rerun whole app (from fragment)")
expect_markdown(app, "selectbox selection: a")
4 changes: 2 additions & 2 deletions frontend/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamlit/app",
"version": "1.37.0",
"version": "1.37.1",
"license": "Apache-2.0",
"private": true,
"homepage": "./",
@@ -30,7 +30,7 @@
"@emotion/react": "^11.10.5",
"@emotion/serialize": "^1.1.1",
"@emotion/styled": "^11.10.5",
"@streamlit/lib": "1.37.0",
"@streamlit/lib": "1.37.1",
"axios": "^1.6.0",
"baseui": "12.2.0",
"classnames": "^2.3.2",
2 changes: 1 addition & 1 deletion frontend/lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@streamlit/lib",
"version": "1.37.0",
"version": "1.37.1",
"private": true,
"license": "Apache-2.0",
"main": "dist/index.js",
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "streamlit",
"version": "1.37.0",
"version": "1.37.1",
"private": true,
"workspaces": [
"app",
2 changes: 1 addition & 1 deletion lib/setup.py
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@

THIS_DIRECTORY = Path(__file__).parent

VERSION = "1.37.0" # PEP-440
VERSION = "1.37.1" # PEP-440

# IMPORTANT: We should try very hard *not* to add dependencies to Streamlit.
# And if you do add one, make the required version as general as possible:
6 changes: 2 additions & 4 deletions lib/streamlit/commands/execution_control.py
Original file line number Diff line number Diff line change
@@ -67,9 +67,7 @@ def _new_fragment_id_queue(
return []

else: # scope == "fragment"
curr_queue = (
ctx.script_requests.fragment_id_queue if ctx.script_requests else []
)
curr_queue = ctx.fragment_ids_this_run

# If st.rerun(scope="fragment") is called during a full script run, we raise an
# exception. This occurs, of course, if st.rerun(scope="fragment") is called
@@ -143,7 +141,7 @@ def rerun( # type: ignore[misc]
query_string=query_string,
page_script_hash=page_script_hash,
fragment_id_queue=_new_fragment_id_queue(ctx, scope),
is_fragment_scoped_rerun=True,
is_fragment_scoped_rerun=scope == "fragment",
)
)
# Force a yield point so the runner can do the rerun
2 changes: 1 addition & 1 deletion lib/streamlit/dataframe_util.py
Original file line number Diff line number Diff line change
@@ -309,7 +309,7 @@ def convert_anything_to_pandas_df(
# back to Arrow when marshalled to protobuf, but area/bar/line charts need
# DataFrame magic to generate the correct output.
if hasattr(data, "to_pandas"):
return cast(pd.DataFrame, data.to_pandas())
return pd.DataFrame(data.to_pandas())

# Try to convert to pandas.DataFrame. This will raise an error is df is not
# compatible with the pandas.DataFrame constructor.
2 changes: 1 addition & 1 deletion lib/streamlit/runtime/fragment.py
Original file line number Diff line number Diff line change
@@ -179,7 +179,7 @@ def wrapped_fragment():
ctx = get_script_run_ctx(suppress_warning=True)
assert ctx is not None

if ctx.script_requests and ctx.script_requests.fragment_id_queue:
if ctx.fragment_ids_this_run:
# This script run is a run of one or more fragments. We restore the
# state of ctx.cursors and dg_stack to the snapshots we took when this
# fragment was declared.
4 changes: 1 addition & 3 deletions lib/streamlit/runtime/metrics_util.py
Original file line number Diff line number Diff line change
@@ -482,8 +482,6 @@ def create_page_profile_message(
page_profile.uncaught_exception = uncaught_exception

if ctx := get_script_run_ctx():
page_profile.is_fragment_run = bool(
ctx.script_requests and ctx.script_requests.fragment_id_queue
)
page_profile.is_fragment_run = bool(ctx.fragment_ids_this_run)

return msg
5 changes: 4 additions & 1 deletion lib/streamlit/runtime/scriptrunner/exceptions.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,10 @@
from streamlit.util import repr_


class ScriptControlException(Exception):
# We inherit from BaseException to avoid being caught by user code.
# For example, having it inherit from Exception might make st.rerun not
# work in a try/except block.
class ScriptControlException(BaseException): # NOSONAR
"""Base exception for ScriptRunner."""

pass
23 changes: 20 additions & 3 deletions lib/streamlit/runtime/scriptrunner/exec_code.py
Original file line number Diff line number Diff line change
@@ -27,7 +27,13 @@

def exec_func_with_error_handling(
func: Callable[[], None], ctx: ScriptRunContext
) -> tuple[Any | None, bool, RerunData | None, bool]:
) -> 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
@@ -53,6 +59,7 @@ def exec_func_with_error_handling(
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
"""

# Avoid circular imports
@@ -71,6 +78,9 @@ def exec_func_with_error_handling(
# 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:
@@ -102,5 +112,12 @@ def exec_func_with_error_handling(
run_without_errors = False
premature_stop = True
handle_uncaught_app_exception(ex)

return result, run_without_errors, rerun_exception_data, premature_stop
uncaught_exception = ex

return (
result,
run_without_errors,
rerun_exception_data,
premature_stop,
uncaught_exception,
)
7 changes: 0 additions & 7 deletions lib/streamlit/runtime/scriptrunner/script_requests.py
Original file line number Diff line number Diff line change
@@ -102,13 +102,6 @@ def __init__(self):
self._state = ScriptRequestType.CONTINUE
self._rerun_data = RerunData()

@property
def fragment_id_queue(self) -> list[str]:
if not self._rerun_data:
return []

return self._rerun_data.fragment_id_queue

def request_stop(self) -> None:
"""Request that the ScriptRunner stop running. A stopped ScriptRunner
can't be used anymore. STOP requests succeed unconditionally.
4 changes: 3 additions & 1 deletion lib/streamlit/runtime/scriptrunner/script_run_context.py
Original file line number Diff line number Diff line change
@@ -77,6 +77,7 @@ class ScriptRunContext:
cursors: dict[int, RunningCursor] = field(default_factory=dict)
script_requests: ScriptRequests | None = None
current_fragment_id: str | None = None
fragment_ids_this_run: list[str] | None = None
new_fragment_ids: set[str] = field(default_factory=set)
# we allow only one dialog to be open at the same time
has_dialog_opened: bool = False
@@ -100,6 +101,7 @@ def reset(
self,
query_string: str = "",
page_script_hash: str = "",
fragment_ids_this_run: list[str] | None = None,
) -> None:
self.cursors = {}
self.widget_ids_this_run = set()
@@ -115,7 +117,7 @@ def reset(
self.tracked_commands_counter = collections.Counter()
self.current_fragment_id = None
self.current_fragment_delta_path: list[int] = []
self.fragment_ids_this_run = None
self.fragment_ids_this_run = fragment_ids_this_run
self.new_fragment_ids = set()
self.has_dialog_opened = False
self.disallow_cached_widget_usage = False
7 changes: 5 additions & 2 deletions lib/streamlit/runtime/scriptrunner/script_runner.py
Original file line number Diff line number Diff line change
@@ -425,14 +425,15 @@ def _run_script(self, rerun_data: RerunData) -> None:
rerun_data.page_script_hash, rerun_data.page_name
)
main_page_info = self._pages_manager.get_main_page()
uncaught_exception = None

page_script_hash = (
active_script["page_script_hash"]
if active_script is not None
else main_page_info["page_script_hash"]
)

fragment_ids_this_run = list(rerun_data.fragment_id_queue)

ctx = self._get_script_run_ctx()
# Clear widget state on page change. This normally happens implicitly
# in the script run cleanup steps, but doing it explicitly ensures
@@ -457,6 +458,7 @@ def _run_script(self, rerun_data: RerunData) -> None:
ctx.reset(
query_string=rerun_data.query_string,
page_script_hash=page_script_hash,
fragment_ids_this_run=fragment_ids_this_run,
)
self._pages_manager.reset_active_script_hash()

@@ -586,6 +588,7 @@ def code_to_exec(code=code, module=module, ctx=ctx, rerun_data=rerun_data):
run_without_errors,
rerun_exception_data,
premature_stop,
uncaught_exception,
) = exec_func_with_error_handling(code_to_exec, ctx)
# setting the session state here triggers a yield-callback call
# which reads self._requests and checks for rerun data
@@ -611,7 +614,7 @@ def code_to_exec(code=code, module=module, ctx=ctx, rerun_data=rerun_data):
# Create and send page profile information
ctx.enqueue(
create_page_profile_message(
ctx.tracked_commands,
commands=ctx.tracked_commands,
exec_time=to_microseconds(timer() - start_time),
prep_time=to_microseconds(prep_time),
uncaught_exception=(
12 changes: 4 additions & 8 deletions lib/streamlit/runtime/state/session_state.py
Original file line number Diff line number Diff line change
@@ -186,7 +186,7 @@ def set_widget_metadata(self, widget_meta: WidgetMetadata[Any]) -> None:
def remove_stale_widgets(
self,
active_widget_ids: set[str],
fragment_ids_this_run: set[str] | None,
fragment_ids_this_run: list[str] | None,
) -> None:
"""Remove widget state for stale widgets."""
self.states = {
@@ -579,13 +579,9 @@ def _remove_stale_widgets(self, active_widget_ids: set[str]) -> None:
if ctx is None:
return

fragment_ids_this_run = (
set(ctx.script_requests.fragment_id_queue) if ctx.script_requests else set()
)

self._new_widget_state.remove_stale_widgets(
active_widget_ids,
fragment_ids_this_run,
ctx.fragment_ids_this_run,
)

# Remove entries from _old_state corresponding to
@@ -598,7 +594,7 @@ def _remove_stale_widgets(self, active_widget_ids: set[str]) -> None:
or not _is_stale_widget(
self._new_widget_state.widget_metadata.get(k),
active_widget_ids,
fragment_ids_this_run,
ctx.fragment_ids_this_run,
)
)
}
@@ -706,7 +702,7 @@ def _is_internal_key(key: str) -> bool:
def _is_stale_widget(
metadata: WidgetMetadata[Any] | None,
active_widget_ids: set[str],
fragment_ids_this_run: set[str] | None,
fragment_ids_this_run: list[str] | None,
) -> bool:
if not metadata:
return True
Loading