streamlit-nightly 1.31.1.dev20240207__py2.py3-none-any.whl → 1.31.1.dev20240208__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,7 @@ from typing import (
30
30
  Any,
31
31
  Callable,
32
32
  Dict,
33
+ Final,
33
34
  Iterator,
34
35
  List,
35
36
  Optional,
@@ -58,9 +59,8 @@ from streamlit.runtime.legacy_caching.hashing import (
58
59
  from streamlit.runtime.metrics_util import gather_metrics
59
60
  from streamlit.runtime.stats import CacheStat, CacheStatsProvider
60
61
  from streamlit.util import HASHLIB_KWARGS
61
- from streamlit.vendor.pympler.asizeof import asizeof
62
62
 
63
- _LOGGER = get_logger(__name__)
63
+ _LOGGER: Final = get_logger(__name__)
64
64
 
65
65
  # The timer function we use with TTLCache. This is the default timer func, but
66
66
  # is exposed here as a constant so that it can be patched in unit tests.
@@ -226,6 +226,9 @@ class _MemCaches(CacheStatsProvider):
226
226
  # lock during stats-gathering.
227
227
  function_caches = self._function_caches.copy()
228
228
 
229
+ # Lazy-load vendored package to prevent import of numpy
230
+ from streamlit.vendor.pympler.asizeof import asizeof
231
+
229
232
  stats = [
230
233
  CacheStat("st_cache", cache.display_name, asizeof(c))
231
234
  for cache in function_caches.values()
@@ -394,203 +394,210 @@ class ScriptRunner:
394
394
  """
395
395
  assert self._is_in_script_thread()
396
396
 
397
- _LOGGER.debug("Running script %s", rerun_data)
398
-
399
- start_time: float = timer()
400
- prep_time: float = 0 # This will be overwritten once preparations are done.
401
-
402
- # Reset DeltaGenerators, widgets, media files.
403
- runtime.get_instance().media_file_mgr.clear_session_refs()
404
-
405
- main_script_path = self._main_script_path
406
- pages = source_util.get_pages(main_script_path)
407
- # Safe because pages will at least contain the app's main page.
408
- main_page_info = list(pages.values())[0]
409
- current_page_info = None
410
- uncaught_exception = None
411
-
412
- if rerun_data.page_script_hash:
413
- current_page_info = pages.get(rerun_data.page_script_hash, None)
414
- elif not rerun_data.page_script_hash and rerun_data.page_name:
415
- # If a user navigates directly to a non-main page of an app, we get
416
- # the first script run request before the list of pages has been
417
- # sent to the frontend. In this case, we choose the first script
418
- # with a name matching the requested page name.
419
- current_page_info = next(
420
- filter(
421
- # There seems to be this weird bug with mypy where it
422
- # thinks that p can be None (which is impossible given the
423
- # types of pages), so we add `p and` at the beginning of
424
- # the predicate to circumvent this.
425
- lambda p: p and (p["page_name"] == rerun_data.page_name),
426
- pages.values(),
427
- ),
428
- None,
397
+ # An explicit loop instead of recursion to avoid stack overflows
398
+ while True:
399
+ _LOGGER.debug("Running script %s", rerun_data)
400
+ start_time: float = timer()
401
+ prep_time: float = 0 # This will be overwritten once preparations are done.
402
+
403
+ # Reset DeltaGenerators, widgets, media files.
404
+ runtime.get_instance().media_file_mgr.clear_session_refs()
405
+
406
+ main_script_path = self._main_script_path
407
+ pages = source_util.get_pages(main_script_path)
408
+ # Safe because pages will at least contain the app's main page.
409
+ main_page_info = list(pages.values())[0]
410
+ current_page_info = None
411
+ uncaught_exception = None
412
+
413
+ if rerun_data.page_script_hash:
414
+ current_page_info = pages.get(rerun_data.page_script_hash, None)
415
+ elif not rerun_data.page_script_hash and rerun_data.page_name:
416
+ # If a user navigates directly to a non-main page of an app, we get
417
+ # the first script run request before the list of pages has been
418
+ # sent to the frontend. In this case, we choose the first script
419
+ # with a name matching the requested page name.
420
+ current_page_info = next(
421
+ filter(
422
+ # There seems to be this weird bug with mypy where it
423
+ # thinks that p can be None (which is impossible given the
424
+ # types of pages), so we add `p and` at the beginning of
425
+ # the predicate to circumvent this.
426
+ lambda p: p and (p["page_name"] == rerun_data.page_name),
427
+ pages.values(),
428
+ ),
429
+ None,
430
+ )
431
+ else:
432
+ # If no information about what page to run is given, default to
433
+ # running the main page.
434
+ current_page_info = main_page_info
435
+
436
+ page_script_hash = (
437
+ current_page_info["page_script_hash"]
438
+ if current_page_info is not None
439
+ else main_page_info["page_script_hash"]
429
440
  )
430
- else:
431
- # If no information about what page to run is given, default to
432
- # running the main page.
433
- current_page_info = main_page_info
434
-
435
- page_script_hash = (
436
- current_page_info["page_script_hash"]
437
- if current_page_info is not None
438
- else main_page_info["page_script_hash"]
439
- )
440
441
 
441
- ctx = self._get_script_run_ctx()
442
- ctx.reset(
443
- query_string=rerun_data.query_string,
444
- page_script_hash=page_script_hash,
445
- )
446
-
447
- self.on_event.send(
448
- self,
449
- event=ScriptRunnerEvent.SCRIPT_STARTED,
450
- page_script_hash=page_script_hash,
451
- )
442
+ ctx = self._get_script_run_ctx()
443
+ ctx.reset(
444
+ query_string=rerun_data.query_string,
445
+ page_script_hash=page_script_hash,
446
+ )
452
447
 
453
- # Compile the script. Any errors thrown here will be surfaced
454
- # to the user via a modal dialog in the frontend, and won't result
455
- # in their previous script elements disappearing.
456
- try:
457
- if current_page_info:
458
- script_path = current_page_info["script_path"]
459
- else:
460
- script_path = main_script_path
461
-
462
- # At this point, we know that either
463
- # * the script corresponding to the hash requested no longer
464
- # exists, or
465
- # * we were not able to find a script with the requested page
466
- # name.
467
- # In both of these cases, we want to send a page_not_found
468
- # message to the frontend.
469
- msg = ForwardMsg()
470
- msg.page_not_found.page_name = rerun_data.page_name
471
- ctx.enqueue(msg)
472
-
473
- code = self._script_cache.get_bytecode(script_path)
474
-
475
- except Exception as ex:
476
- # We got a compile error. Send an error event and bail immediately.
477
- _LOGGER.debug("Fatal script error: %s", ex)
478
- self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = False
479
448
  self.on_event.send(
480
449
  self,
481
- event=ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR,
482
- exception=ex,
450
+ event=ScriptRunnerEvent.SCRIPT_STARTED,
451
+ page_script_hash=page_script_hash,
483
452
  )
484
- return
485
453
 
486
- # If we get here, we've successfully compiled our script. The next step
487
- # is to run it. Errors thrown during execution will be shown to the
488
- # user as ExceptionElements.
489
-
490
- if config.get_option("runner.installTracer"):
491
- self._install_tracer()
492
-
493
- # This will be set to a RerunData instance if our execution
494
- # is interrupted by a RerunException.
495
- rerun_exception_data: Optional[RerunData] = None
496
-
497
- # If the script stops early, we don't want to remove unseen widgets,
498
- # so we track this to potentially skip session state cleanup later.
499
- premature_stop: bool = False
454
+ # Compile the script. Any errors thrown here will be surfaced
455
+ # to the user via a modal dialog in the frontend, and won't result
456
+ # in their previous script elements disappearing.
457
+ try:
458
+ if current_page_info:
459
+ script_path = current_page_info["script_path"]
460
+ else:
461
+ script_path = main_script_path
462
+
463
+ # At this point, we know that either
464
+ # * the script corresponding to the hash requested no longer
465
+ # exists, or
466
+ # * we were not able to find a script with the requested page
467
+ # name.
468
+ # In both of these cases, we want to send a page_not_found
469
+ # message to the frontend.
470
+ msg = ForwardMsg()
471
+ msg.page_not_found.page_name = rerun_data.page_name
472
+ ctx.enqueue(msg)
473
+
474
+ code = self._script_cache.get_bytecode(script_path)
475
+
476
+ except Exception as ex:
477
+ # We got a compile error. Send an error event and bail immediately.
478
+ _LOGGER.debug("Fatal script error: %s", ex)
479
+ self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = False
480
+ self.on_event.send(
481
+ self,
482
+ event=ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR,
483
+ exception=ex,
484
+ )
485
+ return
486
+
487
+ # If we get here, we've successfully compiled our script. The next step
488
+ # is to run it. Errors thrown during execution will be shown to the
489
+ # user as ExceptionElements.
490
+
491
+ if config.get_option("runner.installTracer"):
492
+ self._install_tracer()
493
+
494
+ # This will be set to a RerunData instance if our execution
495
+ # is interrupted by a RerunException.
496
+ rerun_exception_data: Optional[RerunData] = None
497
+
498
+ # If the script stops early, we don't want to remove unseen widgets,
499
+ # so we track this to potentially skip session state cleanup later.
500
+ premature_stop: bool = False
501
+
502
+ try:
503
+ # Create fake module. This gives us a name global namespace to
504
+ # execute the code in.
505
+ # TODO(vdonato): Double-check that we're okay with naming the
506
+ # module for every page `__main__`. I'm pretty sure this is
507
+ # necessary given that people will likely often write
508
+ # ```
509
+ # if __name__ == "__main__":
510
+ # ...
511
+ # ```
512
+ # in their scripts.
513
+ module = _new_module("__main__")
514
+
515
+ # Install the fake module as the __main__ module. This allows
516
+ # the pickle module to work inside the user's code, since it now
517
+ # can know the module where the pickled objects stem from.
518
+ # IMPORTANT: This means we can't use "if __name__ == '__main__'" in
519
+ # our code, as it will point to the wrong module!!!
520
+ sys.modules["__main__"] = module
521
+
522
+ # Add special variables to the module's globals dict.
523
+ # Note: The following is a requirement for the CodeHasher to
524
+ # work correctly. The CodeHasher is scoped to
525
+ # files contained in the directory of __main__.__file__, which we
526
+ # assume is the main script directory.
527
+ module.__dict__["__file__"] = script_path
528
+
529
+ with modified_sys_path(
530
+ self._main_script_path
531
+ ), self._set_execing_flag():
532
+ # Run callbacks for widgets whose values have changed.
533
+ if rerun_data.widget_states is not None:
534
+ self._session_state.on_script_will_rerun(
535
+ rerun_data.widget_states
536
+ )
500
537
 
501
- try:
502
- # Create fake module. This gives us a name global namespace to
503
- # execute the code in.
504
- # TODO(vdonato): Double-check that we're okay with naming the
505
- # module for every page `__main__`. I'm pretty sure this is
506
- # necessary given that people will likely often write
507
- # ```
508
- # if __name__ == "__main__":
509
- # ...
510
- # ```
511
- # in their scripts.
512
- module = _new_module("__main__")
513
-
514
- # Install the fake module as the __main__ module. This allows
515
- # the pickle module to work inside the user's code, since it now
516
- # can know the module where the pickled objects stem from.
517
- # IMPORTANT: This means we can't use "if __name__ == '__main__'" in
518
- # our code, as it will point to the wrong module!!!
519
- sys.modules["__main__"] = module
520
-
521
- # Add special variables to the module's globals dict.
522
- # Note: The following is a requirement for the CodeHasher to
523
- # work correctly. The CodeHasher is scoped to
524
- # files contained in the directory of __main__.__file__, which we
525
- # assume is the main script directory.
526
- module.__dict__["__file__"] = script_path
527
-
528
- with modified_sys_path(self._main_script_path), self._set_execing_flag():
529
- # Run callbacks for widgets whose values have changed.
530
- if rerun_data.widget_states is not None:
531
- self._session_state.on_script_will_rerun(rerun_data.widget_states)
532
-
533
- ctx.on_script_start()
534
- prep_time = timer() - start_time
535
- exec(code, module.__dict__)
536
- self._session_state.maybe_check_serializable()
537
- self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = True
538
- except RerunException as e:
539
- rerun_exception_data = e.rerun_data
540
- # Interruption due to a rerun is usually from `st.rerun()`, which
541
- # we want to count as a script completion so triggers reset.
542
- # It is also possible for this to happen if fast reruns is off,
543
- # but this is very rare.
544
- premature_stop = False
545
-
546
- except StopException:
547
- # This is thrown when the script executes `st.stop()`.
548
- # We don't have to do anything here.
549
- premature_stop = True
550
-
551
- except Exception as ex:
552
- self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = False
553
- uncaught_exception = ex
554
- handle_uncaught_app_exception(uncaught_exception)
555
- premature_stop = True
538
+ ctx.on_script_start()
539
+ prep_time = timer() - start_time
540
+ exec(code, module.__dict__)
541
+ self._session_state.maybe_check_serializable()
542
+ self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = True
543
+ except RerunException as e:
544
+ rerun_exception_data = e.rerun_data
545
+ # Interruption due to a rerun is usually from `st.rerun()`, which
546
+ # we want to count as a script completion so triggers reset.
547
+ # It is also possible for this to happen if fast reruns is off,
548
+ # but this is very rare.
549
+ premature_stop = False
550
+
551
+ except StopException:
552
+ # This is thrown when the script executes `st.stop()`.
553
+ # We don't have to do anything here.
554
+ premature_stop = True
555
+
556
+ except Exception as ex:
557
+ self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = False
558
+ uncaught_exception = ex
559
+ handle_uncaught_app_exception(uncaught_exception)
560
+ premature_stop = True
561
+
562
+ finally:
563
+ if rerun_exception_data:
564
+ finished_event = ScriptRunnerEvent.SCRIPT_STOPPED_FOR_RERUN
565
+ else:
566
+ finished_event = ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
567
+
568
+ if ctx.gather_usage_stats:
569
+ try:
570
+ # Prevent issues with circular import
571
+ from streamlit.runtime.metrics_util import (
572
+ create_page_profile_message,
573
+ to_microseconds,
574
+ )
556
575
 
557
- finally:
558
- if rerun_exception_data:
559
- finished_event = ScriptRunnerEvent.SCRIPT_STOPPED_FOR_RERUN
560
- else:
561
- finished_event = ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
562
-
563
- if ctx.gather_usage_stats:
564
- try:
565
- # Prevent issues with circular import
566
- from streamlit.runtime.metrics_util import (
567
- create_page_profile_message,
568
- to_microseconds,
569
- )
570
-
571
- # Create and send page profile information
572
- ctx.enqueue(
573
- create_page_profile_message(
574
- ctx.tracked_commands,
575
- exec_time=to_microseconds(timer() - start_time),
576
- prep_time=to_microseconds(prep_time),
577
- uncaught_exception=type(uncaught_exception).__name__
578
- if uncaught_exception
579
- else None,
576
+ # Create and send page profile information
577
+ ctx.enqueue(
578
+ create_page_profile_message(
579
+ ctx.tracked_commands,
580
+ exec_time=to_microseconds(timer() - start_time),
581
+ prep_time=to_microseconds(prep_time),
582
+ uncaught_exception=type(uncaught_exception).__name__
583
+ if uncaught_exception
584
+ else None,
585
+ )
580
586
  )
581
- )
582
- except Exception as ex:
583
- # Always capture all exceptions since we want to make sure that
584
- # the telemetry never causes any issues.
585
- _LOGGER.debug("Failed to create page profile", exc_info=ex)
586
- self._on_script_finished(ctx, finished_event, premature_stop)
587
-
588
- # Use _log_if_error() to make sure we never ever ever stop running the
589
- # script without meaning to.
590
- _log_if_error(_clean_problem_modules)
591
-
592
- if rerun_exception_data is not None:
593
- self._run_script(rerun_exception_data)
587
+ except Exception as ex:
588
+ # Always capture all exceptions since we want to make sure that
589
+ # the telemetry never causes any issues.
590
+ _LOGGER.debug("Failed to create page profile", exc_info=ex)
591
+ self._on_script_finished(ctx, finished_event, premature_stop)
592
+
593
+ # Use _log_if_error() to make sure we never ever ever stop running the
594
+ # script without meaning to.
595
+ _log_if_error(_clean_problem_modules)
596
+
597
+ if rerun_exception_data is not None:
598
+ rerun_data = rerun_exception_data
599
+ else:
600
+ break
594
601
 
595
602
  def _on_script_finished(
596
603
  self, ctx: ScriptRunContext, event: ScriptRunnerEvent, premature_stop: bool
@@ -11,6 +11,7 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+
14
15
  from __future__ import annotations
15
16
 
16
17
  import json
@@ -20,6 +21,7 @@ from dataclasses import dataclass, field, replace
20
21
  from typing import (
21
22
  TYPE_CHECKING,
22
23
  Any,
24
+ Final,
23
25
  Iterator,
24
26
  KeysView,
25
27
  List,
@@ -28,7 +30,7 @@ from typing import (
28
30
  cast,
29
31
  )
30
32
 
31
- from typing_extensions import Final, TypeAlias
33
+ from typing_extensions import TypeAlias
32
34
 
33
35
  import streamlit as st
34
36
  from streamlit import config, util
@@ -45,7 +47,6 @@ from streamlit.runtime.state.common import (
45
47
  from streamlit.runtime.state.query_params import QueryParams
46
48
  from streamlit.runtime.stats import CacheStat, CacheStatsProvider, group_stats
47
49
  from streamlit.type_util import ValueFieldName, is_array_value_field_name
48
- from streamlit.vendor.pympler.asizeof import asizeof
49
50
 
50
51
  if TYPE_CHECKING:
51
52
  from streamlit.runtime.session_manager import SessionManager
@@ -637,6 +638,9 @@ class SessionState:
637
638
  return True
638
639
 
639
640
  def get_stats(self) -> list[CacheStat]:
641
+ # Lazy-load vendored package to prevent import of numpy
642
+ from streamlit.vendor.pympler.asizeof import asizeof
643
+
640
644
  stat = CacheStat("st_session_state", "", asizeof(self))
641
645
  return [stat]
642
646
 
streamlit/string_util.py CHANGED
@@ -12,22 +12,18 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  import re
16
18
  import textwrap
17
- from typing import TYPE_CHECKING, Any, Optional, Tuple, cast
19
+ from typing import TYPE_CHECKING, Any, Tuple, cast
18
20
 
19
- from streamlit.emojis import ALL_EMOJIS
20
21
  from streamlit.errors import StreamlitAPIException
21
22
 
22
23
  if TYPE_CHECKING:
23
24
  from streamlit.type_util import SupportsStr
24
25
 
25
-
26
- # The ESCAPED_EMOJI list is sorted in descending order to make that longer emoji appear
27
- # first in the regex compiled below. This ensures that we grab the full emoji in a
28
- # multi-character emoji sequence that starts with a shorter emoji (emoji are weird...).
29
- ESCAPED_EMOJI = [re.escape(e) for e in sorted(ALL_EMOJIS, reverse=True)]
30
- EMOJI_EXTRACTION_REGEX = re.compile(f"^({'|'.join(ESCAPED_EMOJI)})[_ -]*(.*)")
26
+ _ALPHANUMERIC_CHAR_REGEX = re.compile(r"^[a-zA-Z0-9_&\-\. ]+$")
31
27
 
32
28
 
33
29
  def decode_ascii(string: bytes) -> str:
@@ -40,14 +36,29 @@ def clean_text(text: "SupportsStr") -> str:
40
36
  return textwrap.dedent(str(text)).strip()
41
37
 
42
38
 
39
+ def _contains_special_chars(text: str) -> bool:
40
+ """Check if a string contains any special chars.
41
+
42
+ Special chars in that case are all chars that are not
43
+ alphanumeric, underscore, hyphen or whitespace.
44
+ """
45
+ return re.match(_ALPHANUMERIC_CHAR_REGEX, text) is None if text else False
46
+
47
+
43
48
  def is_emoji(text: str) -> bool:
44
49
  """Check if input string is a valid emoji."""
50
+ if not _contains_special_chars(text):
51
+ return False
52
+
53
+ from streamlit.emojis import ALL_EMOJIS
54
+
45
55
  return text.replace("\U0000FE0F", "") in ALL_EMOJIS
46
56
 
47
57
 
48
- def validate_emoji(maybe_emoji: Optional[str]) -> str:
58
+ def validate_emoji(maybe_emoji: str | None) -> str:
49
59
  if maybe_emoji is None:
50
60
  return ""
61
+
51
62
  elif is_emoji(maybe_emoji):
52
63
  return maybe_emoji
53
64
  else:
@@ -60,6 +71,15 @@ def extract_leading_emoji(text: str) -> Tuple[str, str]:
60
71
  """Return a tuple containing the first emoji found in the given string and
61
72
  the rest of the string (minus an optional separator between the two).
62
73
  """
74
+
75
+ if not _contains_special_chars(text):
76
+ # If the string only contains basic alphanumerical chars and/or
77
+ # underscores, hyphen & whitespaces, then it's guaranteed that there
78
+ # is no emoji in the string.
79
+ return "", text
80
+
81
+ from streamlit.emojis import EMOJI_EXTRACTION_REGEX
82
+
63
83
  re_match = re.search(EMOJI_EXTRACTION_REGEX, text)
64
84
  if re_match is None:
65
85
  return "", text
@@ -98,7 +118,7 @@ def escape_markdown(raw_string: str) -> str:
98
118
  TEXTCHARS = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7F})
99
119
 
100
120
 
101
- def is_binary_string(inp):
121
+ def is_binary_string(inp: bytes) -> bool:
102
122
  """Guess if an input bytesarray can be encoded as a string."""
103
123
  # From https://stackoverflow.com/a/7392391
104
124
  return bool(inp.translate(None, TEXTCHARS))