streamlit-nightly 1.31.1.dev20240206__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.
- streamlit/commands/execution_control.py +5 -4
- streamlit/elements/lib/streamlit_plotly_theme.py +179 -163
- streamlit/elements/plotly_chart.py +11 -18
- streamlit/elements/widgets/button.py +8 -7
- streamlit/elements/widgets/time_widgets.py +24 -8
- streamlit/emojis.py +14 -1
- streamlit/runtime/caching/cache_resource_api.py +7 -5
- streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py +2 -3
- streamlit/runtime/legacy_caching/caching.py +5 -2
- streamlit/runtime/memory_session_storage.py +3 -3
- streamlit/runtime/scriptrunner/script_runner.py +194 -187
- streamlit/runtime/state/session_state.py +6 -2
- streamlit/string_util.py +30 -10
- streamlit/util.py +48 -1
- streamlit/watcher/event_based_path_watcher.py +26 -21
- streamlit/watcher/path_watcher.py +43 -41
- {streamlit_nightly-1.31.1.dev20240206.dist-info → streamlit_nightly-1.31.1.dev20240208.dist-info}/METADATA +1 -3
- {streamlit_nightly-1.31.1.dev20240206.dist-info → streamlit_nightly-1.31.1.dev20240208.dist-info}/RECORD +22 -22
- {streamlit_nightly-1.31.1.dev20240206.data → streamlit_nightly-1.31.1.dev20240208.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.31.1.dev20240206.dist-info → streamlit_nightly-1.31.1.dev20240208.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.31.1.dev20240206.dist-info → streamlit_nightly-1.31.1.dev20240208.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.31.1.dev20240206.dist-info → streamlit_nightly-1.31.1.dev20240208.dist-info}/top_level.txt +0 -0
@@ -16,8 +16,6 @@ from __future__ import annotations
|
|
16
16
|
import math
|
17
17
|
import threading
|
18
18
|
|
19
|
-
from cachetools import TTLCache
|
20
|
-
|
21
19
|
from streamlit.logger import get_logger
|
22
20
|
from streamlit.runtime.caching import cache_utils
|
23
21
|
from streamlit.runtime.caching.storage.cache_storage_protocol import (
|
@@ -26,6 +24,7 @@ from streamlit.runtime.caching.storage.cache_storage_protocol import (
|
|
26
24
|
CacheStorageKeyNotFoundError,
|
27
25
|
)
|
28
26
|
from streamlit.runtime.stats import CacheStat
|
27
|
+
from streamlit.util import TimedCleanupCache
|
29
28
|
|
30
29
|
_LOGGER = get_logger(__name__)
|
31
30
|
|
@@ -62,7 +61,7 @@ class InMemoryCacheStorageWrapper(CacheStorage):
|
|
62
61
|
self.function_display_name = context.function_display_name
|
63
62
|
self._ttl_seconds = context.ttl_seconds
|
64
63
|
self._max_entries = context.max_entries
|
65
|
-
self._mem_cache:
|
64
|
+
self._mem_cache: TimedCleanupCache[str, bytes] = TimedCleanupCache(
|
66
65
|
maxsize=self.max_entries,
|
67
66
|
ttl=self.ttl_seconds,
|
68
67
|
timer=cache_utils.TTLCACHE_TIMER,
|
@@ -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()
|
@@ -12,11 +12,11 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
from typing import List, MutableMapping, Optional
|
16
15
|
|
17
|
-
from
|
16
|
+
from typing import List, MutableMapping, Optional
|
18
17
|
|
19
18
|
from streamlit.runtime.session_manager import SessionInfo, SessionStorage
|
19
|
+
from streamlit.util import TimedCleanupCache
|
20
20
|
|
21
21
|
|
22
22
|
class MemorySessionStorage(SessionStorage):
|
@@ -55,7 +55,7 @@ class MemorySessionStorage(SessionStorage):
|
|
55
55
|
inaccessible and will be removed eventually.
|
56
56
|
"""
|
57
57
|
|
58
|
-
self._cache: MutableMapping[str, SessionInfo] =
|
58
|
+
self._cache: MutableMapping[str, SessionInfo] = TimedCleanupCache(
|
59
59
|
maxsize=maxsize, ttl=ttl_seconds
|
60
60
|
)
|
61
61
|
|
@@ -394,203 +394,210 @@ class ScriptRunner:
|
|
394
394
|
"""
|
395
395
|
assert self._is_in_script_thread()
|
396
396
|
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
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
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
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.
|
482
|
-
|
450
|
+
event=ScriptRunnerEvent.SCRIPT_STARTED,
|
451
|
+
page_script_hash=page_script_hash,
|
483
452
|
)
|
484
|
-
return
|
485
453
|
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
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
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
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
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
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
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
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
|
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,
|
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:
|
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))
|