streamlit-nightly 1.33.1.dev20240408__py2.py3-none-any.whl → 1.33.1.dev20240412__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.
Files changed (41) hide show
  1. streamlit/__init__.py +4 -1
  2. streamlit/components/lib/__init__.py +13 -0
  3. streamlit/components/lib/local_component_registry.py +82 -0
  4. streamlit/components/types/__init__.py +13 -0
  5. streamlit/components/types/base_component_registry.py +98 -0
  6. streamlit/components/types/base_custom_component.py +137 -0
  7. streamlit/components/v1/__init__.py +7 -3
  8. streamlit/components/v1/component_registry.py +103 -0
  9. streamlit/components/v1/components.py +9 -375
  10. streamlit/components/v1/custom_component.py +241 -0
  11. streamlit/delta_generator.py +54 -36
  12. streamlit/elements/dialog_decorator.py +166 -0
  13. streamlit/elements/image.py +5 -3
  14. streamlit/elements/layouts.py +19 -0
  15. streamlit/elements/lib/dialog.py +148 -0
  16. streamlit/errors.py +6 -0
  17. streamlit/proto/Block_pb2.py +26 -22
  18. streamlit/proto/Block_pb2.pyi +43 -3
  19. streamlit/proto/Common_pb2.py +1 -1
  20. streamlit/runtime/runtime.py +12 -0
  21. streamlit/runtime/scriptrunner/script_run_context.py +3 -0
  22. streamlit/runtime/scriptrunner/script_runner.py +16 -0
  23. streamlit/runtime/state/query_params.py +28 -11
  24. streamlit/runtime/state/query_params_proxy.py +51 -3
  25. streamlit/runtime/state/session_state.py +3 -0
  26. streamlit/static/asset-manifest.json +4 -4
  27. streamlit/static/index.html +1 -1
  28. streamlit/static/static/js/{1168.3029456a.chunk.js → 1168.1d6408e6.chunk.js} +1 -1
  29. streamlit/static/static/js/8427.d30dffe1.chunk.js +1 -0
  30. streamlit/static/static/js/main.46540eaf.js +2 -0
  31. streamlit/web/server/component_request_handler.py +2 -2
  32. streamlit/web/server/server.py +1 -2
  33. {streamlit_nightly-1.33.1.dev20240408.dist-info → streamlit_nightly-1.33.1.dev20240412.dist-info}/METADATA +1 -1
  34. {streamlit_nightly-1.33.1.dev20240408.dist-info → streamlit_nightly-1.33.1.dev20240412.dist-info}/RECORD +39 -30
  35. streamlit/static/static/js/8427.b0ed496b.chunk.js +0 -1
  36. streamlit/static/static/js/main.285df334.js +0 -2
  37. /streamlit/static/static/js/{main.285df334.js.LICENSE.txt → main.46540eaf.js.LICENSE.txt} +0 -0
  38. {streamlit_nightly-1.33.1.dev20240408.data → streamlit_nightly-1.33.1.dev20240412.data}/scripts/streamlit.cmd +0 -0
  39. {streamlit_nightly-1.33.1.dev20240408.dist-info → streamlit_nightly-1.33.1.dev20240412.dist-info}/WHEEL +0 -0
  40. {streamlit_nightly-1.33.1.dev20240408.dist-info → streamlit_nightly-1.33.1.dev20240412.dist-info}/entry_points.txt +0 -0
  41. {streamlit_nightly-1.33.1.dev20240408.dist-info → streamlit_nightly-1.33.1.dev20240412.dist-info}/top_level.txt +0 -0
