streamlit 1.50.0__py3-none-any.whl → 1.51.0__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 -1
- streamlit/commands/navigation.py +4 -6
- streamlit/commands/page_config.py +4 -6
- streamlit/components/v2/__init__.py +458 -0
- streamlit/components/v2/bidi_component/__init__.py +20 -0
- streamlit/components/v2/bidi_component/constants.py +29 -0
- streamlit/components/v2/bidi_component/main.py +386 -0
- streamlit/components/v2/bidi_component/serialization.py +265 -0
- streamlit/components/v2/bidi_component/state.py +92 -0
- streamlit/components/v2/component_definition_resolver.py +143 -0
- streamlit/components/v2/component_file_watcher.py +403 -0
- streamlit/components/v2/component_manager.py +431 -0
- streamlit/components/v2/component_manifest_handler.py +122 -0
- streamlit/components/v2/component_path_utils.py +245 -0
- streamlit/components/v2/component_registry.py +409 -0
- streamlit/components/v2/get_bidi_component_manager.py +51 -0
- streamlit/components/v2/manifest_scanner.py +615 -0
- streamlit/components/v2/presentation.py +198 -0
- streamlit/components/v2/types.py +324 -0
- streamlit/config.py +456 -53
- streamlit/config_option.py +4 -1
- streamlit/config_util.py +650 -1
- streamlit/dataframe_util.py +15 -8
- streamlit/delta_generator.py +6 -4
- streamlit/delta_generator_singletons.py +3 -1
- streamlit/deprecation_util.py +17 -6
- streamlit/elements/arrow.py +37 -9
- streamlit/elements/deck_gl_json_chart.py +97 -39
- streamlit/elements/dialog_decorator.py +2 -1
- streamlit/elements/exception.py +3 -1
- streamlit/elements/graphviz_chart.py +1 -3
- streamlit/elements/heading.py +3 -5
- streamlit/elements/image.py +2 -4
- streamlit/elements/layouts.py +31 -11
- streamlit/elements/lib/built_in_chart_utils.py +1 -3
- streamlit/elements/lib/color_util.py +8 -18
- streamlit/elements/lib/column_config_utils.py +4 -8
- streamlit/elements/lib/column_types.py +40 -12
- streamlit/elements/lib/dialog.py +2 -2
- streamlit/elements/lib/image_utils.py +3 -5
- streamlit/elements/lib/layout_utils.py +50 -13
- streamlit/elements/lib/mutable_status_container.py +2 -2
- streamlit/elements/lib/options_selector_utils.py +2 -2
- streamlit/elements/lib/utils.py +4 -4
- streamlit/elements/map.py +80 -37
- streamlit/elements/media.py +5 -7
- streamlit/elements/metric.py +3 -5
- streamlit/elements/pdf.py +2 -4
- streamlit/elements/plotly_chart.py +125 -17
- streamlit/elements/progress.py +2 -4
- streamlit/elements/space.py +113 -0
- streamlit/elements/vega_charts.py +339 -148
- streamlit/elements/widgets/audio_input.py +5 -5
- streamlit/elements/widgets/button.py +2 -4
- streamlit/elements/widgets/button_group.py +33 -7
- streamlit/elements/widgets/camera_input.py +2 -4
- streamlit/elements/widgets/chat.py +7 -1
- streamlit/elements/widgets/color_picker.py +1 -1
- streamlit/elements/widgets/data_editor.py +28 -24
- streamlit/elements/widgets/file_uploader.py +5 -10
- streamlit/elements/widgets/multiselect.py +4 -3
- streamlit/elements/widgets/number_input.py +2 -4
- streamlit/elements/widgets/radio.py +10 -3
- streamlit/elements/widgets/select_slider.py +8 -5
- streamlit/elements/widgets/selectbox.py +6 -3
- streamlit/elements/widgets/slider.py +38 -42
- streamlit/elements/widgets/time_widgets.py +6 -12
- streamlit/elements/write.py +27 -6
- streamlit/emojis.py +1 -1
- streamlit/errors.py +115 -0
- streamlit/hello/hello.py +8 -0
- streamlit/hello/utils.py +2 -1
- streamlit/material_icon_names.py +1 -1
- streamlit/navigation/page.py +4 -1
- streamlit/proto/ArrowData_pb2.py +27 -0
- streamlit/proto/ArrowData_pb2.pyi +46 -0
- streamlit/proto/BidiComponent_pb2.py +34 -0
- streamlit/proto/BidiComponent_pb2.pyi +153 -0
- streamlit/proto/Block_pb2.py +7 -7
- streamlit/proto/Block_pb2.pyi +4 -1
- streamlit/proto/DeckGlJsonChart_pb2.py +10 -4
- streamlit/proto/DeckGlJsonChart_pb2.pyi +9 -3
- streamlit/proto/Element_pb2.py +5 -3
- streamlit/proto/Element_pb2.pyi +14 -4
- streamlit/proto/HeightConfig_pb2.py +2 -2
- streamlit/proto/HeightConfig_pb2.pyi +6 -3
- streamlit/proto/NewSession_pb2.py +18 -18
- streamlit/proto/NewSession_pb2.pyi +25 -6
- streamlit/proto/PlotlyChart_pb2.py +8 -6
- streamlit/proto/PlotlyChart_pb2.pyi +3 -1
- streamlit/proto/Space_pb2.py +27 -0
- streamlit/proto/Space_pb2.pyi +42 -0
- streamlit/proto/WidgetStates_pb2.py +2 -2
- streamlit/proto/WidgetStates_pb2.pyi +13 -3
- streamlit/proto/WidthConfig_pb2.py +2 -2
- streamlit/proto/WidthConfig_pb2.pyi +6 -3
- streamlit/runtime/app_session.py +27 -1
- streamlit/runtime/caching/cache_data_api.py +4 -4
- streamlit/runtime/caching/cache_errors.py +4 -1
- streamlit/runtime/caching/cache_resource_api.py +3 -2
- streamlit/runtime/caching/cache_utils.py +2 -1
- streamlit/runtime/caching/cached_message_replay.py +3 -3
- streamlit/runtime/caching/hashing.py +3 -4
- streamlit/runtime/caching/legacy_cache_api.py +2 -1
- streamlit/runtime/connection_factory.py +1 -3
- streamlit/runtime/forward_msg_queue.py +4 -1
- streamlit/runtime/fragment.py +2 -1
- streamlit/runtime/memory_media_file_storage.py +1 -1
- streamlit/runtime/metrics_util.py +6 -2
- streamlit/runtime/runtime.py +14 -0
- streamlit/runtime/scriptrunner/exec_code.py +2 -1
- streamlit/runtime/scriptrunner/script_runner.py +2 -2
- streamlit/runtime/scriptrunner_utils/script_run_context.py +3 -6
- streamlit/runtime/secrets.py +2 -4
- streamlit/runtime/session_manager.py +3 -1
- streamlit/runtime/state/common.py +30 -5
- streamlit/runtime/state/presentation.py +85 -0
- streamlit/runtime/state/safe_session_state.py +2 -2
- streamlit/runtime/state/session_state.py +220 -16
- streamlit/runtime/state/widgets.py +19 -3
- streamlit/runtime/websocket_session_manager.py +3 -1
- streamlit/source_util.py +2 -2
- streamlit/static/index.html +2 -2
- streamlit/static/manifest.json +243 -226
- streamlit/static/static/css/{index.CIiu7Ygf.css → index.BpABIXK9.css} +1 -1
- streamlit/static/static/css/index.DgR7E2CV.css +1 -0
- streamlit/static/static/js/{ErrorOutline.esm.DUpR0_Ka.js → ErrorOutline.esm.YoJdlW1p.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.CN4j9-1w.js → FileDownload.esm.Ddx8VEYy.js} +1 -1
- streamlit/static/static/js/{FileHelper.CaIUKG91.js → FileHelper.90EtOmj9.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.DTcdrasw.js → FormClearHelper.BB1Km6eP.js} +1 -1
- streamlit/static/static/js/InputInstructions.jhH15PqV.js +1 -0
- streamlit/static/static/js/{Particles.CElH0XX2.js → Particles.DUsputn1.js} +1 -1
- streamlit/static/static/js/{ProgressBar.DetlP5aY.js → ProgressBar.DLY8H6nE.js} +1 -1
- streamlit/static/static/js/{Toolbar.C77ar7rq.js → Toolbar.D8nHCkuz.js} +1 -1
- streamlit/static/static/js/{base-input.BQft14La.js → base-input.CJGiNqed.js} +3 -3
- streamlit/static/static/js/{checkbox.yZOfXCeX.js → checkbox.Cpdd482O.js} +1 -1
- streamlit/static/static/js/{createSuper.Dh9w1cs8.js → createSuper.CuQIogbW.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.DcuHuCyW.js → data-grid-overlay-editor.2Ufgxc6y.js} +1 -1
- streamlit/static/static/js/{downloader.MeHtkq8r.js → downloader.CN0K7xlu.js} +1 -1
- streamlit/static/static/js/{es6.VpBPGCnM.js → es6.BJcsVXQ0.js} +2 -2
- streamlit/static/static/js/{iframeResizer.contentWindow.yMw_ARIL.js → iframeResizer.contentWindow.XzUvQqcZ.js} +1 -1
- streamlit/static/static/js/index.B1ZQh4P1.js +1 -0
- streamlit/static/static/js/index.BKstZk0M.js +27 -0
- streamlit/static/static/js/{index.Cnpi3o3E.js → index.BMcFsUee.js} +1 -1
- streamlit/static/static/js/{index.DKv_lNO7.js → index.BR-IdcTb.js} +1 -1
- streamlit/static/static/js/{index.FFOzOWzC.js → index.B_dWA3vd.js} +1 -1
- streamlit/static/static/js/{index.Bj9JgOEC.js → index.BgnZEMVh.js} +1 -1
- streamlit/static/static/js/{index.Bxz2yX3P.js → index.BohqXifI.js} +1 -1
- streamlit/static/static/js/{index.Dbe-Q3C-.js → index.Br5nxKNj.js} +1 -1
- streamlit/static/static/js/{index.BjCwMzj4.js → index.BrIKVbNc.js} +2 -2
- streamlit/static/static/js/index.BtWUPzle.js +1 -0
- streamlit/static/static/js/{index.CGYqqs6j.js → index.C0RLraek.js} +1 -1
- streamlit/static/static/js/{index.D2QEXQq_.js → index.CAIjskgG.js} +1 -1
- streamlit/static/static/js/{index.6xX1278W.js → index.CAj-7vWz.js} +131 -157
- streamlit/static/static/js/{index.DK7hD7_w.js → index.CMtEit2O.js} +1 -1
- streamlit/static/static/js/{index.DNLrMXgm.js → index.CkRlykEE.js} +1 -1
- streamlit/static/static/js/{index.ClELlchS.js → index.CmN3FXfI.js} +1 -1
- streamlit/static/static/js/{index.GRUzrudl.js → index.CwbFI1_-.js} +1 -1
- streamlit/static/static/js/{index.Ctn27_AE.js → index.CxIUUfab.js} +27 -27
- streamlit/static/static/js/index.D2KPNy7e.js +1 -0
- streamlit/static/static/js/{index.B0H9IXUJ.js → index.D3GPA5k4.js} +3 -3
- streamlit/static/static/js/{index.BycLveZ4.js → index.DGAh7DMq.js} +1 -1
- streamlit/static/static/js/index.DKb_NvmG.js +197 -0
- streamlit/static/static/js/{index.BPQo7BKk.js → index.DMqgUYKq.js} +1 -1
- streamlit/static/static/js/{index.CH1tqnSs.js → index.DOFlg3dS.js} +1 -1
- streamlit/static/static/js/{index.64ejlaaT.js → index.DPUXkcQL.js} +1 -1
- streamlit/static/static/js/{index.B-hiXRzw.js → index.DX1xY89g.js} +1 -1
- streamlit/static/static/js/index.DYATBCsq.js +2 -0
- streamlit/static/static/js/{index.DHh-U0dK.js → index.DaSmGJ76.js} +3 -3
- streamlit/static/static/js/{index.DuxqVQpd.js → index.Dd7bMeLP.js} +1 -1
- streamlit/static/static/js/{index.B4cAbHP6.js → index.DjmmgI5U.js} +1 -1
- streamlit/static/static/js/{index.DcPNYEUo.js → index.Dq56CyM2.js} +1 -1
- streamlit/static/static/js/{index.CiAQIz1H.js → index.DuiXaS5_.js} +1 -1
- streamlit/static/static/js/index.DvFidMLe.js +2 -0
- streamlit/static/static/js/{index.C9BdUqTi.js → index.DwkhC5Pc.js} +1 -1
- streamlit/static/static/js/{index.B4dUQfni.js → index.Q-3sFn1v.js} +1 -1
- streamlit/static/static/js/{index.CMItVsFA.js → index.QJ5QO9sJ.js} +1 -1
- streamlit/static/static/js/{index.CTBk8Vk2.js → index.VwTaeety.js} +1 -1
- streamlit/static/static/js/{index.Ck8rQ9OL.js → index.YOqQbeX8.js} +1 -1
- streamlit/static/static/js/{input.s6pjQ49A.js → input.D4MN_FzN.js} +1 -1
- streamlit/static/static/js/{memory.Cuvsdfrl.js → memory.DrZjtdGT.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.DdgVR5m3.js → number-overlay-editor.DRwAw1In.js} +1 -1
- streamlit/static/static/js/{possibleConstructorReturn.CqidKeei.js → possibleConstructorReturn.exeeJQEP.js} +1 -1
- streamlit/static/static/js/record.B-tDciZb.js +1 -0
- streamlit/static/static/js/{sandbox.CCQREcJx.js → sandbox.ClO3IuUr.js} +1 -1
- streamlit/static/static/js/{timepicker.mkJF97Bb.js → timepicker.DAhu-vcF.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.De7I7KVR.js → toConsumableArray.DNbljYEC.js} +1 -1
- streamlit/static/static/js/{uniqueId.RI1LJdtz.js → uniqueId.oG4Gvj1v.js} +1 -1
- streamlit/static/static/js/{useBasicWidgetState.CedkNjUW.js → useBasicWidgetState.D6sOH6oI.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.Ca7w8dVs.js → useTextInputAutoExpand.4u3_GcuN.js} +1 -1
- streamlit/static/static/js/{useUpdateUiValue.DeXelfRH.js → useUpdateUiValue.F2R3eTeR.js} +1 -1
- streamlit/static/static/js/wavesurfer.esm.vI8Eid4k.js +73 -0
- streamlit/static/static/js/{withFullScreenWrapper.C3561XxJ.js → withFullScreenWrapper.zothJIsI.js} +1 -1
- streamlit/static/static/media/MaterialSymbols-Rounded.C7IFxh57.woff2 +0 -0
- streamlit/string_util.py +1 -3
- streamlit/testing/v1/app_test.py +2 -2
- streamlit/testing/v1/element_tree.py +23 -9
- streamlit/testing/v1/util.py +2 -2
- streamlit/type_util.py +3 -4
- streamlit/url_util.py +1 -3
- streamlit/user_info.py +1 -2
- streamlit/util.py +3 -1
- streamlit/watcher/event_based_path_watcher.py +23 -12
- streamlit/watcher/local_sources_watcher.py +11 -1
- streamlit/watcher/path_watcher.py +9 -6
- streamlit/watcher/polling_path_watcher.py +4 -1
- streamlit/watcher/util.py +2 -2
- streamlit/web/cli.py +51 -22
- streamlit/web/server/bidi_component_request_handler.py +193 -0
- streamlit/web/server/component_file_utils.py +97 -0
- streamlit/web/server/component_request_handler.py +8 -21
- streamlit/web/server/oidc_mixin.py +3 -1
- streamlit/web/server/routes.py +2 -2
- streamlit/web/server/server.py +9 -0
- streamlit/web/server/server_util.py +3 -1
- streamlit/web/server/upload_file_request_handler.py +3 -1
- {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/METADATA +4 -5
- {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/RECORD +222 -194
- streamlit/static/static/css/index.CHEnSPGk.css +0 -1
- streamlit/static/static/js/Hooks.BRba_Own.js +0 -1
- streamlit/static/static/js/InputInstructions.xnSDuYeQ.js +0 -1
- streamlit/static/static/js/index.Baqa90pe.js +0 -2
- streamlit/static/static/js/index.Bm3VbPB5.js +0 -1
- streamlit/static/static/js/index.CFMf5_ez.js +0 -197
- streamlit/static/static/js/index.Cj7DSzVR.js +0 -73
- streamlit/static/static/js/index.DH71Ezyj.js +0 -1
- streamlit/static/static/js/index.DW0Grddz.js +0 -1
- streamlit/static/static/media/MaterialSymbols-Rounded.DeCZgS-4.woff2 +0 -0
- {streamlit-1.50.0.data → streamlit-1.51.0.data}/scripts/streamlit.cmd +0 -0
- {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/WHEEL +0 -0
- {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/entry_points.txt +0 -0
- {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
|
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
|
+
"""Security-hardened path utilities for Component v2.
|
|
16
|
+
|
|
17
|
+
This module centralizes path validation and resolution logic used by the
|
|
18
|
+
Component v2 registration and file access code paths. All helpers here are
|
|
19
|
+
designed to prevent path traversal, insecure prefix checks, and symlink escapes
|
|
20
|
+
outside of a declared package root.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Final
|
|
28
|
+
|
|
29
|
+
from streamlit.errors import StreamlitComponentRegistryError
|
|
30
|
+
from streamlit.logger import get_logger
|
|
31
|
+
|
|
32
|
+
_LOGGER: Final = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ComponentPathUtils:
|
|
36
|
+
"""Utility class for component path operations and security validation."""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def has_glob_characters(path: str) -> bool:
|
|
40
|
+
"""Check if a path contains glob pattern characters.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
path : str
|
|
45
|
+
The path to check
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
bool
|
|
50
|
+
True if the path contains glob characters
|
|
51
|
+
"""
|
|
52
|
+
return any(char in path for char in ["*", "?", "[", "]"])
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def validate_path_security(path: str) -> None:
|
|
56
|
+
"""Validate that a path doesn't contain security vulnerabilities.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
path : str
|
|
61
|
+
The path to validate
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
StreamlitComponentRegistryError
|
|
66
|
+
If the path contains security vulnerabilities like path traversal attempts
|
|
67
|
+
"""
|
|
68
|
+
ComponentPathUtils._assert_relative_no_traversal(path, label="component paths")
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def resolve_glob_pattern(pattern: str, package_root: Path) -> Path:
|
|
72
|
+
"""Resolve a glob pattern to a single file path with security checks.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
pattern : str
|
|
77
|
+
The glob pattern to resolve
|
|
78
|
+
package_root : Path
|
|
79
|
+
The package root directory for security validation
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
Path
|
|
84
|
+
The resolved file path
|
|
85
|
+
|
|
86
|
+
Raises
|
|
87
|
+
------
|
|
88
|
+
StreamlitComponentRegistryError
|
|
89
|
+
If zero or more than one file matches the pattern, or if security
|
|
90
|
+
checks fail (path traversal attempts)
|
|
91
|
+
"""
|
|
92
|
+
# Ensure pattern is relative and doesn't contain path traversal attempts
|
|
93
|
+
ComponentPathUtils._assert_relative_no_traversal(pattern, label="glob patterns")
|
|
94
|
+
|
|
95
|
+
# Use glob from the package root so subdirectory patterns are handled correctly
|
|
96
|
+
matching_files = list(package_root.glob(pattern))
|
|
97
|
+
|
|
98
|
+
# Ensure all matched files are within package_root (security check)
|
|
99
|
+
validated_files = []
|
|
100
|
+
for file_path in matching_files:
|
|
101
|
+
try:
|
|
102
|
+
# Resolve to absolute path and check if it's within package_root
|
|
103
|
+
resolved_path = file_path.resolve()
|
|
104
|
+
package_root_resolved = package_root.resolve()
|
|
105
|
+
|
|
106
|
+
# Check if the resolved path is within the package root using
|
|
107
|
+
# pathlib's relative path check to avoid prefix-matching issues
|
|
108
|
+
if not resolved_path.is_relative_to(package_root_resolved):
|
|
109
|
+
_LOGGER.warning(
|
|
110
|
+
"Skipping file outside package root: %s", resolved_path
|
|
111
|
+
)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
validated_files.append(resolved_path)
|
|
115
|
+
except (OSError, ValueError) as e:
|
|
116
|
+
_LOGGER.warning("Failed to resolve path %s: %s", file_path, e)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Ensure exactly one file matches
|
|
120
|
+
if len(validated_files) == 0:
|
|
121
|
+
raise StreamlitComponentRegistryError(
|
|
122
|
+
f"No files found matching pattern '{pattern}' in package root {package_root}"
|
|
123
|
+
)
|
|
124
|
+
if len(validated_files) > 1:
|
|
125
|
+
file_list = ", ".join(str(f) for f in validated_files)
|
|
126
|
+
raise StreamlitComponentRegistryError(
|
|
127
|
+
f"Multiple files found matching pattern '{pattern}': {file_list}. "
|
|
128
|
+
"Exactly one file must match the pattern."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return Path(validated_files[0])
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _assert_relative_no_traversal(path: str, *, label: str) -> None:
|
|
135
|
+
"""Raise if ``path`` is absolute or contains ``..`` segments.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
path : str
|
|
140
|
+
Path string to validate.
|
|
141
|
+
label : str
|
|
142
|
+
Human-readable label used in error messages (e.g., "component paths").
|
|
143
|
+
"""
|
|
144
|
+
# Absolute path checks (POSIX, Windows drive-letter, UNC)
|
|
145
|
+
is_windows_drive_abs = (
|
|
146
|
+
len(path) >= 3
|
|
147
|
+
and path[0].isalpha()
|
|
148
|
+
and path[1] == ":"
|
|
149
|
+
and path[2] in ("/", "\\")
|
|
150
|
+
)
|
|
151
|
+
is_unc_abs = path.startswith("\\\\")
|
|
152
|
+
|
|
153
|
+
# Consider rooted backslash paths "\\dir" as absolute on Windows-like inputs
|
|
154
|
+
is_rooted_backslash = path.startswith("\\") and not is_unc_abs
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
os.path.isabs(path)
|
|
158
|
+
or is_windows_drive_abs
|
|
159
|
+
or is_unc_abs
|
|
160
|
+
or is_rooted_backslash
|
|
161
|
+
):
|
|
162
|
+
raise StreamlitComponentRegistryError(
|
|
163
|
+
f"Absolute paths are not allowed in {label}: {path}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Segment-based traversal detection to avoid false positives (e.g. "file..js")
|
|
167
|
+
normalized = path.replace("\\", "/")
|
|
168
|
+
segments = [seg for seg in normalized.split("/") if seg != ""]
|
|
169
|
+
if any(seg == ".." for seg in segments):
|
|
170
|
+
raise StreamlitComponentRegistryError(
|
|
171
|
+
f"Path traversal attempts are not allowed in {label}: {path}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def ensure_within_root(abs_path: Path, root: Path, *, kind: str) -> None:
|
|
176
|
+
"""Ensure that abs_path is within root; raise if not.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
abs_path : Path
|
|
181
|
+
Absolute file path
|
|
182
|
+
root : Path
|
|
183
|
+
Root directory path
|
|
184
|
+
kind : str
|
|
185
|
+
Human-readable descriptor for error messages (e.g., "js" or "css")
|
|
186
|
+
|
|
187
|
+
Raises
|
|
188
|
+
------
|
|
189
|
+
StreamlitComponentRegistryError
|
|
190
|
+
If the path cannot be resolved or if the resolved path does not
|
|
191
|
+
reside within ``root`` after following symlinks.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
resolved = abs_path.resolve()
|
|
195
|
+
root_resolved = root.resolve()
|
|
196
|
+
except Exception as e:
|
|
197
|
+
raise StreamlitComponentRegistryError(
|
|
198
|
+
f"Failed to resolve {kind} path '{abs_path}': {e}"
|
|
199
|
+
) from e
|
|
200
|
+
|
|
201
|
+
# Use Path.is_relative_to to avoid insecure prefix-based checks
|
|
202
|
+
if not resolved.is_relative_to(root_resolved):
|
|
203
|
+
raise StreamlitComponentRegistryError(
|
|
204
|
+
f"{kind} path '{abs_path}' is outside the declared asset_dir '{root}'."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def looks_like_inline_content(value: str) -> bool:
|
|
209
|
+
r"""Heuristic to detect inline JS/CSS content strings.
|
|
210
|
+
|
|
211
|
+
Treat a string as a file path ONLY if it looks path-like:
|
|
212
|
+
- Does not contain newlines
|
|
213
|
+
- Contains glob characters (*, ?, [, ])
|
|
214
|
+
- Starts with ./, /, or \
|
|
215
|
+
- Contains a path separator ("/" or "\\")
|
|
216
|
+
- Or ends with a common asset extension like .js, .mjs, .cjs, or .css
|
|
217
|
+
|
|
218
|
+
Otherwise, treat it as inline content.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
value : str
|
|
223
|
+
The string to classify as inline content or a file path.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
bool
|
|
228
|
+
True if ``value`` looks like inline content; False if it looks like a
|
|
229
|
+
file path.
|
|
230
|
+
"""
|
|
231
|
+
s = value.strip()
|
|
232
|
+
# If the value contains newlines, it's definitely inline content
|
|
233
|
+
if "\n" in s or "\r" in s:
|
|
234
|
+
return True
|
|
235
|
+
# Glob patterns indicate path-like
|
|
236
|
+
if ComponentPathUtils.has_glob_characters(s):
|
|
237
|
+
return False
|
|
238
|
+
# Obvious path prefixes
|
|
239
|
+
if s.startswith(("./", "/", "\\")):
|
|
240
|
+
return False
|
|
241
|
+
# Any path separator
|
|
242
|
+
if "/" in s or "\\" in s:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
return not (s.lower().endswith((".js", ".css", ".mjs", ".cjs")))
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
|
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
|
+
|
|
16
|
+
"""Component registry for Custom Components v2.
|
|
17
|
+
|
|
18
|
+
This module defines the data model and in-memory registry for Custom Components
|
|
19
|
+
v2. During development, component assets (JS/CSS/HTML) may change on disk as
|
|
20
|
+
build tools produce new outputs.
|
|
21
|
+
|
|
22
|
+
See Also
|
|
23
|
+
--------
|
|
24
|
+
- :class:`streamlit.components.v2.component_file_watcher.ComponentFileWatcher`
|
|
25
|
+
for directory watching and change notifications.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
import threading
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
34
|
+
|
|
35
|
+
from streamlit.components.v2.component_path_utils import ComponentPathUtils
|
|
36
|
+
from streamlit.errors import StreamlitComponentRegistryError
|
|
37
|
+
from streamlit.logger import get_logger
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from collections.abc import MutableMapping
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_LOGGER: Final = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class BidiComponentDefinition:
|
|
48
|
+
"""Definition of a bidirectional component V2.
|
|
49
|
+
|
|
50
|
+
The definition holds inline content or file references for HTML, CSS, and
|
|
51
|
+
JavaScript, plus metadata used by the runtime to serve assets. When CSS/JS
|
|
52
|
+
are provided as file paths, their asset-dir-relative URLs are exposed via
|
|
53
|
+
``css_url`` and ``js_url`` (or can be overridden with
|
|
54
|
+
``css_asset_relative_path``/``js_asset_relative_path``).
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
name : str
|
|
59
|
+
A short, descriptive name for the component.
|
|
60
|
+
html : str or None, optional
|
|
61
|
+
HTML content as a string.
|
|
62
|
+
css : str or None, optional
|
|
63
|
+
Inline CSS content or an absolute/relative path to a ``.css`` file.
|
|
64
|
+
Relative paths are interpreted as asset-dir-relative and validated to
|
|
65
|
+
reside within the component's ``asset_dir``. Absolute paths are rejected
|
|
66
|
+
by the API.
|
|
67
|
+
js : str or None, optional
|
|
68
|
+
Inline JavaScript content or an absolute/relative path to a ``.js``
|
|
69
|
+
file. Relative paths are interpreted as asset-dir-relative and validated
|
|
70
|
+
to reside within the component's ``asset_dir``. Absolute paths are
|
|
71
|
+
rejected by the API.
|
|
72
|
+
css_asset_relative_path : str or None, optional
|
|
73
|
+
Asset-dir-relative URL path to use when serving the CSS file. If not
|
|
74
|
+
provided, the filename from ``css`` is used when ``css`` is file-backed.
|
|
75
|
+
js_asset_relative_path : str or None, optional
|
|
76
|
+
Asset-dir-relative URL path to use when serving the JS file. If not
|
|
77
|
+
provided, the filename from ``js`` is used when ``js`` is file-backed.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
name: str
|
|
81
|
+
html: str | None = None
|
|
82
|
+
css: str | None = None
|
|
83
|
+
js: str | None = None
|
|
84
|
+
# Store processed content and metadata
|
|
85
|
+
_has_css_path: bool = field(default=False, init=False, repr=False)
|
|
86
|
+
_has_js_path: bool = field(default=False, init=False, repr=False)
|
|
87
|
+
_source_paths: dict[str, str] = field(default_factory=dict, init=False, repr=False)
|
|
88
|
+
# Asset-dir-relative paths used for frontend loading. These represent the
|
|
89
|
+
# URL path segment under the component's declared asset_dir (e.g. "build/index.js")
|
|
90
|
+
# and are independent of the on-disk absolute file path stored in css/js.
|
|
91
|
+
css_asset_relative_path: str | None = None
|
|
92
|
+
js_asset_relative_path: str | None = None
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
# Keep track of source paths for content loaded from files
|
|
96
|
+
source_paths = {}
|
|
97
|
+
|
|
98
|
+
# Store CSS and JS paths if provided
|
|
99
|
+
is_css_path, css_path = self._is_file_path(self.css)
|
|
100
|
+
is_js_path, js_path = self._is_file_path(self.js)
|
|
101
|
+
|
|
102
|
+
if css_path:
|
|
103
|
+
source_paths["css"] = os.path.dirname(css_path)
|
|
104
|
+
if js_path:
|
|
105
|
+
source_paths["js"] = os.path.dirname(js_path)
|
|
106
|
+
|
|
107
|
+
object.__setattr__(self, "_has_css_path", is_css_path)
|
|
108
|
+
object.__setattr__(self, "_has_js_path", is_js_path)
|
|
109
|
+
object.__setattr__(self, "_source_paths", source_paths)
|
|
110
|
+
|
|
111
|
+
# Allow empty definitions to support manifest-registered components that
|
|
112
|
+
# declare only an asset sandbox (asset_dir) without inline or file-backed
|
|
113
|
+
# entry content. Runtime API calls can later provide js/css/html.
|
|
114
|
+
|
|
115
|
+
def _is_file_path(self, content: str | None) -> tuple[bool, str | None]:
|
|
116
|
+
"""Determine whether ``content`` is a filesystem path and resolve it.
|
|
117
|
+
|
|
118
|
+
For string inputs that look like paths (contain separators, prefixes, or
|
|
119
|
+
have common asset extensions), values are normally provided by the v2
|
|
120
|
+
public API, which resolves and validates asset-dir-relative inputs and
|
|
121
|
+
passes absolute paths here. When this dataclass is constructed
|
|
122
|
+
internally, callers must supply already-resolved absolute paths that
|
|
123
|
+
have passed the same validation rules upstream. Relative paths are not
|
|
124
|
+
accepted here.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
content : str or None
|
|
129
|
+
The potential inline content or path.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
tuple[bool, str | None]
|
|
134
|
+
``(is_path, abs_path)`` where ``is_path`` indicates whether the
|
|
135
|
+
input was treated as a path and ``abs_path`` is the resolved
|
|
136
|
+
absolute path if a path, otherwise ``None``.
|
|
137
|
+
|
|
138
|
+
Raises
|
|
139
|
+
------
|
|
140
|
+
ValueError
|
|
141
|
+
If ``content`` is treated as a path but the file does not exist, or
|
|
142
|
+
if a non-absolute, path-like string is provided.
|
|
143
|
+
"""
|
|
144
|
+
if content is None:
|
|
145
|
+
return False, None
|
|
146
|
+
|
|
147
|
+
# Determine if it's a file path or inline content for strings
|
|
148
|
+
if isinstance(content, str):
|
|
149
|
+
stripped = content.strip()
|
|
150
|
+
is_likely_path = not ComponentPathUtils.looks_like_inline_content(stripped)
|
|
151
|
+
|
|
152
|
+
if is_likely_path:
|
|
153
|
+
if os.path.isabs(content):
|
|
154
|
+
abs_path = content
|
|
155
|
+
if not os.path.exists(abs_path):
|
|
156
|
+
raise ValueError(f"File does not exist: {abs_path}")
|
|
157
|
+
return True, abs_path
|
|
158
|
+
# Relative, path-like strings are not accepted at this layer.
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"Relative file paths are not accepted in BidiComponentDefinition; "
|
|
161
|
+
"pass absolute, pre-validated paths from the v2 API."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# If we get here, it's content, not a path
|
|
165
|
+
return False, None
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def css_url(self) -> str | None:
|
|
169
|
+
"""Return the asset-dir-relative URL path for CSS when file-backed.
|
|
170
|
+
|
|
171
|
+
When present, servers construct
|
|
172
|
+
``/_stcore/bidi-components/<component>/<css_url>`` using this value. If
|
|
173
|
+
``css_asset_relative_path`` is specified, it takes precedence over the
|
|
174
|
+
filename derived from ``css``.
|
|
175
|
+
"""
|
|
176
|
+
return self._derive_asset_url(
|
|
177
|
+
has_path=self._has_css_path,
|
|
178
|
+
value=self.css,
|
|
179
|
+
override=self.css_asset_relative_path,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def js_url(self) -> str | None:
|
|
184
|
+
"""Return the asset-dir-relative URL path for JS when file-backed.
|
|
185
|
+
|
|
186
|
+
When present, servers construct
|
|
187
|
+
``/_stcore/bidi-components/<component>/<js_url>`` using this value. If
|
|
188
|
+
``js_asset_relative_path`` is specified, it takes precedence over the
|
|
189
|
+
filename derived from ``js``.
|
|
190
|
+
"""
|
|
191
|
+
return self._derive_asset_url(
|
|
192
|
+
has_path=self._has_js_path,
|
|
193
|
+
value=self.js,
|
|
194
|
+
override=self.js_asset_relative_path,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def _derive_asset_url(
|
|
198
|
+
self, *, has_path: bool, value: str | None, override: str | None
|
|
199
|
+
) -> str | None:
|
|
200
|
+
"""Compute asset-dir-relative URL for a file-backed asset.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
has_path
|
|
205
|
+
Whether the value refers to a file path.
|
|
206
|
+
value
|
|
207
|
+
The css/js field value (inline string or path).
|
|
208
|
+
override
|
|
209
|
+
Optional explicit asset-dir-relative override.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
str or None
|
|
214
|
+
The derived URL path or ``None`` if not file-backed.
|
|
215
|
+
"""
|
|
216
|
+
if not has_path:
|
|
217
|
+
return None
|
|
218
|
+
# Prefer explicit URL override if provided (relative to asset_dir)
|
|
219
|
+
if override:
|
|
220
|
+
return override
|
|
221
|
+
# Fallback: preserve relative subpath if the provided path is relative;
|
|
222
|
+
# otherwise default to the basename for absolute paths. Normalize
|
|
223
|
+
# leading "./" to avoid awkward prefixes in URLs.
|
|
224
|
+
path_str = str(value)
|
|
225
|
+
if os.path.isabs(path_str):
|
|
226
|
+
return os.path.basename(path_str)
|
|
227
|
+
norm = path_str.replace("\\", "/").removeprefix("./")
|
|
228
|
+
# If there's a subpath remaining, preserve it; otherwise use basename
|
|
229
|
+
return norm if "/" in norm else os.path.basename(norm)
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def css_content(self) -> str | None:
|
|
233
|
+
"""Return inline CSS content or ``None`` if file-backed or missing."""
|
|
234
|
+
if self._has_css_path or self.css is None:
|
|
235
|
+
return None
|
|
236
|
+
# Return as string if it's not a path
|
|
237
|
+
return str(self.css)
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def js_content(self) -> str | None:
|
|
241
|
+
"""Return inline JavaScript content or ``None`` if file-backed or missing."""
|
|
242
|
+
if self._has_js_path or self.js is None:
|
|
243
|
+
return None
|
|
244
|
+
# Return as string if it's not a path
|
|
245
|
+
return str(self.js)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def html_content(self) -> str | None:
|
|
249
|
+
"""Return inline HTML content or ``None`` if not provided."""
|
|
250
|
+
return self.html
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def source_paths(self) -> dict[str, str]:
|
|
254
|
+
"""Return source directories for file-backed CSS/JS content.
|
|
255
|
+
|
|
256
|
+
The returned mapping contains keys like ``"js"`` and ``"css"`` with the
|
|
257
|
+
directory path from which each was loaded.
|
|
258
|
+
"""
|
|
259
|
+
return self._source_paths
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class BidiComponentRegistry:
|
|
263
|
+
"""Registry for bidirectional components V2.
|
|
264
|
+
|
|
265
|
+
The registry stores and updates :class:`BidiComponentDefinition` instances in
|
|
266
|
+
a thread-safe mapping guarded by a lock.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def __init__(self) -> None:
|
|
270
|
+
"""Initialize the component registry with an empty, thread-safe store."""
|
|
271
|
+
self._components: MutableMapping[str, BidiComponentDefinition] = {}
|
|
272
|
+
self._lock = threading.Lock()
|
|
273
|
+
|
|
274
|
+
def register_components_from_definitions(
|
|
275
|
+
self, component_definitions: dict[str, dict[str, Any]]
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Register components from processed definition data.
|
|
278
|
+
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
281
|
+
component_definitions : dict[str, dict[str, Any]]
|
|
282
|
+
Mapping from component identifier to definition data.
|
|
283
|
+
"""
|
|
284
|
+
with self._lock:
|
|
285
|
+
# Register all component definitions
|
|
286
|
+
for comp_name, comp_def_data in component_definitions.items():
|
|
287
|
+
# Validate required keys and gracefully handle optional ones.
|
|
288
|
+
name = comp_def_data.get("name")
|
|
289
|
+
if not name:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Component definition for key '{comp_name}' is missing required 'name' field"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
definition = BidiComponentDefinition(
|
|
295
|
+
name=name,
|
|
296
|
+
js=comp_def_data.get("js"),
|
|
297
|
+
css=comp_def_data.get("css"),
|
|
298
|
+
html=comp_def_data.get("html"),
|
|
299
|
+
css_asset_relative_path=comp_def_data.get(
|
|
300
|
+
"css_asset_relative_path"
|
|
301
|
+
),
|
|
302
|
+
js_asset_relative_path=comp_def_data.get("js_asset_relative_path"),
|
|
303
|
+
)
|
|
304
|
+
self._components[comp_name] = definition
|
|
305
|
+
_LOGGER.debug(
|
|
306
|
+
"Registered component %s from processed definitions", comp_name
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def register(self, definition: BidiComponentDefinition) -> None:
|
|
310
|
+
"""Register or overwrite a component definition by name.
|
|
311
|
+
|
|
312
|
+
This method is the primary entry point for adding a component to the
|
|
313
|
+
registry. It is used when a component is first declared via the public
|
|
314
|
+
API (e.g., ``st.components.v2.component``).
|
|
315
|
+
|
|
316
|
+
If a component with the same name already exists (e.g., a placeholder
|
|
317
|
+
from a manifest scan), it is overwritten. A warning is logged if the
|
|
318
|
+
new definition differs from the old one to alert developers of
|
|
319
|
+
potential conflicts.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
definition : BidiComponentDefinition
|
|
324
|
+
The component definition to store.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
# Register the definition
|
|
328
|
+
with self._lock:
|
|
329
|
+
name = definition.name
|
|
330
|
+
if name in self._components:
|
|
331
|
+
existing_definition = self._components[name]
|
|
332
|
+
if existing_definition != definition:
|
|
333
|
+
_LOGGER.warning(
|
|
334
|
+
"Component %s is already registered. Overwriting "
|
|
335
|
+
"previous definition. This may lead to unexpected behavior "
|
|
336
|
+
"if different modules register the same component name with "
|
|
337
|
+
"different definitions.",
|
|
338
|
+
name,
|
|
339
|
+
)
|
|
340
|
+
self._components[name] = definition
|
|
341
|
+
_LOGGER.debug("Registered component %s", name)
|
|
342
|
+
|
|
343
|
+
def get(self, name: str) -> BidiComponentDefinition | None:
|
|
344
|
+
"""Return a component definition by name, or ``None`` if not found.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
name : str
|
|
349
|
+
Component name to retrieve.
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
BidiComponentDefinition or None
|
|
354
|
+
The component definition if present, otherwise ``None``.
|
|
355
|
+
"""
|
|
356
|
+
with self._lock:
|
|
357
|
+
return self._components.get(name)
|
|
358
|
+
|
|
359
|
+
def unregister(self, name: str) -> None:
|
|
360
|
+
"""Remove a component definition from the registry.
|
|
361
|
+
|
|
362
|
+
Primarily useful for tests and dynamic scenarios.
|
|
363
|
+
|
|
364
|
+
Parameters
|
|
365
|
+
----------
|
|
366
|
+
name : str
|
|
367
|
+
Component name to unregister.
|
|
368
|
+
"""
|
|
369
|
+
with self._lock:
|
|
370
|
+
if name in self._components:
|
|
371
|
+
del self._components[name]
|
|
372
|
+
_LOGGER.debug("Unregistered component %s", name)
|
|
373
|
+
|
|
374
|
+
def clear(self) -> None:
|
|
375
|
+
"""Clear all component definitions from the registry."""
|
|
376
|
+
with self._lock:
|
|
377
|
+
self._components.clear()
|
|
378
|
+
_LOGGER.debug("Cleared all components from registry")
|
|
379
|
+
|
|
380
|
+
def update_component(self, definition: BidiComponentDefinition) -> None:
|
|
381
|
+
"""Update (replace) a stored component definition by name.
|
|
382
|
+
|
|
383
|
+
This method provides a stricter way to update a component definition
|
|
384
|
+
and is used for internal processes like file-watcher updates. Unlike
|
|
385
|
+
``register``, it will raise an error if the component is not already
|
|
386
|
+
present in the registry.
|
|
387
|
+
|
|
388
|
+
This ensures that background processes can only modify components that
|
|
389
|
+
have been explicitly defined in the current session, preventing race
|
|
390
|
+
conditions or unexpected behavior where a file-watcher event might try
|
|
391
|
+
to update a component that has since been unregistered.
|
|
392
|
+
|
|
393
|
+
Callers must supply a fully validated :class:`BidiComponentDefinition`.
|
|
394
|
+
The registry replaces the stored definition under ``definition.name`` in
|
|
395
|
+
a thread-safe manner.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
definition : BidiComponentDefinition
|
|
400
|
+
The fully-resolved component definition to store.
|
|
401
|
+
"""
|
|
402
|
+
with self._lock:
|
|
403
|
+
name = definition.name
|
|
404
|
+
if name not in self._components:
|
|
405
|
+
raise StreamlitComponentRegistryError(
|
|
406
|
+
f"Cannot update unregistered component: {name}"
|
|
407
|
+
)
|
|
408
|
+
self._components[name] = definition
|
|
409
|
+
_LOGGER.debug("Updated component definition for %s", name)
|