streamlit-nightly 1.32.3.dev20240326__py2.py3-none-any.whl → 1.32.3.dev20240328__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/__init__.py +4 -2
- streamlit/components/v1/__init__.py +3 -17
- streamlit/components/v1/{custom_component.py → components.py} +159 -11
- streamlit/delta_generator.py +3 -0
- streamlit/elements/widgets/time_widgets.py +5 -21
- streamlit/errors.py +0 -6
- streamlit/proto/AutoRerun_pb2.py +25 -0
- streamlit/proto/AutoRerun_pb2.pyi +48 -0
- streamlit/proto/ClientState_pb2.py +3 -3
- streamlit/proto/ClientState_pb2.pyi +4 -1
- streamlit/proto/Delta_pb2.py +2 -2
- streamlit/proto/Delta_pb2.pyi +4 -1
- streamlit/proto/ForwardMsg_pb2.py +10 -9
- streamlit/proto/ForwardMsg_pb2.pyi +12 -3
- streamlit/proto/NewSession_pb2.py +24 -24
- streamlit/proto/NewSession_pb2.pyi +8 -1
- streamlit/proto/PageProfile_pb2.py +6 -6
- streamlit/proto/PageProfile_pb2.pyi +4 -1
- streamlit/runtime/app_session.py +67 -24
- streamlit/runtime/caching/cache_data_api.py +3 -3
- streamlit/runtime/caching/cache_resource_api.py +2 -2
- streamlit/runtime/fragment.py +239 -0
- streamlit/runtime/metrics_util.py +17 -9
- streamlit/runtime/runtime.py +6 -12
- streamlit/runtime/scriptrunner/script_requests.py +53 -37
- streamlit/runtime/scriptrunner/script_run_context.py +15 -2
- streamlit/runtime/scriptrunner/script_runner.py +63 -14
- streamlit/runtime/state/common.py +2 -0
- streamlit/runtime/state/session_state.py +51 -7
- streamlit/runtime/state/widgets.py +10 -2
- streamlit/static/asset-manifest.json +19 -19
- streamlit/static/index.html +1 -1
- streamlit/static/static/js/1074.73973756.chunk.js +1 -0
- streamlit/static/static/js/1451.3b0a3e31.chunk.js +1 -0
- streamlit/static/static/js/1792.b8efa879.chunk.js +1 -0
- streamlit/static/static/js/{3092.3d4df25e.chunk.js → 3092.ad569cc8.chunk.js} +1 -1
- streamlit/static/static/js/3513.e3e7300a.chunk.js +1 -0
- streamlit/static/static/js/4177.69f9f18d.chunk.js +1 -0
- streamlit/static/static/js/4319.a6745434.chunk.js +1 -0
- streamlit/static/static/js/{4477.2555c11a.chunk.js → 4477.e10e4373.chunk.js} +1 -1
- streamlit/static/static/js/{4666.99f3abc3.chunk.js → 4666.b694c5a9.chunk.js} +1 -1
- streamlit/static/static/js/5106.44f0ff51.chunk.js +1 -0
- streamlit/static/static/js/5379.6571574f.chunk.js +1 -0
- streamlit/static/static/js/6013.8e80e091.chunk.js +1 -0
- streamlit/static/static/js/6718.802da17e.chunk.js +1 -0
- streamlit/static/static/js/7175.be4076bc.chunk.js +1 -0
- streamlit/static/static/js/{7602.f0420392.chunk.js → 7602.6175e969.chunk.js} +1 -1
- streamlit/static/static/js/{8492.e6dab83f.chunk.js → 8492.f56c9d4c.chunk.js} +1 -1
- streamlit/static/static/js/8691.9ccf7f89.chunk.js +1 -0
- streamlit/static/static/js/main.722453f0.js +2 -0
- streamlit/testing/v1/local_script_runner.py +2 -0
- streamlit/time_util.py +88 -0
- streamlit/web/server/component_request_handler.py +2 -2
- streamlit/web/server/server.py +2 -1
- {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/RECORD +61 -63
- streamlit/components/lib/__init__.py +0 -13
- streamlit/components/lib/local_component_registry.py +0 -82
- streamlit/components/types/__init__.py +0 -13
- streamlit/components/types/base_component_registry.py +0 -98
- streamlit/components/types/base_custom_component.py +0 -137
- streamlit/components/v1/component_registry.py +0 -103
- streamlit/static/static/js/1074.71719df6.chunk.js +0 -1
- streamlit/static/static/js/1451.e3be1711.chunk.js +0 -1
- streamlit/static/static/js/1792.16c16498.chunk.js +0 -1
- streamlit/static/static/js/3513.57cff89c.chunk.js +0 -1
- streamlit/static/static/js/4177.ab9a7aa1.chunk.js +0 -1
- streamlit/static/static/js/4319.213fc321.chunk.js +0 -1
- streamlit/static/static/js/5106.22187bfc.chunk.js +0 -1
- streamlit/static/static/js/5379.e466522d.chunk.js +0 -1
- streamlit/static/static/js/6013.75c92264.chunk.js +0 -1
- streamlit/static/static/js/6718.97945fc6.chunk.js +0 -1
- streamlit/static/static/js/7175.8c1b4d38.chunk.js +0 -1
- streamlit/static/static/js/8691.24a5792f.chunk.js +0 -1
- streamlit/static/static/js/main.7fde7092.js +0 -2
- /streamlit/static/static/js/{main.7fde7092.js.LICENSE.txt → main.722453f0.js.LICENSE.txt} +0 -0
- {streamlit_nightly-1.32.3.dev20240326.data → streamlit_nightly-1.32.3.dev20240328.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/top_level.txt +0 -0
@@ -15,7 +15,7 @@ from streamlit.proto import AppPage_pb2 as streamlit_dot_proto_dot_AppPage__pb2
|
|
15
15
|
from streamlit.proto import SessionStatus_pb2 as streamlit_dot_proto_dot_SessionStatus__pb2
|
16
16
|
|
17
17
|
|
18
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n streamlit/proto/NewSession.proto\x1a\x1dstreamlit/proto/AppPage.proto\x1a#streamlit/proto/SessionStatus.proto\"\
|
18
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n streamlit/proto/NewSession.proto\x1a\x1dstreamlit/proto/AppPage.proto\x1a#streamlit/proto/SessionStatus.proto\"\x8b\x02\n\nNewSession\x12\x1f\n\ninitialize\x18\x01 \x01(\x0b\x32\x0b.Initialize\x12\x15\n\rscript_run_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x18\n\x10main_script_path\x18\x04 \x01(\t\x12\x17\n\x06\x63onfig\x18\x06 \x01(\x0b\x32\x07.Config\x12(\n\x0c\x63ustom_theme\x18\x07 \x01(\x0b\x32\x12.CustomThemeConfig\x12\x1b\n\tapp_pages\x18\x08 \x03(\x0b\x32\x08.AppPage\x12\x18\n\x10page_script_hash\x18\t \x01(\t\x12\x1d\n\x15\x66ragment_ids_this_run\x18\n \x03(\tJ\x04\x08\x05\x10\x06\"\xba\x01\n\nInitialize\x12\x1c\n\tuser_info\x18\x01 \x01(\x0b\x32\t.UserInfo\x12*\n\x10\x65nvironment_info\x18\x03 \x01(\x0b\x32\x10.EnvironmentInfo\x12&\n\x0esession_status\x18\x04 \x01(\x0b\x32\x0e.SessionStatus\x12\x14\n\x0c\x63ommand_line\x18\x05 \x01(\t\x12\x12\n\nsession_id\x18\x06 \x01(\t\x12\x10\n\x08is_hello\x18\x07 \x01(\x08\"\x97\x02\n\x06\x43onfig\x12\x1a\n\x12gather_usage_stats\x18\x02 \x01(\x08\x12\x1e\n\x16max_cached_message_age\x18\x03 \x01(\x05\x12\x14\n\x0cmapbox_token\x18\x04 \x01(\t\x12\x19\n\x11\x61llow_run_on_save\x18\x05 \x01(\x08\x12\x14\n\x0chide_top_bar\x18\x06 \x01(\x08\x12\x18\n\x10hide_sidebar_nav\x18\x07 \x01(\x08\x12)\n\x0ctoolbar_mode\x18\x08 \x01(\x0e\x32\x13.Config.ToolbarMode\"?\n\x0bToolbarMode\x12\x08\n\x04\x41UTO\x10\x00\x12\r\n\tDEVELOPER\x10\x01\x12\n\n\x06VIEWER\x10\x02\x12\x0b\n\x07MINIMAL\x10\x03J\x04\x08\x01\x10\x02\"\x8c\x04\n\x11\x43ustomThemeConfig\x12\x15\n\rprimary_color\x18\x01 \x01(\t\x12\"\n\x1asecondary_background_color\x18\x02 \x01(\t\x12\x18\n\x10\x62\x61\x63kground_color\x18\x03 \x01(\t\x12\x12\n\ntext_color\x18\x04 \x01(\t\x12+\n\x04\x66ont\x18\x05 \x01(\x0e\x32\x1d.CustomThemeConfig.FontFamily\x12*\n\x04\x62\x61se\x18\x06 \x01(\x0e\x32\x1c.CustomThemeConfig.BaseTheme\x12\x1f\n\x17widget_background_color\x18\x07 \x01(\t\x12\x1b\n\x13widget_border_color\x18\x08 \x01(\t\x12\x15\n\x05radii\x18\t \x01(\x0b\x32\x06.Radii\x12\x11\n\tbody_font\x18\r \x01(\t\x12\x11\n\tcode_font\x18\x0e \x01(\t\x12\x1d\n\nfont_faces\x18\x0f \x03(\x0b\x32\t.FontFace\x12\x1e\n\nfont_sizes\x18\x10 \x01(\x0b\x32\n.FontSizes\x12!\n\x19skeleton_background_color\x18\x11 \x01(\t\" \n\tBaseTheme\x12\t\n\x05LIGHT\x10\x00\x12\x08\n\x04\x44\x41RK\x10\x01\"6\n\nFontFamily\x12\x0e\n\nSANS_SERIF\x10\x00\x12\t\n\x05SERIF\x10\x01\x12\r\n\tMONOSPACE\x10\x02\"F\n\x08\x46ontFace\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0e\n\x06\x66\x61mily\x18\x02 \x01(\t\x12\x0e\n\x06weight\x18\x03 \x01(\x05\x12\r\n\x05style\x18\x04 \x01(\t\"<\n\x05Radii\x12\x1a\n\x12\x62\x61se_widget_radius\x18\x01 \x01(\x05\x12\x17\n\x0f\x63heckbox_radius\x18\x02 \x01(\x05\"T\n\tFontSizes\x12\x16\n\x0etiny_font_size\x18\x01 \x01(\x05\x12\x17\n\x0fsmall_font_size\x18\x02 \x01(\x05\x12\x16\n\x0e\x62\x61se_font_size\x18\x03 \x01(\x05\"E\n\x08UserInfo\x12\x17\n\x0finstallation_id\x18\x01 \x01(\t\x12\x1a\n\x12installation_id_v3\x18\x05 \x01(\tJ\x04\x08\x02\x10\x03\"D\n\x0f\x45nvironmentInfo\x12\x19\n\x11streamlit_version\x18\x01 \x01(\t\x12\x16\n\x0epython_version\x18\x02 \x01(\tB/\n\x1c\x63om.snowflake.apps.streamlitB\x0fNewSessionProtob\x06proto3')
|
19
19
|
|
20
20
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
21
21
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'streamlit.proto.NewSession_pb2', globals())
|
@@ -24,27 +24,27 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
|
24
24
|
DESCRIPTOR._options = None
|
25
25
|
DESCRIPTOR._serialized_options = b'\n\034com.snowflake.apps.streamlitB\017NewSessionProto'
|
26
26
|
_NEWSESSION._serialized_start=105
|
27
|
-
_NEWSESSION._serialized_end=
|
28
|
-
_INITIALIZE._serialized_start=
|
29
|
-
_INITIALIZE._serialized_end=
|
30
|
-
_CONFIG._serialized_start=
|
31
|
-
_CONFIG._serialized_end=
|
32
|
-
_CONFIG_TOOLBARMODE._serialized_start=
|
33
|
-
_CONFIG_TOOLBARMODE._serialized_end=
|
34
|
-
_CUSTOMTHEMECONFIG._serialized_start=
|
35
|
-
_CUSTOMTHEMECONFIG._serialized_end=
|
36
|
-
_CUSTOMTHEMECONFIG_BASETHEME._serialized_start=
|
37
|
-
_CUSTOMTHEMECONFIG_BASETHEME._serialized_end=
|
38
|
-
_CUSTOMTHEMECONFIG_FONTFAMILY._serialized_start=
|
39
|
-
_CUSTOMTHEMECONFIG_FONTFAMILY._serialized_end=
|
40
|
-
_FONTFACE._serialized_start=
|
41
|
-
_FONTFACE._serialized_end=
|
42
|
-
_RADII._serialized_start=
|
43
|
-
_RADII._serialized_end=
|
44
|
-
_FONTSIZES._serialized_start=
|
45
|
-
_FONTSIZES._serialized_end=
|
46
|
-
_USERINFO._serialized_start=
|
47
|
-
_USERINFO._serialized_end=
|
48
|
-
_ENVIRONMENTINFO._serialized_start=
|
49
|
-
_ENVIRONMENTINFO._serialized_end=
|
27
|
+
_NEWSESSION._serialized_end=372
|
28
|
+
_INITIALIZE._serialized_start=375
|
29
|
+
_INITIALIZE._serialized_end=561
|
30
|
+
_CONFIG._serialized_start=564
|
31
|
+
_CONFIG._serialized_end=843
|
32
|
+
_CONFIG_TOOLBARMODE._serialized_start=774
|
33
|
+
_CONFIG_TOOLBARMODE._serialized_end=837
|
34
|
+
_CUSTOMTHEMECONFIG._serialized_start=846
|
35
|
+
_CUSTOMTHEMECONFIG._serialized_end=1370
|
36
|
+
_CUSTOMTHEMECONFIG_BASETHEME._serialized_start=1282
|
37
|
+
_CUSTOMTHEMECONFIG_BASETHEME._serialized_end=1314
|
38
|
+
_CUSTOMTHEMECONFIG_FONTFAMILY._serialized_start=1316
|
39
|
+
_CUSTOMTHEMECONFIG_FONTFAMILY._serialized_end=1370
|
40
|
+
_FONTFACE._serialized_start=1372
|
41
|
+
_FONTFACE._serialized_end=1442
|
42
|
+
_RADII._serialized_start=1444
|
43
|
+
_RADII._serialized_end=1504
|
44
|
+
_FONTSIZES._serialized_start=1506
|
45
|
+
_FONTSIZES._serialized_end=1590
|
46
|
+
_USERINFO._serialized_start=1592
|
47
|
+
_USERINFO._serialized_end=1661
|
48
|
+
_ENVIRONMENTINFO._serialized_start=1663
|
49
|
+
_ENVIRONMENTINFO._serialized_end=1731
|
50
50
|
# @@protoc_insertion_point(module_scope)
|
@@ -52,6 +52,7 @@ class NewSession(google.protobuf.message.Message):
|
|
52
52
|
CUSTOM_THEME_FIELD_NUMBER: builtins.int
|
53
53
|
APP_PAGES_FIELD_NUMBER: builtins.int
|
54
54
|
PAGE_SCRIPT_HASH_FIELD_NUMBER: builtins.int
|
55
|
+
FRAGMENT_IDS_THIS_RUN_FIELD_NUMBER: builtins.int
|
55
56
|
@property
|
56
57
|
def initialize(self) -> global___Initialize:
|
57
58
|
"""Initialization data. This data does *not* change from rerun to rerun,
|
@@ -82,6 +83,11 @@ class NewSession(google.protobuf.message.Message):
|
|
82
83
|
"""A list of all of this app's pages, in order and including the main page."""
|
83
84
|
page_script_hash: builtins.str
|
84
85
|
"""A hash of the script corresponding to the page currently being viewed."""
|
86
|
+
@property
|
87
|
+
def fragment_ids_this_run(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
|
88
|
+
"""The fragment IDs being run in this session if it corresponds to a fragment
|
89
|
+
script run.
|
90
|
+
"""
|
85
91
|
def __init__(
|
86
92
|
self,
|
87
93
|
*,
|
@@ -93,9 +99,10 @@ class NewSession(google.protobuf.message.Message):
|
|
93
99
|
custom_theme: global___CustomThemeConfig | None = ...,
|
94
100
|
app_pages: collections.abc.Iterable[streamlit.proto.AppPage_pb2.AppPage] | None = ...,
|
95
101
|
page_script_hash: builtins.str = ...,
|
102
|
+
fragment_ids_this_run: collections.abc.Iterable[builtins.str] | None = ...,
|
96
103
|
) -> None: ...
|
97
104
|
def HasField(self, field_name: typing_extensions.Literal["config", b"config", "custom_theme", b"custom_theme", "initialize", b"initialize"]) -> builtins.bool: ...
|
98
|
-
def ClearField(self, field_name: typing_extensions.Literal["app_pages", b"app_pages", "config", b"config", "custom_theme", b"custom_theme", "initialize", b"initialize", "main_script_path", b"main_script_path", "name", b"name", "page_script_hash", b"page_script_hash", "script_run_id", b"script_run_id"]) -> None: ...
|
105
|
+
def ClearField(self, field_name: typing_extensions.Literal["app_pages", b"app_pages", "config", b"config", "custom_theme", b"custom_theme", "fragment_ids_this_run", b"fragment_ids_this_run", "initialize", b"initialize", "main_script_path", b"main_script_path", "name", b"name", "page_script_hash", b"page_script_hash", "script_run_id", b"script_run_id"]) -> None: ...
|
99
106
|
|
100
107
|
global___NewSession = NewSession
|
101
108
|
|
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
|
|
13
13
|
|
14
14
|
|
15
15
|
|
16
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!streamlit/proto/PageProfile.proto\"\
|
16
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!streamlit/proto/PageProfile.proto\"\xda\x01\n\x0bPageProfile\x12\x1a\n\x08\x63ommands\x18\x01 \x03(\x0b\x32\x08.Command\x12\x11\n\texec_time\x18\x02 \x01(\x03\x12\x11\n\tprep_time\x18\x03 \x01(\x03\x12\x0e\n\x06\x63onfig\x18\x05 \x03(\t\x12\x1a\n\x12uncaught_exception\x18\x06 \x01(\t\x12\x14\n\x0c\x61ttributions\x18\x07 \x03(\t\x12\n\n\x02os\x18\x08 \x01(\t\x12\x10\n\x08timezone\x18\t \x01(\t\x12\x10\n\x08headless\x18\n \x01(\x08\x12\x17\n\x0fis_fragment_run\x18\x0b \x01(\x08\"6\n\x08\x41rgument\x12\t\n\x01k\x18\x01 \x01(\t\x12\t\n\x01t\x18\x02 \x01(\t\x12\t\n\x01m\x18\x03 \x01(\t\x12\t\n\x01p\x18\x05 \x01(\x05\">\n\x07\x43ommand\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x04\x61rgs\x18\x02 \x03(\x0b\x32\t.Argument\x12\x0c\n\x04time\x18\x04 \x01(\x03\x42\x30\n\x1c\x63om.snowflake.apps.streamlitB\x10PageProfileProtob\x06proto3')
|
17
17
|
|
18
18
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
19
19
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'streamlit.proto.PageProfile_pb2', globals())
|
@@ -22,9 +22,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
|
22
22
|
DESCRIPTOR._options = None
|
23
23
|
DESCRIPTOR._serialized_options = b'\n\034com.snowflake.apps.streamlitB\020PageProfileProto'
|
24
24
|
_PAGEPROFILE._serialized_start=38
|
25
|
-
_PAGEPROFILE._serialized_end=
|
26
|
-
_ARGUMENT._serialized_start=
|
27
|
-
_ARGUMENT._serialized_end=
|
28
|
-
_COMMAND._serialized_start=
|
29
|
-
_COMMAND._serialized_end=
|
25
|
+
_PAGEPROFILE._serialized_end=256
|
26
|
+
_ARGUMENT._serialized_start=258
|
27
|
+
_ARGUMENT._serialized_end=312
|
28
|
+
_COMMAND._serialized_start=314
|
29
|
+
_COMMAND._serialized_end=376
|
30
30
|
# @@protoc_insertion_point(module_scope)
|
@@ -42,6 +42,7 @@ class PageProfile(google.protobuf.message.Message):
|
|
42
42
|
OS_FIELD_NUMBER: builtins.int
|
43
43
|
TIMEZONE_FIELD_NUMBER: builtins.int
|
44
44
|
HEADLESS_FIELD_NUMBER: builtins.int
|
45
|
+
IS_FRAGMENT_RUN_FIELD_NUMBER: builtins.int
|
45
46
|
@property
|
46
47
|
def commands(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Command]: ...
|
47
48
|
exec_time: builtins.int
|
@@ -54,6 +55,7 @@ class PageProfile(google.protobuf.message.Message):
|
|
54
55
|
os: builtins.str
|
55
56
|
timezone: builtins.str
|
56
57
|
headless: builtins.bool
|
58
|
+
is_fragment_run: builtins.bool
|
57
59
|
def __init__(
|
58
60
|
self,
|
59
61
|
*,
|
@@ -66,8 +68,9 @@ class PageProfile(google.protobuf.message.Message):
|
|
66
68
|
os: builtins.str = ...,
|
67
69
|
timezone: builtins.str = ...,
|
68
70
|
headless: builtins.bool = ...,
|
71
|
+
is_fragment_run: builtins.bool = ...,
|
69
72
|
) -> None: ...
|
70
|
-
def ClearField(self, field_name: typing_extensions.Literal["attributions", b"attributions", "commands", b"commands", "config", b"config", "exec_time", b"exec_time", "headless", b"headless", "os", b"os", "prep_time", b"prep_time", "timezone", b"timezone", "uncaught_exception", b"uncaught_exception"]) -> None: ...
|
73
|
+
def ClearField(self, field_name: typing_extensions.Literal["attributions", b"attributions", "commands", b"commands", "config", b"config", "exec_time", b"exec_time", "headless", b"headless", "is_fragment_run", b"is_fragment_run", "os", b"os", "prep_time", b"prep_time", "timezone", b"timezone", "uncaught_exception", b"uncaught_exception"]) -> None: ...
|
71
74
|
|
72
75
|
global___PageProfile = PageProfile
|
73
76
|
|
streamlit/runtime/app_session.py
CHANGED
@@ -38,6 +38,7 @@ from streamlit.proto.NewSession_pb2 import (
|
|
38
38
|
from streamlit.proto.PagesChanged_pb2 import PagesChanged
|
39
39
|
from streamlit.runtime import caching, legacy_caching
|
40
40
|
from streamlit.runtime.forward_msg_queue import ForwardMsgQueue
|
41
|
+
from streamlit.runtime.fragment import FragmentStorage, MemoryFragmentStorage
|
41
42
|
from streamlit.runtime.metrics_util import Installation
|
42
43
|
from streamlit.runtime.script_data import ScriptData
|
43
44
|
from streamlit.runtime.scriptrunner import RerunData, ScriptRunner, ScriptRunnerEvent
|
@@ -161,6 +162,8 @@ class AppSession:
|
|
161
162
|
|
162
163
|
self._debug_last_backmsg_id: str | None = None
|
163
164
|
|
165
|
+
self._fragment_storage: FragmentStorage = MemoryFragmentStorage()
|
166
|
+
|
164
167
|
_LOGGER.debug("AppSession initialized (id=%s)", self.id)
|
165
168
|
|
166
169
|
def __del__(self) -> None:
|
@@ -353,26 +356,33 @@ class AppSession:
|
|
353
356
|
return
|
354
357
|
|
355
358
|
if client_state:
|
359
|
+
fragment_id = client_state.fragment_id
|
360
|
+
|
356
361
|
rerun_data = RerunData(
|
357
362
|
client_state.query_string,
|
358
363
|
client_state.widget_states,
|
359
364
|
client_state.page_script_hash,
|
360
365
|
client_state.page_name,
|
366
|
+
fragment_id_queue=[fragment_id] if fragment_id else [],
|
361
367
|
)
|
362
368
|
else:
|
363
369
|
rerun_data = RerunData()
|
364
370
|
|
365
371
|
if self._scriptrunner is not None:
|
366
|
-
if
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
#
|
372
|
+
if (
|
373
|
+
bool(config.get_option("runner.fastReruns"))
|
374
|
+
and not rerun_data.fragment_id_queue
|
375
|
+
):
|
376
|
+
# If fastReruns is enabled and this is *not* a rerun of a fragment,
|
377
|
+
# we don't send rerun requests to our existing ScriptRunner. Instead, we
|
378
|
+
# tell it to shut down. We'll then spin up a new ScriptRunner, below, to
|
379
|
+
# handle the rerun immediately.
|
371
380
|
self._scriptrunner.request_stop()
|
372
381
|
self._scriptrunner = None
|
373
382
|
else:
|
374
|
-
# fastReruns is not enabled
|
375
|
-
#
|
383
|
+
# Either fastReruns is not enabled or this RERUN request is a request to
|
384
|
+
# run a fragment. We send our current ScriptRunner a rerun request, and
|
385
|
+
# if it's accepted, we're done.
|
376
386
|
success = self._scriptrunner.request_rerun(rerun_data)
|
377
387
|
if success:
|
378
388
|
return
|
@@ -400,6 +410,7 @@ class AppSession:
|
|
400
410
|
script_cache=self._script_cache,
|
401
411
|
initial_rerun_data=initial_rerun_data,
|
402
412
|
user_info=self._user_info,
|
413
|
+
fragment_storage=self._fragment_storage,
|
403
414
|
)
|
404
415
|
self._scriptrunner.on_event.connect(self._on_scriptrunner_event)
|
405
416
|
self._scriptrunner.start()
|
@@ -464,6 +475,7 @@ class AppSession:
|
|
464
475
|
exception: BaseException | None = None,
|
465
476
|
client_state: ClientState | None = None,
|
466
477
|
page_script_hash: str | None = None,
|
478
|
+
fragment_ids_this_run: set[str] | None = None,
|
467
479
|
) -> None:
|
468
480
|
"""Called when our ScriptRunner emits an event.
|
469
481
|
|
@@ -473,7 +485,13 @@ class AppSession:
|
|
473
485
|
"""
|
474
486
|
self._event_loop.call_soon_threadsafe(
|
475
487
|
lambda: self._handle_scriptrunner_event_on_event_loop(
|
476
|
-
sender,
|
488
|
+
sender,
|
489
|
+
event,
|
490
|
+
forward_msg,
|
491
|
+
exception,
|
492
|
+
client_state,
|
493
|
+
page_script_hash,
|
494
|
+
fragment_ids_this_run,
|
477
495
|
)
|
478
496
|
)
|
479
497
|
|
@@ -485,6 +503,7 @@ class AppSession:
|
|
485
503
|
exception: BaseException | None = None,
|
486
504
|
client_state: ClientState | None = None,
|
487
505
|
page_script_hash: str | None = None,
|
506
|
+
fragment_ids_this_run: set[str] | None = None,
|
488
507
|
) -> None:
|
489
508
|
"""Handle a ScriptRunner event.
|
490
509
|
|
@@ -515,6 +534,11 @@ class AppSession:
|
|
515
534
|
page_script_hash : str | None
|
516
535
|
A hash of the script path corresponding to the page currently being
|
517
536
|
run. Set only for the SCRIPT_STARTED event.
|
537
|
+
|
538
|
+
fragment_ids_this_run : set[str] | None
|
539
|
+
The fragment IDs of the fragments being executed in this script run. Only
|
540
|
+
set for the SCRIPT_STARTED event. If this value is falsy, this script run
|
541
|
+
must be for the full script.
|
518
542
|
"""
|
519
543
|
|
520
544
|
assert (
|
@@ -540,30 +564,43 @@ class AppSession:
|
|
540
564
|
page_script_hash is not None
|
541
565
|
), "page_script_hash must be set for the SCRIPT_STARTED event"
|
542
566
|
|
543
|
-
|
567
|
+
# When running the full script, we clear the browser ForwardMsg queue since
|
568
|
+
# anything from a previous script run that has yet to be sent to the browser
|
569
|
+
# will be overwritten. For fragment runs, however, we don't want to do this
|
570
|
+
# as the ForwardMsgs in the queue may not correspond to the running
|
571
|
+
# fragment, so dropping the messages may result in the app missing
|
572
|
+
# information.
|
573
|
+
if not fragment_ids_this_run:
|
574
|
+
self._clear_queue()
|
575
|
+
|
544
576
|
self._enqueue_forward_msg(
|
545
|
-
self._create_new_session_message(
|
577
|
+
self._create_new_session_message(
|
578
|
+
page_script_hash, fragment_ids_this_run
|
579
|
+
)
|
546
580
|
)
|
547
581
|
|
548
582
|
elif (
|
549
583
|
event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
|
550
584
|
or event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR
|
585
|
+
or event == ScriptRunnerEvent.FRAGMENT_STOPPED_WITH_SUCCESS
|
551
586
|
):
|
552
587
|
if self._state != AppSessionState.SHUTDOWN_REQUESTED:
|
553
588
|
self._state = AppSessionState.APP_NOT_RUNNING
|
554
589
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
ForwardMsg.
|
559
|
-
|
560
|
-
|
561
|
-
)
|
562
|
-
self._enqueue_forward_msg(script_finished_msg)
|
590
|
+
if event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS:
|
591
|
+
status = ForwardMsg.FINISHED_SUCCESSFULLY
|
592
|
+
elif event == ScriptRunnerEvent.FRAGMENT_STOPPED_WITH_SUCCESS:
|
593
|
+
status = ForwardMsg.FINISHED_FRAGMENT_RUN_SUCCESSFULLY
|
594
|
+
else:
|
595
|
+
status = ForwardMsg.FINISHED_WITH_COMPILE_ERROR
|
563
596
|
|
597
|
+
self._enqueue_forward_msg(self._create_script_finished_message(status))
|
564
598
|
self._debug_last_backmsg_id = None
|
565
599
|
|
566
|
-
if
|
600
|
+
if (
|
601
|
+
event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
|
602
|
+
or event == ScriptRunnerEvent.FRAGMENT_STOPPED_WITH_SUCCESS
|
603
|
+
):
|
567
604
|
# The script completed successfully: update our
|
568
605
|
# LocalSourcesWatcher to account for any source code changes
|
569
606
|
# that change which modules should be watched.
|
@@ -582,11 +619,12 @@ class AppSession:
|
|
582
619
|
self._enqueue_forward_msg(msg)
|
583
620
|
|
584
621
|
elif event == ScriptRunnerEvent.SCRIPT_STOPPED_FOR_RERUN:
|
585
|
-
script_finished_msg = self._create_script_finished_message(
|
586
|
-
ForwardMsg.FINISHED_EARLY_FOR_RERUN
|
587
|
-
)
|
588
622
|
self._state = AppSessionState.APP_NOT_RUNNING
|
589
|
-
self._enqueue_forward_msg(
|
623
|
+
self._enqueue_forward_msg(
|
624
|
+
self._create_script_finished_message(
|
625
|
+
ForwardMsg.FINISHED_EARLY_FOR_RERUN
|
626
|
+
)
|
627
|
+
)
|
590
628
|
if self._local_sources_watcher:
|
591
629
|
self._local_sources_watcher.update_watched_modules()
|
592
630
|
|
@@ -630,7 +668,9 @@ class AppSession:
|
|
630
668
|
msg.session_event.script_changed_on_disk = True
|
631
669
|
return msg
|
632
670
|
|
633
|
-
def _create_new_session_message(
|
671
|
+
def _create_new_session_message(
|
672
|
+
self, page_script_hash: str, fragment_ids_this_run: set[str] | None = None
|
673
|
+
) -> ForwardMsg:
|
634
674
|
"""Create and return a new_session ForwardMsg."""
|
635
675
|
msg = ForwardMsg()
|
636
676
|
|
@@ -639,6 +679,9 @@ class AppSession:
|
|
639
679
|
msg.new_session.main_script_path = self._script_data.main_script_path
|
640
680
|
msg.new_session.page_script_hash = page_script_hash
|
641
681
|
|
682
|
+
if fragment_ids_this_run:
|
683
|
+
msg.new_session.fragment_ids_this_run.extend(fragment_ids_this_run)
|
684
|
+
|
642
685
|
_populate_app_pages(msg.new_session, self._script_data.main_script_path)
|
643
686
|
_populate_config_msg(msg.new_session.config)
|
644
687
|
_populate_theme_msg(msg.new_session.custom_theme)
|
@@ -58,9 +58,9 @@ from streamlit.runtime.caching.storage.dummy_cache_storage import (
|
|
58
58
|
MemoryCacheStorageManager,
|
59
59
|
)
|
60
60
|
from streamlit.runtime.metrics_util import gather_metrics
|
61
|
-
from streamlit.runtime.runtime_util import duration_to_seconds
|
62
61
|
from streamlit.runtime.scriptrunner.script_run_context import get_script_run_ctx
|
63
62
|
from streamlit.runtime.stats import CacheStat, CacheStatsProvider, group_stats
|
63
|
+
from streamlit.time_util import time_to_seconds
|
64
64
|
|
65
65
|
_LOGGER: Final = get_logger(__name__)
|
66
66
|
|
@@ -154,7 +154,7 @@ class DataCaches(CacheStatsProvider):
|
|
154
154
|
If it doesn't exist, create a new one with the given params.
|
155
155
|
"""
|
156
156
|
|
157
|
-
ttl_seconds =
|
157
|
+
ttl_seconds = time_to_seconds(ttl, coerce_none_to_inf=False)
|
158
158
|
|
159
159
|
# Get the existing cache, if it exists, and validate that its params
|
160
160
|
# haven't changed.
|
@@ -254,7 +254,7 @@ class DataCaches(CacheStatsProvider):
|
|
254
254
|
CacheStorageContext.
|
255
255
|
"""
|
256
256
|
|
257
|
-
ttl_seconds =
|
257
|
+
ttl_seconds = time_to_seconds(ttl, coerce_none_to_inf=False)
|
258
258
|
|
259
259
|
cache_context = self.create_cache_storage_context(
|
260
260
|
function_key="DUMMY_KEY",
|
@@ -45,9 +45,9 @@ from streamlit.runtime.caching.cached_message_replay import (
|
|
45
45
|
)
|
46
46
|
from streamlit.runtime.caching.hashing import HashFuncsDict
|
47
47
|
from streamlit.runtime.metrics_util import gather_metrics
|
48
|
-
from streamlit.runtime.runtime_util import duration_to_seconds
|
49
48
|
from streamlit.runtime.scriptrunner.script_run_context import get_script_run_ctx
|
50
49
|
from streamlit.runtime.stats import CacheStat, CacheStatsProvider, group_stats
|
50
|
+
from streamlit.time_util import time_to_seconds
|
51
51
|
|
52
52
|
_LOGGER: Final = get_logger(__name__)
|
53
53
|
|
@@ -89,7 +89,7 @@ class ResourceCaches(CacheStatsProvider):
|
|
89
89
|
if max_entries is None:
|
90
90
|
max_entries = math.inf
|
91
91
|
|
92
|
-
ttl_seconds =
|
92
|
+
ttl_seconds = time_to_seconds(ttl)
|
93
93
|
|
94
94
|
# Get the existing cache, if it exists, and validate that its params
|
95
95
|
# haven't changed.
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
import contextlib
|
18
|
+
import hashlib
|
19
|
+
import inspect
|
20
|
+
from abc import abstractmethod
|
21
|
+
from copy import deepcopy
|
22
|
+
from datetime import timedelta
|
23
|
+
from functools import wraps
|
24
|
+
from typing import Any, Callable, Protocol, TypeVar, overload
|
25
|
+
|
26
|
+
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
27
|
+
from streamlit.runtime.metrics_util import gather_metrics
|
28
|
+
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
29
|
+
from streamlit.time_util import time_to_seconds
|
30
|
+
|
31
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
32
|
+
Fragment = Callable[[], Any]
|
33
|
+
|
34
|
+
|
35
|
+
class FragmentStorage(Protocol):
|
36
|
+
"""A key-value store for Fragments. Used to implement the @st.experimental_fragment
|
37
|
+
decorator.
|
38
|
+
|
39
|
+
We intentionally define this as its own protocol despite how generic it appears to
|
40
|
+
be at first glance. The reason why is that, in any case where fragments aren't just
|
41
|
+
stored as Python closures in memory, storing and retrieving Fragments will generally
|
42
|
+
involve serializing and deserializing function bytecode, which is a tricky aspect
|
43
|
+
to implementing FragmentStorages that won't generally appear with our other *Storage
|
44
|
+
protocols.
|
45
|
+
"""
|
46
|
+
|
47
|
+
@abstractmethod
|
48
|
+
def get(self, key: str) -> Fragment:
|
49
|
+
"""Returns the stored fragment for the given key."""
|
50
|
+
raise NotImplementedError
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def set(self, key: str, value: Fragment) -> None:
|
54
|
+
"""Saves a fragment under the given key."""
|
55
|
+
raise NotImplementedError
|
56
|
+
|
57
|
+
@abstractmethod
|
58
|
+
def delete(self, key: str) -> None:
|
59
|
+
"""Delete the fragment corresponding to the given key."""
|
60
|
+
raise NotImplementedError
|
61
|
+
|
62
|
+
@abstractmethod
|
63
|
+
def clear(self) -> None:
|
64
|
+
"""Remove all fragments saved in this FragmentStorage."""
|
65
|
+
raise NotImplementedError
|
66
|
+
|
67
|
+
|
68
|
+
# NOTE: Ideally, we'd like to add a MemoryFragmentStorageStatProvider implementation to
|
69
|
+
# keep track of memory usage due to fragments, but doing something like this ends up
|
70
|
+
# being difficult in practice as the memory usage of a closure is hard to measure (the
|
71
|
+
# vendored implementation of pympler.asizeof that we use elsewhere is unable to measure
|
72
|
+
# the size of a function).
|
73
|
+
class MemoryFragmentStorage(FragmentStorage):
|
74
|
+
"""A simple, memory-backed implementation of FragmentStorage.
|
75
|
+
|
76
|
+
MemoryFragmentStorage is just a wrapper around a plain Python dict that complies with
|
77
|
+
the FragmentStorage protocol.
|
78
|
+
"""
|
79
|
+
|
80
|
+
def __init__(self):
|
81
|
+
self._fragments: dict[str, Fragment] = {}
|
82
|
+
|
83
|
+
def get(self, key: str) -> Fragment:
|
84
|
+
return self._fragments[key]
|
85
|
+
|
86
|
+
def set(self, key: str, value: Fragment) -> None:
|
87
|
+
self._fragments[key] = value
|
88
|
+
|
89
|
+
def delete(self, key: str) -> None:
|
90
|
+
del self._fragments[key]
|
91
|
+
|
92
|
+
def clear(self) -> None:
|
93
|
+
self._fragments.clear()
|
94
|
+
|
95
|
+
|
96
|
+
@overload
|
97
|
+
def fragment(
|
98
|
+
func: F,
|
99
|
+
*,
|
100
|
+
run_every: int | float | timedelta | str | None = None,
|
101
|
+
) -> F:
|
102
|
+
...
|
103
|
+
|
104
|
+
|
105
|
+
# Support being able to pass parameters to this decorator (that is, being able to write
|
106
|
+
# `@fragment(run_every=5.0)`).
|
107
|
+
@overload
|
108
|
+
def fragment(
|
109
|
+
func: None = None,
|
110
|
+
*,
|
111
|
+
run_every: int | float | timedelta | str | None = None,
|
112
|
+
) -> Callable[[F], F]:
|
113
|
+
...
|
114
|
+
|
115
|
+
|
116
|
+
@gather_metrics("experimental_fragment")
|
117
|
+
def fragment(
|
118
|
+
func: F | None = None,
|
119
|
+
*,
|
120
|
+
run_every: int | float | timedelta | str | None = None,
|
121
|
+
) -> Callable[[F], F] | F:
|
122
|
+
"""Allow a function to be run independently of the full script.
|
123
|
+
|
124
|
+
Functions decorated with ``@st.experimental_fragment`` are handled specially within
|
125
|
+
an app: when a widget created within an invocation of the function (a fragment) is
|
126
|
+
interacted with, then only that fragment is rerun rather than the full streamlit app.
|
127
|
+
|
128
|
+
Parameters
|
129
|
+
----------
|
130
|
+
run_every: int, float, timedelta, str, or None
|
131
|
+
If set, fragments created from this function rerun periodically at the specified
|
132
|
+
time interval.
|
133
|
+
|
134
|
+
Example
|
135
|
+
-------
|
136
|
+
The following example demonstrates basic usage of ``@st.experimental_fragment``. In
|
137
|
+
this app, clicking on the "rerun full script" button will increment both counters,
|
138
|
+
but the "rerun fragment" button will only increment the counter within the fragment.
|
139
|
+
|
140
|
+
```python3
|
141
|
+
import streamlit as st
|
142
|
+
|
143
|
+
if "script_runs" not in st.session_state:
|
144
|
+
st.session_state.script_runs = 0
|
145
|
+
st.session_state.fragment_runs = 0
|
146
|
+
|
147
|
+
@st.experimental_fragment
|
148
|
+
def fragment():
|
149
|
+
st.button("rerun fragment")
|
150
|
+
st.write(f"fragment runs: {st.session_state.fragment_runs}")
|
151
|
+
st.session_state.fragment_runs += 1
|
152
|
+
|
153
|
+
fragment()
|
154
|
+
|
155
|
+
st.button("rerun full script")
|
156
|
+
st.write(f"full script runs: {st.session_state.script_runs}")
|
157
|
+
st.session_state.script_runs += 1
|
158
|
+
```
|
159
|
+
"""
|
160
|
+
|
161
|
+
if func is None:
|
162
|
+
# Support passing the params via function decorator
|
163
|
+
def wrapper(f: F) -> F:
|
164
|
+
return fragment(
|
165
|
+
func=f,
|
166
|
+
run_every=run_every,
|
167
|
+
)
|
168
|
+
|
169
|
+
return wrapper
|
170
|
+
else:
|
171
|
+
non_optional_func = func
|
172
|
+
|
173
|
+
@wraps(non_optional_func)
|
174
|
+
def wrap(*args, **kwargs):
|
175
|
+
from streamlit.delta_generator import dg_stack
|
176
|
+
|
177
|
+
ctx = get_script_run_ctx()
|
178
|
+
if ctx is None:
|
179
|
+
return
|
180
|
+
|
181
|
+
cursors_snapshot = deepcopy(ctx.cursors)
|
182
|
+
dg_stack_snapshot = deepcopy(dg_stack.get())
|
183
|
+
active_dg = dg_stack_snapshot[-1]
|
184
|
+
h = hashlib.new("md5")
|
185
|
+
h.update(
|
186
|
+
f"{non_optional_func.__module__}.{non_optional_func.__qualname__}{active_dg._get_delta_path_str()}".encode(
|
187
|
+
"utf-8"
|
188
|
+
)
|
189
|
+
)
|
190
|
+
fragment_id = h.hexdigest()
|
191
|
+
|
192
|
+
def wrapped_fragment():
|
193
|
+
import streamlit as st
|
194
|
+
|
195
|
+
# NOTE: We need to call get_script_run_ctx here again and can't just use the
|
196
|
+
# value of ctx from above captured by the closure because subsequent
|
197
|
+
# fragment runs will generally run in a new script run, thus we'll have a
|
198
|
+
# new ctx.
|
199
|
+
ctx = get_script_run_ctx(suppress_warning=True)
|
200
|
+
assert ctx is not None
|
201
|
+
|
202
|
+
if ctx.fragment_ids_this_run:
|
203
|
+
# This script run is a run of one or more fragments. We restore the
|
204
|
+
# state of ctx.cursors and dg_stack to the snapshots we took when this
|
205
|
+
# fragment was declared.
|
206
|
+
ctx.cursors = deepcopy(cursors_snapshot)
|
207
|
+
dg_stack.set(deepcopy(dg_stack_snapshot))
|
208
|
+
else:
|
209
|
+
# Otherwise, we must be in a full script run. We need to temporarily set
|
210
|
+
# ctx.current_fragment_id so that elements corresponding to this
|
211
|
+
# fragment get tagged with the appropriate ID. ctx.current_fragment_id
|
212
|
+
# gets reset after the fragment function finishes running.
|
213
|
+
ctx.current_fragment_id = fragment_id
|
214
|
+
|
215
|
+
try:
|
216
|
+
with st.container():
|
217
|
+
result = non_optional_func(*args, **kwargs)
|
218
|
+
finally:
|
219
|
+
ctx.current_fragment_id = None
|
220
|
+
|
221
|
+
return result
|
222
|
+
|
223
|
+
ctx.fragment_storage.set(fragment_id, wrapped_fragment)
|
224
|
+
|
225
|
+
if run_every:
|
226
|
+
msg = ForwardMsg()
|
227
|
+
msg.auto_rerun.interval = time_to_seconds(run_every)
|
228
|
+
msg.auto_rerun.fragment_id = fragment_id
|
229
|
+
ctx.enqueue(msg)
|
230
|
+
|
231
|
+
return wrapped_fragment()
|
232
|
+
|
233
|
+
with contextlib.suppress(AttributeError):
|
234
|
+
# Make this a well-behaved decorator by preserving important function
|
235
|
+
# attributes.
|
236
|
+
wrap.__dict__.update(non_optional_func.__dict__)
|
237
|
+
wrap.__signature__ = inspect.signature(non_optional_func) # type: ignore
|
238
|
+
|
239
|
+
return wrap
|