@@ -12,378 +12,12 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from __future__ import annotations
16
-
17
- import inspect
18
- import json
19
- import os
20
- import threading
21
- from typing import TYPE_CHECKING, Any, Final
22
-
23
- import streamlit
24
- from streamlit import type_util, util
25
- from streamlit.elements.form import current_form_id
26
- from streamlit.errors import StreamlitAPIException
27
- from streamlit.logger import get_logger
28
- from streamlit.proto.Components_pb2 import ArrowTable as ArrowTableProto
29
- from streamlit.proto.Components_pb2 import SpecialArg
30
- from streamlit.proto.Element_pb2 import Element
31
- from streamlit.runtime.metrics_util import gather_metrics
32
- from streamlit.runtime.scriptrunner import get_script_run_ctx
33
- from streamlit.runtime.state import NoValue, register_widget
34
- from streamlit.runtime.state.common import compute_widget_id
35
- from streamlit.type_util import to_bytes
36
-
37
- if TYPE_CHECKING:
38
- from streamlit.delta_generator import DeltaGenerator
39
-
40
- _LOGGER: Final = get_logger(__name__)
41
-
42
-
43
- class MarshallComponentException(StreamlitAPIException):
44
- """Class for exceptions generated during custom component marshalling."""
45
-
46
- pass
47
-
48
-
49
- class CustomComponent:
50
- """A Custom Component declaration."""
51
-
52
- def __init__(
53
- self,
54
- name: str,
55
- path: str | None = None,
56
- url: str | None = None,
57
- ):
58
- if (path is None and url is None) or (path is not None and url is not None):
59
- raise StreamlitAPIException(
60
- "Either 'path' or 'url' must be set, but not both."
61
- )
62
-
63
- self.name = name
64
- self.path = path
65
- self.url = url
66
-
67
- def __repr__(self) -> str:
68
- return util.repr_(self)
69
-
70
- @property
71
- def abspath(self) -> str | None:
72
- """The absolute path that the component is served from."""
73
- if self.path is None:
74
- return None
75
- return os.path.abspath(self.path)
76
-
77
- def __call__(
78
- self,
79
- *args,
80
- default: Any = None,
81
- key: str | None = None,
82
- **kwargs,
83
- ) -> Any:
84
- """An alias for create_instance."""
85
- return self.create_instance(*args, default=default, key=key, **kwargs)
86
-
87
- @gather_metrics("create_instance")
88
- def create_instance(
89
- self,
90
- *args,
91
- default: Any = None,
92
- key: str | None = None,
93
- **kwargs,
94
- ) -> Any:
95
- """Create a new instance of the component.
96
-
97
- Parameters
98
- ----------
99
- *args
100
- Must be empty; all args must be named. (This parameter exists to
101
- enforce correct use of the function.)
102
- default: any or None
103
- The default return value for the component. This is returned when
104
- the component's frontend hasn't yet specified a value with
105
- `setComponentValue`.
106
- key: str or None
107
- If not None, this is the user key we use to generate the
108
- component's "widget ID".
109
- **kwargs
110
- Keyword args to pass to the component.
111
-
112
- Returns
113
- -------
114
- any or None
115
- The component's widget value.
116
-
117
- """
118
- if len(args) > 0:
119
- raise MarshallComponentException(f"Argument '{args[0]}' needs a label")
120
-
121
- try:
122
- import pyarrow
123
-
124
- from streamlit.components.v1 import component_arrow
125
- except ImportError:
126
- raise StreamlitAPIException(
127
- """To use Custom Components in Streamlit, you need to install
128
- PyArrow. To do so locally:
129
-
130
- `pip install pyarrow`
131
-
132
- And if you're using Streamlit Cloud, add "pyarrow" to your requirements.txt."""
133
- )
134
-
135
- # In addition to the custom kwargs passed to the component, we also
136
- # send the special 'default' and 'key' params to the component
137
- # frontend.
138
- all_args = dict(kwargs, **{"default": default, "key": key})
139
-
140
- json_args = {}
141
- special_args = []
142
- for arg_name, arg_val in all_args.items():
143
- if type_util.is_bytes_like(arg_val):
144
- bytes_arg = SpecialArg()
145
- bytes_arg.key = arg_name
146
- bytes_arg.bytes = to_bytes(arg_val)
147
- special_args.append(bytes_arg)
148
- elif type_util.is_dataframe_like(arg_val):
149
- dataframe_arg = SpecialArg()
150
- dataframe_arg.key = arg_name
151
- component_arrow.marshall(dataframe_arg.arrow_dataframe.data, arg_val)
152
- special_args.append(dataframe_arg)
153
- else:
154
- json_args[arg_name] = arg_val
155
-
156
- try:
157
- serialized_json_args = json.dumps(json_args)
158
- except Exception as ex:
159
- raise MarshallComponentException(
160
- "Could not convert component args to JSON", ex
161
- )
162
-
163
- def marshall_component(
164
- dg: DeltaGenerator, element: Element
165
- ) -> Any | type[NoValue]:
166
- element.component_instance.component_name = self.name
167
- element.component_instance.form_id = current_form_id(dg)
168
- if self.url is not None:
169
- element.component_instance.url = self.url
170
-
171
- # Normally, a widget's element_hash (which determines
172
- # its identity across multiple runs of an app) is computed
173
- # by hashing its arguments. This means that, if any of the arguments
174
- # to the widget are changed, Streamlit considers it a new widget
175
- # instance and it loses its previous state.
176
- #
177
- # However! If a *component* has a `key` argument, then the
178
- # component's hash identity is determined by entirely by
179
- # `component_name + url + key`. This means that, when `key`
180
- # exists, the component will maintain its identity even when its
181
- # other arguments change, and the component's iframe won't be
182
- # remounted on the frontend.
183
-
184
- def marshall_element_args():
185
- element.component_instance.json_args = serialized_json_args
186
- element.component_instance.special_args.extend(special_args)
187
-
188
- ctx = get_script_run_ctx()
189
-
190
- if key is None:
191
- marshall_element_args()
192
- id = compute_widget_id(
193
- "component_instance",
194
- user_key=key,
195
- name=self.name,
196
- form_id=current_form_id(dg),
197
- url=self.url,
198
- key=key,
199
- json_args=serialized_json_args,
200
- special_args=special_args,
201
- page=ctx.page_script_hash if ctx else None,
202
- )
203
- else:
204
- id = compute_widget_id(
205
- "component_instance",
206
- user_key=key,
207
- name=self.name,
208
- form_id=current_form_id(dg),
209
- url=self.url,
210
- key=key,
211
- page=ctx.page_script_hash if ctx else None,
212
- )
213
- element.component_instance.id = id
214
-
215
- def deserialize_component(ui_value, widget_id=""):
216
- # ui_value is an object from json, an ArrowTable proto, or a bytearray
217
- return ui_value
218
-
219
- component_state = register_widget(
220
- element_type="component_instance",
221
- element_proto=element.component_instance,
222
- user_key=key,
223
- widget_func_name=self.name,
224
- deserializer=deserialize_component,
225
- serializer=lambda x: x,
226
- ctx=ctx,
227
- )
228
- widget_value = component_state.value
229
-
230
- if key is not None:
231
- marshall_element_args()
232
-
233
- if widget_value is None:
234
- widget_value = default
235
- elif isinstance(widget_value, ArrowTableProto):
236
- widget_value = component_arrow.arrow_proto_to_dataframe(widget_value)
237
-
238
- # widget_value will be either None or whatever the component's most
239
- # recent setWidgetValue value is. We coerce None -> NoValue,
240
- # because that's what DeltaGenerator._enqueue expects.
241
- return widget_value if widget_value is not None else NoValue
242
-
243
- # We currently only support writing to st._main, but this will change
244
- # when we settle on an improved API in a post-layout world.
245
- dg = streamlit._main
246
-
247
- element = Element()
248
- return_value = marshall_component(dg, element)
249
- result = dg._enqueue(
250
- "component_instance", element.component_instance, return_value
251
- )
252
-
253
- return result
254
-
255
- def __eq__(self, other) -> bool:
256
- """Equality operator."""
257
- return (
258
- isinstance(other, CustomComponent)
259
- and self.name == other.name
260
- and self.path == other.path
261
- and self.url == other.url
262
- )
263
-
264
- def __ne__(self, other) -> bool:
265
- """Inequality operator."""
266
- return not self == other
267
-
268
- def __str__(self) -> str:
269
- return f"'{self.name}': {self.path if self.path is not None else self.url}"
270
-
271
-
272
- def declare_component(
273
- name: str,
274
- path: str | None = None,
275
- url: str | None = None,
276
- ) -> CustomComponent:
277
- """Create and register a custom component.
278
-
279
- Parameters
280
- ----------
281
- name: str
282
- A short, descriptive name for the component. Like, "slider".
283
- path: str or None
284
- The path to serve the component's frontend files from. Either
285
- `path` or `url` must be specified, but not both.
286
- url: str or None
287
- The URL that the component is served from. Either `path` or `url`
288
- must be specified, but not both.
289
-
290
- Returns
291
- -------
292
- CustomComponent
293
- A CustomComponent that can be called like a function.
294
- Calling the component will create a new instance of the component
295
- in the Streamlit app.
296
-
297
- """
298
-
299
- # Get our stack frame.
300
- current_frame = inspect.currentframe()
301
- assert current_frame is not None
302
-
303
- # Get the stack frame of our calling function.
304
- caller_frame = current_frame.f_back
305
- assert caller_frame is not None
306
-
307
- # Get the caller's module name. `__name__` gives us the module's
308
- # fully-qualified name, which includes its package.
309
- module = inspect.getmodule(caller_frame)
310
- assert module is not None
311
- module_name = module.__name__
312
-
313
- # If the caller was the main module that was executed (that is, if the
314
- # user executed `python my_component.py`), then this name will be
315
- # "__main__" instead of the actual package name. In this case, we use
316
- # the main module's filename, sans `.py` extension, as the component name.
317
- if module_name == "__main__":
318
- file_path = inspect.getfile(caller_frame)
319
- filename = os.path.basename(file_path)
320
- module_name, _ = os.path.splitext(filename)
321
-
322
- # Build the component name.
323
- component_name = f"{module_name}.{name}"
324
-
325
- # Create our component object, and register it.
326
- component = CustomComponent(name=component_name, path=path, url=url)
327
- ComponentRegistry.instance().register_component(component)
328
-
329
- return component
330
-
331
-
332
- class ComponentRegistry:
333
- _instance_lock: threading.Lock = threading.Lock()
334
- _instance: ComponentRegistry | None = None
335
-
336
- @classmethod
337
- def instance(cls) -> ComponentRegistry:
338
- """Returns the singleton ComponentRegistry"""
339
- # We use a double-checked locking optimization to avoid the overhead
340
- # of acquiring the lock in the common case:
341
- # https://en.wikipedia.org/wiki/Double-checked_locking
342
- if cls._instance is None:
343
- with cls._instance_lock:
344
- if cls._instance is None:
345
- cls._instance = ComponentRegistry()
346
- return cls._instance
347
-
348
- def __init__(self):
349
- self._components: dict[str, CustomComponent] = {}
350
- self._lock = threading.Lock()
351
-
352
- def __repr__(self) -> str:
353
- return util.repr_(self)
354
-
355
- def register_component(self, component: CustomComponent) -> None:
356
- """Register a CustomComponent.
357
-
358
- Parameters
359
- ----------
360
- component : CustomComponent
361
- The component to register.
362
- """
363
-
364
- # Validate the component's path
365
- abspath = component.abspath
366
- if abspath is not None and not os.path.isdir(abspath):
367
- raise StreamlitAPIException(f"No such component directory: '{abspath}'")
368
-
369
- with self._lock:
370
- existing = self._components.get(component.name)
371
- self._components[component.name] = component
372
-
373
- if existing is not None and component != existing:
374
- _LOGGER.warning(
375
- "%s overriding previously-registered %s",
376
- component,
377
- existing,
378
- )
379
-
380
- _LOGGER.debug("Registered component %s", component)
381
-
382
- def get_component_path(self, name: str) -> str | None:
383
- """Return the filesystem path for the component with the given name.
384
-
385
- If no such component is registered, or if the component exists but is
386
- being served from a URL, return None instead.
387
- """
388
- component = self._components.get(name, None)
389
- return component.abspath if component is not None else None
15
+ # The components.py file exists because existing custom components have started
16
+ # to rely on internals of the components package. For example, streamlit-option-menu accesses
17
+ # [register_widget](https://github.com/victoryhb/streamlit-option-menu/blob/master/streamlit_option_menu/streamlit_callback.py#L28),
18
+ # which is only a transitive import through `streamlit.components.v1.custom_component`.
19
+ # Since we do not know what other internals are used out in the wild, let's try to
20
+ # model the old behavior and not to break things.
21
+
22
+ from streamlit.components.v1.component_registry import declare_component
23
+ from streamlit.components.v1.custom_component import *
@@ -0,0 +1,241 @@
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 json
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from streamlit import _main, type_util
21
+ from streamlit.components.types.base_custom_component import BaseCustomComponent
22
+ from streamlit.elements.form import current_form_id
23
+ from streamlit.errors import StreamlitAPIException
24
+ from streamlit.proto.Components_pb2 import ArrowTable as ArrowTableProto
25
+ from streamlit.proto.Components_pb2 import SpecialArg
26
+ from streamlit.proto.Element_pb2 import Element
27
+ from streamlit.runtime.metrics_util import gather_metrics
28
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
29
+ from streamlit.runtime.state import NoValue, register_widget
30
+ from streamlit.runtime.state.common import compute_widget_id
31
+ from streamlit.type_util import to_bytes
32
+
33
+ if TYPE_CHECKING:
34
+ from streamlit.delta_generator import DeltaGenerator
35
+
36
+
37
+ class MarshallComponentException(StreamlitAPIException):
38
+ """Class for exceptions generated during custom component marshalling."""
39
+
40
+ pass
41
+
42
+
43
+ class CustomComponent(BaseCustomComponent):
44
+ """A Custom Component declaration."""
45
+
46
+ def __call__(
47
+ self,
48
+ *args,
49
+ default: Any = None,
50
+ key: str | None = None,
51
+ **kwargs,
52
+ ) -> Any:
53
+ """An alias for create_instance."""
54
+ return self.create_instance(*args, default=default, key=key, **kwargs)
55
+
56
+ @gather_metrics("create_instance")
57
+ def create_instance(
58
+ self,
59
+ *args,
60
+ default: Any = None,
61
+ key: str | None = None,
62
+ **kwargs,
63
+ ) -> Any:
64
+ """Create a new instance of the component.
65
+
66
+ Parameters
67
+ ----------
68
+ *args
69
+ Must be empty; all args must be named. (This parameter exists to
70
+ enforce correct use of the function.)
71
+ default: any or None
72
+ The default return value for the component. This is returned when
73
+ the component's frontend hasn't yet specified a value with
74
+ `setComponentValue`.
75
+ key: str or None
76
+ If not None, this is the user key we use to generate the
77
+ component's "widget ID".
78
+ **kwargs
79
+ Keyword args to pass to the component.
80
+
81
+ Returns
82
+ -------
83
+ any or None
84
+ The component's widget value.
85
+
86
+ """
87
+ if len(args) > 0:
88
+ raise MarshallComponentException(f"Argument '{args[0]}' needs a label")
89
+
90
+ try:
91
+ import pyarrow
92
+
93
+ from streamlit.components.v1 import component_arrow
94
+ except ImportError:
95
+ raise StreamlitAPIException(
96
+ """To use Custom Components in Streamlit, you need to install
97
+ PyArrow. To do so locally:
98
+
99
+ `pip install pyarrow`
100
+
101
+ And if you're using Streamlit Cloud, add "pyarrow" to your requirements.txt."""
102
+ )
103
+
104
+ # In addition to the custom kwargs passed to the component, we also
105
+ # send the special 'default' and 'key' params to the component
106
+ # frontend.
107
+ all_args = dict(kwargs, **{"default": default, "key": key})
108
+
109
+ json_args = {}
110
+ special_args = []
111
+ for arg_name, arg_val in all_args.items():
112
+ if type_util.is_bytes_like(arg_val):
113
+ bytes_arg = SpecialArg()
114
+ bytes_arg.key = arg_name
115
+ bytes_arg.bytes = to_bytes(arg_val)
116
+ special_args.append(bytes_arg)
117
+ elif type_util.is_dataframe_like(arg_val):
118
+ dataframe_arg = SpecialArg()
119
+ dataframe_arg.key = arg_name
120
+ component_arrow.marshall(dataframe_arg.arrow_dataframe.data, arg_val)
121
+ special_args.append(dataframe_arg)
122
+ else:
123
+ json_args[arg_name] = arg_val
124
+
125
+ try:
126
+ serialized_json_args = json.dumps(json_args)
127
+ except Exception as ex:
128
+ raise MarshallComponentException(
129
+ "Could not convert component args to JSON", ex
130
+ )
131
+
132
+ def marshall_component(
133
+ dg: DeltaGenerator, element: Element
134
+ ) -> Any | type[NoValue]:
135
+ element.component_instance.component_name = self.name
136
+ element.component_instance.form_id = current_form_id(dg)
137
+ if self.url is not None:
138
+ element.component_instance.url = self.url
139
+
140
+ # Normally, a widget's element_hash (which determines
141
+ # its identity across multiple runs of an app) is computed
142
+ # by hashing its arguments. This means that, if any of the arguments
143
+ # to the widget are changed, Streamlit considers it a new widget
144
+ # instance and it loses its previous state.
145
+ #
146
+ # However! If a *component* has a `key` argument, then the
147
+ # component's hash identity is determined by entirely by
148
+ # `component_name + url + key`. This means that, when `key`
149
+ # exists, the component will maintain its identity even when its
150
+ # other arguments change, and the component's iframe won't be
151
+ # remounted on the frontend.
152
+
153
+ def marshall_element_args():
154
+ element.component_instance.json_args = serialized_json_args
155
+ element.component_instance.special_args.extend(special_args)
156
+
157
+ ctx = get_script_run_ctx()
158
+
159
+ if key is None:
160
+ marshall_element_args()
161
+ computed_id = compute_widget_id(
162
+ "component_instance",
163
+ user_key=key,
164
+ name=self.name,
165
+ form_id=current_form_id(dg),
166
+ url=self.url,
167
+ key=key,
168
+ json_args=serialized_json_args,
169
+ special_args=special_args,
170
+ page=ctx.page_script_hash if ctx else None,
171
+ )
172
+ else:
173
+ computed_id = compute_widget_id(
174
+ "component_instance",
175
+ user_key=key,
176
+ name=self.name,
177
+ form_id=current_form_id(dg),
178
+ url=self.url,
179
+ key=key,
180
+ page=ctx.page_script_hash if ctx else None,
181
+ )
182
+ element.component_instance.id = computed_id
183
+
184
+ def deserialize_component(ui_value, widget_id=""):
185
+ # ui_value is an object from json, an ArrowTable proto, or a bytearray
186
+ return ui_value
187
+
188
+ component_state = register_widget(
189
+ element_type="component_instance",
190
+ element_proto=element.component_instance,
191
+ user_key=key,
192
+ widget_func_name=self.name,
193
+ deserializer=deserialize_component,
194
+ serializer=lambda x: x,
195
+ ctx=ctx,
196
+ )
197
+ widget_value = component_state.value
198
+
199
+ if key is not None:
200
+ marshall_element_args()
201
+
202
+ if widget_value is None:
203
+ widget_value = default
204
+ elif isinstance(widget_value, ArrowTableProto):
205
+ widget_value = component_arrow.arrow_proto_to_dataframe(widget_value)
206
+
207
+ # widget_value will be either None or whatever the component's most
208
+ # recent setWidgetValue value is. We coerce None -> NoValue,
209
+ # because that's what DeltaGenerator._enqueue expects.
210
+ return widget_value if widget_value is not None else NoValue
211
+
212
+ # We currently only support writing to st._main, but this will change
213
+ # when we settle on an improved API in a post-layout world.
214
+ dg = _main
215
+
216
+ element = Element()
217
+ return_value = marshall_component(dg, element)
218
+ result = dg._enqueue(
219
+ "component_instance", element.component_instance, return_value
220
+ )
221
+
222
+ return result
223
+
224
+ def __eq__(self, other) -> bool:
225
+ """Equality operator."""
226
+ return (
227
+ isinstance(other, CustomComponent)
228
+ and self.name == other.name
229
+ and self.path == other.path
230
+ and self.url == other.url
231
+ and self.module_name == other.module_name
232
+ )
233
+
234
+ def __ne__(self, other) -> bool:
235
+ """Inequality operator."""
236
+
237
+ # we have to use "not X == Y"" here because if we use "X != Y" we call __ne__ again and end up in recursion
238
+ return not self == other
239
+
240
+ def __str__(self) -> str:
241
+ return f"'{self.name}': {self.path if self.path is not None else self.url}"