streamlit 1.49.1__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/column_config.py +2 -0
- streamlit/commands/navigation.py +7 -7
- streamlit/commands/page_config.py +4 -6
- streamlit/components/v1/custom_component.py +17 -42
- 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 +741 -32
- streamlit/config_option.py +4 -1
- streamlit/config_util.py +650 -1
- streamlit/connections/base_connection.py +4 -2
- streamlit/dataframe_util.py +18 -10
- streamlit/delta_generator.py +8 -7
- streamlit/delta_generator_singletons.py +3 -1
- streamlit/deprecation_util.py +17 -6
- streamlit/elements/arrow.py +90 -42
- streamlit/elements/deck_gl_json_chart.py +98 -39
- streamlit/elements/dialog_decorator.py +2 -1
- streamlit/elements/exception.py +3 -1
- streamlit/elements/form.py +6 -6
- streamlit/elements/graphviz_chart.py +24 -9
- streamlit/elements/heading.py +3 -5
- streamlit/elements/iframe.py +0 -2
- streamlit/elements/image.py +12 -13
- streamlit/elements/layouts.py +89 -22
- streamlit/elements/lib/built_in_chart_utils.py +95 -31
- streamlit/elements/lib/color_util.py +8 -18
- streamlit/elements/lib/column_config_utils.py +9 -8
- streamlit/elements/lib/column_types.py +595 -148
- streamlit/elements/lib/dialog.py +3 -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/pandas_styler_utils.py +30 -14
- streamlit/elements/lib/utils.py +21 -9
- streamlit/elements/map.py +81 -40
- streamlit/elements/media.py +7 -7
- streamlit/elements/metric.py +11 -35
- streamlit/elements/pdf.py +2 -4
- streamlit/elements/plotly_chart.py +142 -26
- streamlit/elements/progress.py +2 -4
- streamlit/elements/pyplot.py +6 -6
- streamlit/elements/space.py +113 -0
- streamlit/elements/vega_charts.py +400 -143
- streamlit/elements/widgets/audio_input.py +52 -4
- streamlit/elements/widgets/button.py +29 -29
- streamlit/elements/widgets/button_group.py +33 -6
- streamlit/elements/widgets/camera_input.py +3 -4
- streamlit/elements/widgets/chat.py +7 -0
- streamlit/elements/widgets/checkbox.py +1 -0
- streamlit/elements/widgets/color_picker.py +1 -0
- streamlit/elements/widgets/data_editor.py +34 -29
- streamlit/elements/widgets/file_uploader.py +6 -10
- streamlit/elements/widgets/multiselect.py +14 -3
- streamlit/elements/widgets/number_input.py +5 -4
- streamlit/elements/widgets/radio.py +10 -2
- streamlit/elements/widgets/select_slider.py +8 -4
- streamlit/elements/widgets/selectbox.py +9 -2
- streamlit/elements/widgets/slider.py +38 -41
- streamlit/elements/widgets/text_widgets.py +6 -0
- streamlit/elements/widgets/time_widgets.py +15 -12
- streamlit/elements/write.py +28 -23
- streamlit/emojis.py +1 -1
- streamlit/errors.py +115 -0
- streamlit/git_util.py +65 -43
- 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/Arrow_pb2.py +10 -8
- streamlit/proto/Arrow_pb2.pyi +31 -2
- streamlit/proto/AudioInput_pb2.py +2 -2
- streamlit/proto/AudioInput_pb2.pyi +6 -2
- streamlit/proto/BidiComponent_pb2.py +34 -0
- streamlit/proto/BidiComponent_pb2.pyi +153 -0
- streamlit/proto/Block_pb2.py +11 -11
- streamlit/proto/Block_pb2.pyi +9 -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 -16
- streamlit/proto/NewSession_pb2.pyi +158 -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 +45 -6
- 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/theme_util.py +148 -0
- streamlit/runtime/websocket_session_manager.py +3 -1
- streamlit/source_util.py +2 -2
- streamlit/static/index.html +2 -2
- streamlit/static/manifest.json +244 -227
- streamlit/static/static/css/{index.C8X8rNzw.css → index.BpABIXK9.css} +1 -1
- streamlit/static/static/css/index.DgR7E2CV.css +1 -0
- streamlit/static/static/js/{ErrorOutline.esm.DcGrhbBP.js → ErrorOutline.esm.YoJdlW1p.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.DgBvV6Pq.js → FileDownload.esm.Ddx8VEYy.js} +1 -1
- streamlit/static/static/js/{FileHelper.M6AAaeuA.js → FileHelper.90EtOmj9.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.DHh1GFzm.js → FormClearHelper.BB1Km6eP.js} +1 -1
- streamlit/static/static/js/InputInstructions.jhH15PqV.js +1 -0
- streamlit/static/static/js/{Particles.DDVT-6Qc.js → Particles.DUsputn1.js} +1 -1
- streamlit/static/static/js/{ProgressBar.BEY0cXXV.js → ProgressBar.DLY8H6nE.js} +2 -2
- streamlit/static/static/js/Toolbar.D8nHCkuz.js +1 -0
- streamlit/static/static/js/{base-input.CK3UVGp1.js → base-input.CJGiNqed.js} +3 -3
- streamlit/static/static/js/{checkbox.D8W881TL.js → checkbox.Cpdd482O.js} +1 -1
- streamlit/static/static/js/{createSuper.B6W-Dh9S.js → createSuper.CuQIogbW.js} +1 -1
- streamlit/static/static/js/data-grid-overlay-editor.2Ufgxc6y.js +1 -0
- streamlit/static/static/js/{downloader.DiKpuU_S.js → downloader.CN0K7xlu.js} +1 -1
- streamlit/static/static/js/{es6.B8zRNPZ-.js → es6.BJcsVXQ0.js} +2 -2
- streamlit/static/static/js/{iframeResizer.contentWindow.DIewJmmh.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.Bte_9Lyq.js → index.BMcFsUee.js} +1 -1
- streamlit/static/static/js/{index.qhs54UAB.js → index.BR-IdcTb.js} +1 -1
- streamlit/static/static/js/{index.CejBxbg1.js → index.B_dWA3vd.js} +1 -1
- streamlit/static/static/js/{index.D5naqx-J.js → index.BgnZEMVh.js} +1 -1
- streamlit/static/static/js/{index.C7fRKRs4.js → index.BohqXifI.js} +1 -1
- streamlit/static/static/js/{index.cnnXF7xQ.js → index.Br5nxKNj.js} +1 -1
- streamlit/static/static/js/index.BrIKVbNc.js +3 -0
- streamlit/static/static/js/index.BtWUPzle.js +1 -0
- streamlit/static/static/js/index.C0RLraek.js +1 -0
- streamlit/static/static/js/{index.CP5TD2z1.js → index.CAIjskgG.js} +1 -1
- streamlit/static/static/js/{index.CD8HuT3N.js → index.CAj-7vWz.js} +135 -162
- streamlit/static/static/js/{index.DtYN2x4k.js → index.CMtEit2O.js} +1 -1
- streamlit/static/static/js/index.CkRlykEE.js +12 -0
- streamlit/static/static/js/{index.Ts_0SdB9.js → index.CmN3FXfI.js} +2 -2
- streamlit/static/static/js/{index.BnEpvLEz.js → index.CwbFI1_-.js} +1 -1
- streamlit/static/static/js/{index.CcJf6BCU.js → index.CxIUUfab.js} +27 -27
- streamlit/static/static/js/index.D2KPNy7e.js +1 -0
- streamlit/static/static/js/{index.Ch7MBCx0.js → index.D3GPA5k4.js} +47 -47
- streamlit/static/static/js/{index.ho6NIXGl.js → index.DGAh7DMq.js} +1 -1
- streamlit/static/static/js/index.DKb_NvmG.js +197 -0
- streamlit/static/static/js/{index.CvYYtxD_.js → index.DMqgUYKq.js} +1 -1
- streamlit/static/static/js/{index.zecpGxtj.js → index.DOFlg3dS.js} +1 -1
- streamlit/static/static/js/{index.B9mjBcgE.js → index.DPUXkcQL.js} +1 -1
- streamlit/static/static/js/index.DX1xY89g.js +1 -0
- streamlit/static/static/js/index.DYATBCsq.js +2 -0
- streamlit/static/static/js/{index.D2-atlaQ.js → index.DaSmGJ76.js} +3 -3
- streamlit/static/static/js/index.Dd7bMeLP.js +1 -0
- streamlit/static/static/js/{index.4eF4NxG2.js → index.DjmmgI5U.js} +1 -1
- streamlit/static/static/js/index.Dq56CyM2.js +1 -0
- streamlit/static/static/js/index.DuiXaS5_.js +7 -0
- streamlit/static/static/js/index.DvFidMLe.js +2 -0
- streamlit/static/static/js/{index.452cqrrL.js → index.DwkhC5Pc.js} +1 -1
- streamlit/static/static/js/{index.Dk4C7X3i.js → index.Q-3sFn1v.js} +1 -1
- streamlit/static/static/js/{index.CjXWwH-y.js → index.QJ5QO9sJ.js} +1 -1
- streamlit/static/static/js/{index.B6U8LQo3.js → index.VwTaeety.js} +1 -1
- streamlit/static/static/js/index.YOqQbeX8.js +1 -0
- streamlit/static/static/js/{input.nzVJphXi.js → input.D4MN_FzN.js} +1 -1
- streamlit/static/static/js/{memory.CjCgTQz3.js → memory.DrZjtdGT.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.DaRFzZEO.js → number-overlay-editor.DRwAw1In.js} +1 -1
- streamlit/static/static/js/{possibleConstructorReturn.DgiPnZ9N.js → possibleConstructorReturn.exeeJQEP.js} +1 -1
- streamlit/static/static/js/record.B-tDciZb.js +1 -0
- streamlit/static/static/js/{sandbox.mithfq7Z.js → sandbox.ClO3IuUr.js} +1 -1
- streamlit/static/static/js/{timepicker.Dbl5KFh6.js → timepicker.DAhu-vcF.js} +4 -4
- streamlit/static/static/js/{toConsumableArray.D-Dx88BQ.js → toConsumableArray.DNbljYEC.js} +1 -1
- streamlit/static/static/js/{uniqueId.Bh26R_3S.js → uniqueId.oG4Gvj1v.js} +1 -1
- streamlit/static/static/js/{useBasicWidgetState.DeK-QJpD.js → useBasicWidgetState.D6sOH6oI.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.4iAdLWD-.js → useTextInputAutoExpand.4u3_GcuN.js} +2 -2
- streamlit/static/static/js/{useUpdateUiValue.CmT7_nJN.js → useUpdateUiValue.F2R3eTeR.js} +1 -1
- streamlit/static/static/js/wavesurfer.esm.vI8Eid4k.js +73 -0
- streamlit/static/static/js/withFullScreenWrapper.zothJIsI.js +1 -0
- streamlit/static/static/media/MaterialSymbols-Rounded.C7IFxh57.woff2 +0 -0
- streamlit/string_util.py +56 -1
- 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/bootstrap.py +0 -31
- 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 +18 -5
- streamlit/web/server/server.py +10 -0
- streamlit/web/server/server_util.py +3 -1
- streamlit/web/server/upload_file_request_handler.py +3 -1
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/METADATA +4 -5
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/RECORD +238 -209
- streamlit/static/static/css/index.COe1010n.css +0 -1
- streamlit/static/static/js/Hooks.DGu1od_L.js +0 -1
- streamlit/static/static/js/InputInstructions.z6sVgyYt.js +0 -1
- streamlit/static/static/js/Toolbar.DSnK1fUh.js +0 -1
- streamlit/static/static/js/data-grid-overlay-editor.DRTHOydk.js +0 -1
- streamlit/static/static/js/index.BXYmrqnf.js +0 -1
- streamlit/static/static/js/index.B_8AnktO.js +0 -1
- streamlit/static/static/js/index.Bl7zGQSh.js +0 -7
- streamlit/static/static/js/index.BnJIOYn9.js +0 -73
- streamlit/static/static/js/index.C1HcTl5K.js +0 -1
- streamlit/static/static/js/index.C7lSmSOP.js +0 -1
- streamlit/static/static/js/index.C_tmcx4B.js +0 -1
- streamlit/static/static/js/index.D3K5nOu9.js +0 -197
- streamlit/static/static/js/index.DkKT3LUI.js +0 -1
- streamlit/static/static/js/index.MTPPBDHk.js +0 -2
- streamlit/static/static/js/index.pqW9AMJD.js +0 -3
- streamlit/static/static/js/index.urHgTgMQ.js +0 -12
- streamlit/static/static/js/index.wzkv_11M.js +0 -1
- streamlit/static/static/js/index.yF5AncHY.js +0 -1
- streamlit/static/static/js/withFullScreenWrapper.DLp1ENGm.js +0 -1
- streamlit/static/static/media/MaterialSymbols-Rounded.CBxVaFdk.woff2 +0 -0
- {streamlit-1.49.1.data → streamlit-1.51.0.data}/scripts/streamlit.cmd +0 -0
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/WHEEL +0 -0
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/entry_points.txt +0 -0
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, TypedDict
|
|
18
|
+
|
|
19
|
+
from streamlit.util import AttributeDictionary
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BidiComponentState(TypedDict, total=False):
|
|
23
|
+
"""
|
|
24
|
+
The schema for the state of a bidirectional component.
|
|
25
|
+
|
|
26
|
+
The state is a flat dictionary-like object (key -> value) that supports
|
|
27
|
+
both key and attribute notation via :class:`AttributeDictionary`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Flat mapping of state key -> value
|
|
31
|
+
# (kept empty to reflect open set of keys)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BidiComponentResult(AttributeDictionary):
|
|
35
|
+
"""The schema for the custom component result object.
|
|
36
|
+
|
|
37
|
+
The custom component result object is a dictionary-like object that
|
|
38
|
+
supports both key and attribute notation. It contains all of the
|
|
39
|
+
component's state and trigger values.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
<state_keys> : Any
|
|
44
|
+
All state values from the component. State values are persistent across
|
|
45
|
+
app reruns until explicitly changed. You can have multiple state keys
|
|
46
|
+
as attributes.
|
|
47
|
+
<trigger_keys> : Any
|
|
48
|
+
All trigger values from the component. Trigger values are transient and
|
|
49
|
+
reset to ``None`` after one script run. You can have multiple trigger
|
|
50
|
+
keys as attributes.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
state_vals: dict[str, Any] | None = None,
|
|
57
|
+
trigger_vals: dict[str, Any] | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize a BidiComponentResult.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
state_vals : dict[str, Any] or None
|
|
64
|
+
A dictionary of state values from the component.
|
|
65
|
+
trigger_vals : dict[str, Any] or None
|
|
66
|
+
A dictionary of trigger values from the component.
|
|
67
|
+
"""
|
|
68
|
+
if state_vals is None:
|
|
69
|
+
state_vals = {}
|
|
70
|
+
if trigger_vals is None:
|
|
71
|
+
trigger_vals = {}
|
|
72
|
+
|
|
73
|
+
super().__init__(
|
|
74
|
+
{
|
|
75
|
+
# The order here matters, because all stateful values will
|
|
76
|
+
# always be returned, but trigger values may be transient. This
|
|
77
|
+
# mirrors presentation behavior in
|
|
78
|
+
# `make_bidi_component_presenter`.
|
|
79
|
+
**trigger_vals,
|
|
80
|
+
**state_vals,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def unwrap_component_state(raw_state: Any) -> dict[str, Any]:
|
|
86
|
+
"""Return flat mapping when given a dict; otherwise, empty dict.
|
|
87
|
+
|
|
88
|
+
The new canonical state is flat, so this is effectively an identity for
|
|
89
|
+
dict inputs and a guard for other types.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
return dict(raw_state) if isinstance(raw_state, dict) else {}
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
"""Shared resolver for building component definitions with path validation.
|
|
16
|
+
|
|
17
|
+
This module centralizes the logic for interpreting js/css inputs as inline
|
|
18
|
+
content vs path/glob strings, validating them against a component's asset
|
|
19
|
+
directory, and producing a BidiComponentDefinition with correct asset-relative
|
|
20
|
+
URLs used by the server.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
from streamlit.components.v2.component_path_utils import ComponentPathUtils
|
|
29
|
+
from streamlit.components.v2.component_registry import BidiComponentDefinition
|
|
30
|
+
from streamlit.errors import StreamlitAPIException
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from streamlit.components.v2.component_manager import BidiComponentManager
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_definition_with_validation(
|
|
37
|
+
*,
|
|
38
|
+
manager: BidiComponentManager,
|
|
39
|
+
component_key: str,
|
|
40
|
+
html: str | None,
|
|
41
|
+
css: str | None,
|
|
42
|
+
js: str | None,
|
|
43
|
+
) -> BidiComponentDefinition:
|
|
44
|
+
"""Construct a definition and validate ``js``/``css`` inputs against ``asset_dir``.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
manager : BidiComponentManager
|
|
49
|
+
Component manager used to resolve the component's ``asset_dir`` and
|
|
50
|
+
related metadata.
|
|
51
|
+
component_key : str
|
|
52
|
+
Fully-qualified name of the component to build a definition for.
|
|
53
|
+
html : str | None
|
|
54
|
+
Inline HTML content to include in the definition. If ``None``, the
|
|
55
|
+
component will not include HTML content.
|
|
56
|
+
css : str | None
|
|
57
|
+
Either inline CSS content or a path/glob to a CSS file inside the
|
|
58
|
+
component's ``asset_dir``. Inline strings are kept as-is; file-backed
|
|
59
|
+
inputs are validated and converted to an ``asset_dir``-relative URL.
|
|
60
|
+
js : str | None
|
|
61
|
+
Either inline JavaScript content or a path/glob to a JS file inside the
|
|
62
|
+
component's ``asset_dir``. Inline strings are kept as-is; file-backed
|
|
63
|
+
inputs are validated and converted to an ``asset_dir``-relative URL.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
BidiComponentDefinition
|
|
68
|
+
A component definition with inline content preserved and file-backed
|
|
69
|
+
entries resolved to absolute filesystem paths plus their
|
|
70
|
+
``asset_dir``-relative URLs.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
StreamlitAPIException
|
|
75
|
+
If a path/glob is provided but the component has no declared
|
|
76
|
+
``asset_dir``, if a glob resolves to zero or multiple files, or if any
|
|
77
|
+
resolved path escapes the declared ``asset_dir``.
|
|
78
|
+
|
|
79
|
+
Notes
|
|
80
|
+
-----
|
|
81
|
+
- Inline strings are treated as content (no manifest required).
|
|
82
|
+
- Path-like strings require the component to be declared in the package
|
|
83
|
+
manifest with an ``asset_dir``.
|
|
84
|
+
- Globs are supported only within ``asset_dir`` and must resolve to exactly
|
|
85
|
+
one file.
|
|
86
|
+
- Relative paths are resolved strictly against the component's ``asset_dir``
|
|
87
|
+
and must remain within it after resolution. Absolute paths are not
|
|
88
|
+
allowed.
|
|
89
|
+
- For file-backed entries, the URL sent to the frontend is the
|
|
90
|
+
``asset_dir``-relative path, served under
|
|
91
|
+
``/_stcore/bidi-components/<component>/<relative_path>``.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
asset_root = manager.get_component_asset_root(component_key)
|
|
95
|
+
|
|
96
|
+
def _resolve_entry(
|
|
97
|
+
value: str | None, *, kind: str
|
|
98
|
+
) -> tuple[str | None, str | None]:
|
|
99
|
+
# Inline content: None rel URL
|
|
100
|
+
if value is None:
|
|
101
|
+
return None, None
|
|
102
|
+
if ComponentPathUtils.looks_like_inline_content(value):
|
|
103
|
+
return value, None
|
|
104
|
+
|
|
105
|
+
# For path-like strings, asset_root must exist
|
|
106
|
+
if asset_root is None:
|
|
107
|
+
raise StreamlitAPIException(
|
|
108
|
+
f"Component '{component_key}' must be declared in pyproject.toml with asset_dir "
|
|
109
|
+
f"to use file-backed {kind}."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
value_str = value
|
|
113
|
+
|
|
114
|
+
# If looks like a glob, resolve strictly inside asset_root
|
|
115
|
+
if ComponentPathUtils.has_glob_characters(value_str):
|
|
116
|
+
resolved = ComponentPathUtils.resolve_glob_pattern(value_str, asset_root)
|
|
117
|
+
ComponentPathUtils.ensure_within_root(resolved, asset_root, kind=kind)
|
|
118
|
+
# Use resolved absolute paths to avoid macOS /private prefix mismatch
|
|
119
|
+
rel_url = str(
|
|
120
|
+
resolved.resolve().relative_to(asset_root.resolve()).as_posix()
|
|
121
|
+
)
|
|
122
|
+
return str(resolved), rel_url
|
|
123
|
+
|
|
124
|
+
# Concrete path: must be asset-dir-relative (reject absolute & traversal)
|
|
125
|
+
ComponentPathUtils.validate_path_security(value_str)
|
|
126
|
+
candidate = asset_root / Path(value_str)
|
|
127
|
+
ComponentPathUtils.ensure_within_root(candidate, asset_root, kind=kind)
|
|
128
|
+
resolved_candidate = candidate.resolve()
|
|
129
|
+
rel_url = str(resolved_candidate.relative_to(asset_root.resolve()).as_posix())
|
|
130
|
+
return str(resolved_candidate), rel_url
|
|
131
|
+
|
|
132
|
+
css_value, css_rel = _resolve_entry(css, kind="css")
|
|
133
|
+
js_value, js_rel = _resolve_entry(js, kind="js")
|
|
134
|
+
|
|
135
|
+
# Build definition with possible asset_dir-relative paths
|
|
136
|
+
return BidiComponentDefinition(
|
|
137
|
+
name=component_key,
|
|
138
|
+
html=html,
|
|
139
|
+
css=css_value,
|
|
140
|
+
js=js_value,
|
|
141
|
+
css_asset_relative_path=css_rel,
|
|
142
|
+
js_asset_relative_path=js_rel,
|
|
143
|
+
)
|
|
@@ -0,0 +1,403 @@
|
|
|
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 file watching utilities.
|
|
17
|
+
|
|
18
|
+
This module provides the `ComponentFileWatcher`, a utility that watches
|
|
19
|
+
component asset directories for changes and notifies a caller-provided callback
|
|
20
|
+
with the affected component names. It abstracts the underlying path-watcher
|
|
21
|
+
implementation and ensures exception-safe startup and cleanup.
|
|
22
|
+
|
|
23
|
+
Why this exists
|
|
24
|
+
---------------
|
|
25
|
+
Streamlit supports advanced Custom Components that ship a package of static
|
|
26
|
+
assets (for example, a Vite/Webpack build output). While a user develops their
|
|
27
|
+
app, those frontend files may change. The component registry for Custom
|
|
28
|
+
Components v2 must stay synchronized with the on-disk assets so that the server
|
|
29
|
+
can resolve the up-to-date files.
|
|
30
|
+
|
|
31
|
+
This watcher exists to keep the registry in sync by listening for changes in
|
|
32
|
+
component asset roots and notifying a higher-level manager that can re-resolve
|
|
33
|
+
the affected component definitions.
|
|
34
|
+
|
|
35
|
+
Notes
|
|
36
|
+
-----
|
|
37
|
+
- Watching is directory-based with a recursive glob ("**/*").
|
|
38
|
+
- Common noisy directories (e.g., ``node_modules``) are ignored in callbacks.
|
|
39
|
+
- Startup is exception-safe and does not leak partially created watchers.
|
|
40
|
+
|
|
41
|
+
See Also
|
|
42
|
+
--------
|
|
43
|
+
- :class:`streamlit.watcher.local_sources_watcher.LocalSourcesWatcher` - watches
|
|
44
|
+
app source files per session to trigger reruns.
|
|
45
|
+
- :class:`streamlit.components.v2.component_registry.BidiComponentRegistry` -
|
|
46
|
+
the server-side store of Custom Component v2 definitions that reacts to
|
|
47
|
+
watcher notifications.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import threading
|
|
53
|
+
from typing import TYPE_CHECKING, Final, Protocol, cast
|
|
54
|
+
|
|
55
|
+
from streamlit.logger import get_logger
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from collections.abc import Callable
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_LOGGER: Final = get_logger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _HasClose(Protocol):
|
|
66
|
+
def close(self) -> None: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ComponentFileWatcher:
|
|
70
|
+
"""Handle file watching for component asset directories.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
component_update_callback : Callable[[list[str]], None]
|
|
75
|
+
Callback invoked when files change under any watched directory. It
|
|
76
|
+
receives a list of component names affected by the change.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, component_update_callback: Callable[[list[str]], None]) -> None:
|
|
80
|
+
"""Initialize the file watcher.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
component_update_callback : Callable[[list[str]], None]
|
|
85
|
+
Callback function to call when components under watched roots change.
|
|
86
|
+
Signature: (affected_component_names)
|
|
87
|
+
"""
|
|
88
|
+
self._component_update_callback = component_update_callback
|
|
89
|
+
self._lock = threading.Lock()
|
|
90
|
+
|
|
91
|
+
# File watching state
|
|
92
|
+
self._watched_directories: dict[
|
|
93
|
+
str, list[str]
|
|
94
|
+
] = {} # directory -> component_names
|
|
95
|
+
self._path_watchers: list[_HasClose] = [] # Store actual watcher instances
|
|
96
|
+
self._watching_active = False
|
|
97
|
+
|
|
98
|
+
# Store asset roots to watch: component_name -> asset_root
|
|
99
|
+
self._asset_watch_roots: dict[str, Path] = {}
|
|
100
|
+
|
|
101
|
+
# Default noisy directories to ignore in callbacks
|
|
102
|
+
self._ignored_dirs: tuple[str, ...] = (
|
|
103
|
+
"__pycache__",
|
|
104
|
+
".cache",
|
|
105
|
+
".git",
|
|
106
|
+
".hg",
|
|
107
|
+
".mypy_cache",
|
|
108
|
+
".pytest_cache",
|
|
109
|
+
".ruff_cache",
|
|
110
|
+
".svn",
|
|
111
|
+
".swc",
|
|
112
|
+
".yarn",
|
|
113
|
+
"coverage",
|
|
114
|
+
"node_modules",
|
|
115
|
+
"venv",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def is_watching_active(self) -> bool:
|
|
120
|
+
"""Check if file watching is currently active.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
bool
|
|
125
|
+
True if file watching is active, False otherwise
|
|
126
|
+
"""
|
|
127
|
+
return self._watching_active
|
|
128
|
+
|
|
129
|
+
def start_file_watching(self, asset_watch_roots: dict[str, Path]) -> None:
|
|
130
|
+
"""Start file watching for asset roots.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
asset_watch_roots : dict[str, Path]
|
|
135
|
+
Mapping of component names to asset root directories to watch.
|
|
136
|
+
|
|
137
|
+
Notes
|
|
138
|
+
-----
|
|
139
|
+
The method is idempotent: it stops any active watchers first, then
|
|
140
|
+
re-initializes watchers for the provided ``asset_watch_roots``.
|
|
141
|
+
"""
|
|
142
|
+
# Always stop first to ensure a clean state, then start with the new roots.
|
|
143
|
+
# This sequencing avoids races between concurrent stop/start calls.
|
|
144
|
+
self.stop_file_watching()
|
|
145
|
+
self._start_file_watching(asset_watch_roots)
|
|
146
|
+
|
|
147
|
+
def stop_file_watching(self) -> None:
|
|
148
|
+
"""Stop file watching and clean up watchers.
|
|
149
|
+
|
|
150
|
+
Notes
|
|
151
|
+
-----
|
|
152
|
+
This method is safe to call multiple times and will no-op if
|
|
153
|
+
watching is not active.
|
|
154
|
+
"""
|
|
155
|
+
with self._lock:
|
|
156
|
+
if not self._watching_active:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Close all path watchers
|
|
160
|
+
for watcher in self._path_watchers:
|
|
161
|
+
try:
|
|
162
|
+
watcher.close()
|
|
163
|
+
except Exception: # noqa: PERF203
|
|
164
|
+
_LOGGER.exception("Failed to close path watcher")
|
|
165
|
+
|
|
166
|
+
self._path_watchers.clear()
|
|
167
|
+
self._watched_directories.clear()
|
|
168
|
+
# Also clear asset root references to avoid stale state retention
|
|
169
|
+
self._asset_watch_roots.clear()
|
|
170
|
+
self._watching_active = False
|
|
171
|
+
_LOGGER.debug("Stopped file watching for component registry")
|
|
172
|
+
|
|
173
|
+
def _start_file_watching(self, asset_watch_roots: dict[str, Path]) -> None:
|
|
174
|
+
"""Internal method to start file watching with the given roots.
|
|
175
|
+
|
|
176
|
+
This method is exception-safe: in case of failures while creating
|
|
177
|
+
watchers, any previously created watcher instances are closed and no
|
|
178
|
+
internal state is committed.
|
|
179
|
+
"""
|
|
180
|
+
with self._lock:
|
|
181
|
+
if self._watching_active:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if not asset_watch_roots:
|
|
185
|
+
_LOGGER.debug("No asset roots to watch")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
path_watcher_class = self._get_default_path_watcher_class()
|
|
190
|
+
if path_watcher_class is None:
|
|
191
|
+
# NoOp watcher; skip activation
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
directories_to_watch = self._prepare_directories_to_watch(
|
|
195
|
+
asset_watch_roots
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
new_watchers, new_watched_dirs = self._build_watchers_for_directories(
|
|
199
|
+
path_watcher_class, directories_to_watch
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Commit new watchers and state only after successful creation
|
|
203
|
+
if new_watchers:
|
|
204
|
+
self._commit_watch_state(
|
|
205
|
+
new_watchers, new_watched_dirs, asset_watch_roots
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
_LOGGER.debug("No directories were watched; staying inactive")
|
|
209
|
+
except Exception:
|
|
210
|
+
_LOGGER.exception("Failed to start file watching")
|
|
211
|
+
|
|
212
|
+
def _get_default_path_watcher_class(self) -> type | None:
|
|
213
|
+
"""Return the default path watcher class.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
type | None
|
|
218
|
+
The concrete path watcher class to instantiate, or ``None`` if
|
|
219
|
+
the NoOp watcher is configured and file watching should be
|
|
220
|
+
skipped.
|
|
221
|
+
"""
|
|
222
|
+
from streamlit.watcher.path_watcher import (
|
|
223
|
+
NoOpPathWatcher,
|
|
224
|
+
get_default_path_watcher_class,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
path_watcher_class = get_default_path_watcher_class()
|
|
228
|
+
if path_watcher_class is NoOpPathWatcher:
|
|
229
|
+
_LOGGER.debug("NoOpPathWatcher in use; skipping component file watching")
|
|
230
|
+
return None
|
|
231
|
+
return path_watcher_class
|
|
232
|
+
|
|
233
|
+
def _prepare_directories_to_watch(
|
|
234
|
+
self, asset_watch_roots: dict[str, Path]
|
|
235
|
+
) -> dict[str, list[str]]:
|
|
236
|
+
"""Build a mapping of directory to component names.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
asset_watch_roots : dict[str, Path]
|
|
241
|
+
Mapping of component names to their asset root directories.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
dict[str, list[str]]
|
|
246
|
+
A map from absolute directory path to a deduplicated list of
|
|
247
|
+
component names contained in that directory.
|
|
248
|
+
"""
|
|
249
|
+
directories_to_watch: dict[str, list[str]] = {}
|
|
250
|
+
for comp_name, root in asset_watch_roots.items():
|
|
251
|
+
directory = str(root.resolve())
|
|
252
|
+
if directory not in directories_to_watch:
|
|
253
|
+
directories_to_watch[directory] = []
|
|
254
|
+
if comp_name not in directories_to_watch[directory]:
|
|
255
|
+
directories_to_watch[directory].append(comp_name)
|
|
256
|
+
return directories_to_watch
|
|
257
|
+
|
|
258
|
+
def _build_watchers_for_directories(
|
|
259
|
+
self, path_watcher_class: type, directories_to_watch: dict[str, list[str]]
|
|
260
|
+
) -> tuple[list[_HasClose], dict[str, list[str]]]:
|
|
261
|
+
"""Create watchers for directories with rollback on failure.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
path_watcher_class : type
|
|
266
|
+
The path watcher class to instantiate for each directory.
|
|
267
|
+
directories_to_watch : dict[str, list[str]]
|
|
268
|
+
A map of directory to the associated component name list.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
tuple[list[_HasClose], dict[str, list[str]]]
|
|
273
|
+
The list of created watcher instances and the watched directory
|
|
274
|
+
mapping.
|
|
275
|
+
|
|
276
|
+
Raises
|
|
277
|
+
------
|
|
278
|
+
Exception
|
|
279
|
+
Propagates any exception during watcher creation after closing
|
|
280
|
+
already-created watchers.
|
|
281
|
+
"""
|
|
282
|
+
new_watchers: list[_HasClose] = []
|
|
283
|
+
new_watched_dirs: dict[str, list[str]] = {}
|
|
284
|
+
|
|
285
|
+
for directory, component_names in directories_to_watch.items():
|
|
286
|
+
try:
|
|
287
|
+
cb = self._make_directory_callback(tuple(component_names))
|
|
288
|
+
# Use a glob pattern that matches all files to let Streamlit's
|
|
289
|
+
# watcher handle MD5 calculation and change detection
|
|
290
|
+
watcher = path_watcher_class(
|
|
291
|
+
directory,
|
|
292
|
+
cb,
|
|
293
|
+
glob_pattern="**/*",
|
|
294
|
+
allow_nonexistent=False,
|
|
295
|
+
)
|
|
296
|
+
new_watchers.append(cast("_HasClose", watcher))
|
|
297
|
+
new_watched_dirs[directory] = component_names
|
|
298
|
+
_LOGGER.debug(
|
|
299
|
+
"Prepared watcher for directory %s (components: %s)",
|
|
300
|
+
directory,
|
|
301
|
+
component_names,
|
|
302
|
+
)
|
|
303
|
+
except Exception: # noqa: PERF203
|
|
304
|
+
# Roll back watchers created so far
|
|
305
|
+
self._rollback_watchers(new_watchers)
|
|
306
|
+
raise
|
|
307
|
+
|
|
308
|
+
return new_watchers, new_watched_dirs
|
|
309
|
+
|
|
310
|
+
def _commit_watch_state(
|
|
311
|
+
self,
|
|
312
|
+
new_watchers: list[_HasClose],
|
|
313
|
+
new_watched_dirs: dict[str, list[str]],
|
|
314
|
+
asset_watch_roots: dict[str, Path],
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Commit created watchers and mark watching active.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
new_watchers : list[_HasClose]
|
|
321
|
+
Fully initialized watcher instances.
|
|
322
|
+
new_watched_dirs : dict[str, list[str]]
|
|
323
|
+
Mapping from directory to component names.
|
|
324
|
+
asset_watch_roots : dict[str, Path]
|
|
325
|
+
The asset roots used to initialize watchers; stored for reference.
|
|
326
|
+
"""
|
|
327
|
+
self._path_watchers = new_watchers
|
|
328
|
+
self._watched_directories = new_watched_dirs
|
|
329
|
+
self._asset_watch_roots = dict(asset_watch_roots)
|
|
330
|
+
self._watching_active = True
|
|
331
|
+
_LOGGER.debug(
|
|
332
|
+
"Started file watching for %d directories", len(self._watched_directories)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def _rollback_watchers(self, watchers: list[_HasClose]) -> None:
|
|
336
|
+
"""Close any created watchers when setup fails.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
watchers : list[_HasClose]
|
|
341
|
+
Watcher instances that were successfully created before a failure.
|
|
342
|
+
"""
|
|
343
|
+
for w in watchers:
|
|
344
|
+
try:
|
|
345
|
+
w.close()
|
|
346
|
+
except Exception: # noqa: PERF203
|
|
347
|
+
_LOGGER.exception("Failed to close path watcher during rollback")
|
|
348
|
+
|
|
349
|
+
def _make_directory_callback(self, comps: tuple[str, ...]) -> Callable[[str], None]:
|
|
350
|
+
"""Create a callback for a directory watcher that captures component names."""
|
|
351
|
+
|
|
352
|
+
def callback(changed_path: str) -> None:
|
|
353
|
+
if self._is_in_ignored_directory(changed_path):
|
|
354
|
+
_LOGGER.debug("Ignoring change in noisy directory: %s", changed_path)
|
|
355
|
+
return
|
|
356
|
+
_LOGGER.debug(
|
|
357
|
+
"Directory change detected: %s, checking components: %s",
|
|
358
|
+
changed_path,
|
|
359
|
+
comps,
|
|
360
|
+
)
|
|
361
|
+
self._handle_component_change(list(comps))
|
|
362
|
+
|
|
363
|
+
return callback
|
|
364
|
+
|
|
365
|
+
def _handle_component_change(self, affected_components: list[str]) -> None:
|
|
366
|
+
"""Handle component changes for both directory and file events.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
affected_components : list[str]
|
|
371
|
+
List of component names affected by the change
|
|
372
|
+
"""
|
|
373
|
+
if not self._watching_active:
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
# Notify manager to handle re-resolution based on recorded API inputs
|
|
377
|
+
try:
|
|
378
|
+
self._component_update_callback(affected_components)
|
|
379
|
+
except Exception:
|
|
380
|
+
# Never allow exceptions from user callbacks to break watcher loops
|
|
381
|
+
_LOGGER.exception("Component update callback raised")
|
|
382
|
+
|
|
383
|
+
def _is_in_ignored_directory(self, changed_path: str) -> bool:
|
|
384
|
+
"""Return True if the changed path is inside an ignored directory.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
changed_path : str
|
|
389
|
+
The filesystem path that triggered the change event.
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
bool
|
|
394
|
+
True if the path is located inside one of the ignored directories,
|
|
395
|
+
False otherwise.
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
from pathlib import Path as _Path
|
|
399
|
+
|
|
400
|
+
parts = set(_Path(changed_path).resolve().parts)
|
|
401
|
+
return any(ignored in parts for ignored in self._ignored_dirs)
|
|
402
|
+
except Exception:
|
|
403
|
+
return False
|