streamlit-nightly 1.36.1.dev20240710__py2.py3-none-any.whl → 1.36.1.dev20240711__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 (26) hide show
  1. streamlit/__init__.py +1 -0
  2. streamlit/delta_generator.py +2 -0
  3. streamlit/elements/lib/options_selector_utils.py +76 -0
  4. streamlit/elements/widgets/button.py +9 -1
  5. streamlit/elements/widgets/button_group.py +411 -0
  6. streamlit/elements/widgets/multiselect.py +88 -115
  7. streamlit/proto/ButtonGroup_pb2.py +33 -0
  8. streamlit/proto/ButtonGroup_pb2.pyi +122 -0
  9. streamlit/proto/Element_pb2.py +4 -3
  10. streamlit/proto/Element_pb2.pyi +9 -4
  11. streamlit/runtime/state/common.py +2 -0
  12. streamlit/runtime/state/widgets.py +1 -0
  13. streamlit/static/asset-manifest.json +3 -2
  14. streamlit/static/index.html +1 -1
  15. streamlit/static/static/js/1116.841caf48.chunk.js +1 -0
  16. streamlit/static/static/js/main.917a5920.js +2 -0
  17. streamlit/testing/v1/app_test.py +5 -0
  18. streamlit/testing/v1/element_tree.py +92 -0
  19. {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/METADATA +1 -1
  20. {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/RECORD +25 -20
  21. streamlit/static/static/js/main.2bfed63a.js +0 -2
  22. /streamlit/static/static/js/{main.2bfed63a.js.LICENSE.txt → main.917a5920.js.LICENSE.txt} +0 -0
  23. {streamlit_nightly-1.36.1.dev20240710.data → streamlit_nightly-1.36.1.dev20240711.data}/scripts/streamlit.cmd +0 -0
  24. {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/WHEEL +0 -0
  25. {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/entry_points.txt +0 -0
  26. {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/top_level.txt +0 -0
streamlit/__init__.py CHANGED
@@ -157,6 +157,7 @@ date_input = _main.date_input
157
157
  divider = _main.divider
158
158
  download_button = _main.download_button
159
159
  expander = _main.expander
160
+ feedback = _main.feedback
160
161
  pydeck_chart = _main.pydeck_chart
161
162
  empty = _main.empty
162
163
  error = _main.error
@@ -73,6 +73,7 @@ from streamlit.elements.text import TextMixin
73
73
  from streamlit.elements.toast import ToastMixin
74
74
  from streamlit.elements.vega_charts import VegaChartsMixin
75
75
  from streamlit.elements.widgets.button import ButtonMixin
76
+ from streamlit.elements.widgets.button_group import ButtonGroupMixin
76
77
  from streamlit.elements.widgets.camera_input import CameraInputMixin
77
78
  from streamlit.elements.widgets.chat import ChatMixin
78
79
  from streamlit.elements.widgets.checkbox import CheckboxMixin
@@ -148,6 +149,7 @@ class DeltaGenerator(
148
149
  BalloonsMixin,
149
150
  BokehMixin,
150
151
  ButtonMixin,
152
+ ButtonGroupMixin,
151
153
  CameraInputMixin,
152
154
  ChatMixin,
153
155
  CheckboxMixin,
@@ -0,0 +1,76 @@
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
+ from typing import (
18
+ Any,
19
+ Sequence,
20
+ cast,
21
+ )
22
+
23
+ from streamlit.dataframe_util import OptionSequence, convert_anything_to_sequence
24
+ from streamlit.errors import StreamlitAPIException
25
+ from streamlit.type_util import (
26
+ T,
27
+ check_python_comparable,
28
+ is_type,
29
+ )
30
+
31
+
32
+ def check_and_convert_to_indices(
33
+ opt: Sequence[Any], default_values: Sequence[Any] | Any | None
34
+ ) -> list[int] | None:
35
+ """Perform validation checks and return indices based on the default values."""
36
+ if default_values is None and None not in opt:
37
+ return None
38
+
39
+ if not isinstance(default_values, list):
40
+ # This if is done before others because calling if not x (done
41
+ # right below) when x is of type pd.Series() or np.array() throws a
42
+ # ValueError exception.
43
+ if is_type(default_values, "numpy.ndarray") or is_type(
44
+ default_values, "pandas.core.series.Series"
45
+ ):
46
+ default_values = list(cast(Sequence[Any], default_values))
47
+ elif (
48
+ isinstance(default_values, (tuple, set))
49
+ or default_values
50
+ and default_values not in opt
51
+ ):
52
+ default_values = list(default_values)
53
+ else:
54
+ default_values = [default_values]
55
+ for value in default_values:
56
+ if value not in opt:
57
+ raise StreamlitAPIException(
58
+ f"The default value '{value}' is not part of the options. "
59
+ "Please make sure that every default values also exists in the options."
60
+ )
61
+
62
+ return [opt.index(value) for value in default_values]
63
+
64
+
65
+ def convert_to_sequence_and_check_comparable(options: OptionSequence[T]) -> Sequence[T]:
66
+ indexable_options = convert_anything_to_sequence(options)
67
+ check_python_comparable(indexable_options)
68
+ return indexable_options
69
+
70
+
71
+ def get_default_indices(
72
+ indexable_options: Sequence[T], default: Sequence[Any] | Any | None = None
73
+ ) -> list[int]:
74
+ default_indices = check_and_convert_to_indices(indexable_options, default)
75
+ default_indices = default_indices if default_indices is not None else []
76
+ return default_indices
@@ -18,7 +18,15 @@ import io
18
18
  import os
19
19
  from dataclasses import dataclass
20
20
  from textwrap import dedent
21
- from typing import TYPE_CHECKING, BinaryIO, Final, Literal, TextIO, Union, cast
21
+ from typing import (
22
+ TYPE_CHECKING,
23
+ BinaryIO,
24
+ Final,
25
+ Literal,
26
+ TextIO,
27
+ Union,
28
+ cast,
29
+ )
22
30
 
23
31
  from typing_extensions import TypeAlias
24
32
 
@@ -0,0 +1,411 @@
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
+ from typing import (
18
+ TYPE_CHECKING,
19
+ Any,
20
+ Callable,
21
+ Final,
22
+ Literal,
23
+ Sequence,
24
+ TypeVar,
25
+ cast,
26
+ get_args,
27
+ )
28
+
29
+ from typing_extensions import TypeAlias
30
+
31
+ from streamlit.elements.form import current_form_id
32
+ from streamlit.elements.lib.options_selector_utils import (
33
+ convert_to_sequence_and_check_comparable,
34
+ get_default_indices,
35
+ )
36
+ from streamlit.elements.lib.policies import check_widget_policies
37
+ from streamlit.elements.lib.utils import (
38
+ Key,
39
+ maybe_coerce_enum_sequence,
40
+ to_key,
41
+ )
42
+ from streamlit.elements.widgets.multiselect import MultiSelectSerde
43
+ from streamlit.errors import StreamlitAPIException
44
+ from streamlit.proto.ButtonGroup_pb2 import ButtonGroup as ButtonGroupProto
45
+ from streamlit.runtime.metrics_util import gather_metrics
46
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
47
+ from streamlit.runtime.state import register_widget
48
+ from streamlit.runtime.state.common import (
49
+ RegisterWidgetResult,
50
+ WidgetDeserializer,
51
+ WidgetSerializer,
52
+ compute_widget_id,
53
+ save_for_app_testing,
54
+ )
55
+
56
+ if TYPE_CHECKING:
57
+ from streamlit.dataframe_util import OptionSequence
58
+ from streamlit.delta_generator import DeltaGenerator
59
+ from streamlit.runtime.state import (
60
+ WidgetArgs,
61
+ WidgetCallback,
62
+ WidgetKwargs,
63
+ )
64
+ from streamlit.type_util import T
65
+
66
+
67
+ V = TypeVar("V")
68
+
69
+ _THUMB_ICONS: Final = (":material/thumb_up:", ":material/thumb_down:")
70
+ _FACES_ICONS: Final = (
71
+ ":material/sentiment_sad:",
72
+ ":material/sentiment_dissatisfied:",
73
+ ":material/sentiment_neutral:",
74
+ ":material/sentiment_satisfied:",
75
+ ":material/sentiment_very_satisfied:",
76
+ )
77
+ _NUMBER_STARS: Final = 5
78
+ _STAR_ICON: Final = ":material/star:"
79
+ # we don't have the filled-material icon library as a dependency. Hence, we have it here
80
+ # in base64 format and send it over the wire as an image.
81
+ _SELECTED_STAR_ICON: Final = (
82
+ "<img src='data:image/svg+xml;base64,"
83
+ "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0i"
84
+ "MCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjNWY2MzY4Ij48cGF0aCBkPSJNNDgwLTI2OSAz"
85
+ "MTQtMTY5cS0xMSA3LTIzIDZ0LTIxLThxLTktNy0xNC0xNy41dC0yLTIzLjVsNDQtMTg5LTE0Ny0xMjdxLTEwL"
86
+ "TktMTIuNS0yMC41VDE0MC01NzFxNC0xMSAxMi0xOHQyMi05bDE5NC0xNyA3NS0xNzhxNS0xMiAxNS41LTE4dDI"
87
+ "xLjUtNnExMSAwIDIxLjUgNnQxNS41IDE4bDc1IDE3OCAxOTQgMTdxMTQgMiAyMiA5dDEyIDE4cTQgMTEgMS41I"
88
+ "DIyLjVUODA5LTUyOEw2NjItNDAxbDQ0IDE4OXEzIDEzLTIgMjMuNVQ2OTAtMTcxcS05IDctMjEgOHQtMjMtNkw"
89
+ "0ODAtMjY5WiIvPjwvc3ZnPg=='/>"
90
+ )
91
+
92
+ _FeedbackOptions: TypeAlias = Literal["thumbs", "faces", "stars"]
93
+
94
+
95
+ class FeedbackSerde:
96
+ """Uses the MultiSelectSerde under-the-hood, but accepts a single index value
97
+ and deserializes to a single index value. This is because for feedback, we always
98
+ allow just a single selection.
99
+
100
+ When a sentiment_mapping is provided, the sentiment corresponding to the index is
101
+ serialized/deserialized. Otherwise, the index is used as the sentiment.
102
+ """
103
+
104
+ def __init__(self, option_indices: list[int]) -> None:
105
+ """Initialize the FeedbackSerde with a list of sentimets."""
106
+ self.multiselect_serde: MultiSelectSerde[int] = MultiSelectSerde(option_indices)
107
+
108
+ def serialize(self, value: int | None) -> list[int]:
109
+ """Serialize the passed sentiment option into its corresponding index
110
+ (wrapped in a list).
111
+ """
112
+ _value = [value] if value is not None else []
113
+ return self.multiselect_serde.serialize(_value)
114
+
115
+ def deserialize(self, ui_value: list[int], widget_id: str = "") -> int | None:
116
+ """Receive a list of indices and return the corresponding sentiments."""
117
+ deserialized = self.multiselect_serde.deserialize(ui_value, widget_id)
118
+
119
+ if len(deserialized) == 0:
120
+ return None
121
+
122
+ return deserialized[0]
123
+
124
+
125
+ def get_mapped_options(
126
+ feedback_option: _FeedbackOptions,
127
+ ) -> tuple[list[ButtonGroupProto.Option], list[int]]:
128
+ # options object understandable by the web app
129
+ options: list[ButtonGroupProto.Option] = []
130
+ # we use the option index in the webapp communication to
131
+ # indicate which option is selected
132
+ options_indices: list[int] = []
133
+
134
+ if feedback_option == "thumbs":
135
+ # reversing the index mapping to have thumbs up first (but still with the higher
136
+ # index (=sentiment) in the list)
137
+ options_indices = list(reversed(range(len(_THUMB_ICONS))))
138
+ options = [ButtonGroupProto.Option(content=icon) for icon in _THUMB_ICONS]
139
+ elif feedback_option == "faces":
140
+ options_indices = list(range(len(_FACES_ICONS)))
141
+ options = [ButtonGroupProto.Option(content=icon) for icon in _FACES_ICONS]
142
+ elif feedback_option == "stars":
143
+ options_indices = list(range(_NUMBER_STARS))
144
+ options = [
145
+ ButtonGroupProto.Option(
146
+ content=_STAR_ICON,
147
+ selected_content=_SELECTED_STAR_ICON,
148
+ )
149
+ ] * _NUMBER_STARS
150
+
151
+ return options, options_indices
152
+
153
+
154
+ def _build_proto(
155
+ widget_id: str,
156
+ formatted_options: Sequence[ButtonGroupProto.Option],
157
+ default_values: list[int],
158
+ disabled: bool,
159
+ current_form_id: str,
160
+ click_mode: ButtonGroupProto.ClickMode.ValueType,
161
+ selection_visualization: ButtonGroupProto.SelectionVisualization.ValueType = (
162
+ ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
163
+ ),
164
+ ) -> ButtonGroupProto:
165
+ proto = ButtonGroupProto()
166
+
167
+ proto.id = widget_id
168
+ proto.default[:] = default_values
169
+ proto.form_id = current_form_id
170
+ proto.disabled = disabled
171
+ proto.click_mode = click_mode
172
+
173
+ for formatted_option in formatted_options:
174
+ proto.options.append(formatted_option)
175
+ proto.selection_visualization = selection_visualization
176
+ return proto
177
+
178
+
179
+ class ButtonGroupMixin:
180
+ @gather_metrics("feedback")
181
+ def feedback(
182
+ self,
183
+ options: _FeedbackOptions = "thumbs",
184
+ *,
185
+ key: str | None = None,
186
+ disabled: bool = False,
187
+ on_change: WidgetCallback | None = None,
188
+ args: Any | None = None,
189
+ kwargs: Any | None = None,
190
+ ) -> int | None:
191
+ """Display a feedback widget.
192
+
193
+ This is useful to collect user feedback, especially in chat-based apps.
194
+
195
+ Parameters:
196
+ -----------
197
+ options: "thumbs", "faces", or "stars"
198
+ The feedback options displayed to the user. The options are:
199
+ - "thumbs" (default): displays a row of thumbs-up and thumbs-down buttons.
200
+ - "faces": displays a row of five buttons with facial expressions, each
201
+ depicting increasing satisfaction from left to right.
202
+ - "stars": displays a row of star icons typically used for ratings.
203
+ key : str or int
204
+ An optional string or integer to use as the unique key for the widget.
205
+ If this is omitted, a key will be generated for the widget
206
+ based on its content. Multiple widgets of the same type may
207
+ not share the same key.
208
+ disabled : bool
209
+ An optional boolean, which disables the multiselect widget if set
210
+ to True. The default is False. This argument can only be supplied
211
+ by keyword.
212
+ on_change : callable
213
+ An optional callback invoked when this multiselect's value changes.
214
+ args : tuple
215
+ An optional tuple of args to pass to the callback.
216
+ kwargs : dict
217
+ An optional dict of kwargs to pass to the callback.
218
+
219
+ Returns
220
+ -------
221
+ An integer indicating the user's selection, where 0 is the lowest
222
+ feedback and higher values indicate more positive feedback.
223
+ If no option was selected, returns None.
224
+ - For "thumbs": a return value of 0 is for thumbs-down and 1 for thumbs-up.
225
+ - For "faces" and "stars":
226
+ values range from 0 (least satisfied) to 4 (most satisfied).
227
+
228
+
229
+ Examples
230
+ --------
231
+ Example 1: Display a feedback widget with stars and show the selected sentiment
232
+
233
+ >>> import streamlit as st
234
+ >>> sentiment_mapping: = [0.0, 0.25, 0.5, 0.75, 1.0]
235
+ >>> selected = st.feedback("stars")
236
+ >>> st.write(f"You selected: {sentiment_mapping[selected]}")
237
+
238
+ Example 2: Display a feedback widget with thumbs and show the selected sentiment
239
+
240
+ >>> import streamlit as st
241
+ >>> sentiment_mapping: = [0.0, 1.0]
242
+ >>> selected = st.feedback("thumbs")
243
+ >>> st.write(f"You selected: {sentiment_mapping[selected]}")
244
+ """
245
+
246
+ if not isinstance(options, list) and options not in get_args(_FeedbackOptions):
247
+ raise StreamlitAPIException(
248
+ "The options argument to st.feedback must be one of "
249
+ "['thumbs', 'faces', 'stars']. "
250
+ f"The argument passed was '{options}'."
251
+ )
252
+ transformed_options, options_indices = get_mapped_options(options)
253
+ serde = FeedbackSerde(options_indices)
254
+
255
+ selection_visualization = ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
256
+ if options == "stars":
257
+ selection_visualization = (
258
+ ButtonGroupProto.SelectionVisualization.ALL_UP_TO_SELECTED
259
+ )
260
+
261
+ sentiment = self._button_group(
262
+ transformed_options,
263
+ default=None,
264
+ key=key,
265
+ click_mode=ButtonGroupProto.SINGLE_SELECT,
266
+ disabled=disabled,
267
+ deserializer=serde.deserialize,
268
+ serializer=serde.serialize,
269
+ on_change=on_change,
270
+ args=args,
271
+ kwargs=kwargs,
272
+ selection_visualization=selection_visualization,
273
+ )
274
+ return sentiment.value
275
+
276
+ # Disable this more generic widget for now
277
+ # @gather_metrics("button_group")
278
+ def _internal_button_group(
279
+ self,
280
+ options: OptionSequence[V],
281
+ *,
282
+ key: Key | None = None,
283
+ default: Sequence[Any] | None = None,
284
+ click_mode: Literal["select", "multiselect"] = "select",
285
+ disabled: bool = False,
286
+ format_func: Callable[[V], dict[str, str]] | None = None,
287
+ on_change: WidgetCallback | None = None,
288
+ args: WidgetArgs | None = None,
289
+ kwargs: WidgetKwargs | None = None,
290
+ ) -> list[V]:
291
+ def _transformed_format_func(x: V) -> ButtonGroupProto.Option:
292
+ if format_func is None:
293
+ return ButtonGroupProto.Option(content=str(x))
294
+
295
+ transformed = format_func(x)
296
+ return ButtonGroupProto.Option(
297
+ content=transformed["content"],
298
+ selected_content=transformed["selected_content"],
299
+ )
300
+
301
+ indexable_options = convert_to_sequence_and_check_comparable(options)
302
+ default_values = get_default_indices(indexable_options, default)
303
+ serde = MultiSelectSerde(indexable_options, default_values)
304
+
305
+ res = self._button_group(
306
+ indexable_options,
307
+ key=key,
308
+ default=default_values,
309
+ click_mode=ButtonGroupProto.ClickMode.MULTI_SELECT
310
+ if click_mode == "multiselect"
311
+ else ButtonGroupProto.SINGLE_SELECT,
312
+ disabled=disabled,
313
+ format_func=_transformed_format_func,
314
+ serializer=serde.serialize,
315
+ deserializer=serde.deserialize,
316
+ on_change=on_change,
317
+ args=args,
318
+ kwargs=kwargs,
319
+ after_register_callback=lambda widget_state: maybe_coerce_enum_sequence(
320
+ widget_state, options, indexable_options
321
+ ),
322
+ )
323
+ return res.value
324
+
325
+ def _button_group(
326
+ self,
327
+ indexable_options: Sequence[Any],
328
+ *,
329
+ key: Key | None = None,
330
+ default: list[int] | None = None,
331
+ click_mode: ButtonGroupProto.ClickMode.ValueType = (
332
+ ButtonGroupProto.SINGLE_SELECT
333
+ ),
334
+ disabled: bool = False,
335
+ format_func: Callable[[V], ButtonGroupProto.Option] | None = None,
336
+ deserializer: WidgetDeserializer[T],
337
+ serializer: WidgetSerializer[T],
338
+ on_change: WidgetCallback | None = None,
339
+ args: WidgetArgs | None = None,
340
+ kwargs: WidgetKwargs | None = None,
341
+ selection_visualization: ButtonGroupProto.SelectionVisualization.ValueType = (
342
+ ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
343
+ ),
344
+ after_register_callback: Callable[
345
+ [RegisterWidgetResult[T]], RegisterWidgetResult[T]
346
+ ]
347
+ | None = None,
348
+ ) -> RegisterWidgetResult[T]:
349
+ key = to_key(key)
350
+
351
+ check_widget_policies(self.dg, key, on_change, default_value=default)
352
+
353
+ widget_name = "button_group"
354
+ ctx = get_script_run_ctx()
355
+ form_id = current_form_id(self.dg)
356
+ formatted_options = (
357
+ indexable_options
358
+ if format_func is None
359
+ else [format_func(option) for option in indexable_options]
360
+ )
361
+ widget_id = compute_widget_id(
362
+ widget_name,
363
+ user_key=key,
364
+ key=key,
365
+ options=formatted_options,
366
+ default=default,
367
+ form_id=form_id,
368
+ click_mode=click_mode,
369
+ page=ctx.active_script_hash if ctx else None,
370
+ )
371
+
372
+ proto = _build_proto(
373
+ widget_id,
374
+ formatted_options,
375
+ default or [],
376
+ disabled,
377
+ form_id,
378
+ click_mode=click_mode,
379
+ selection_visualization=selection_visualization,
380
+ )
381
+
382
+ widget_state = register_widget(
383
+ widget_name,
384
+ proto,
385
+ # user_key=key,
386
+ on_change_handler=on_change,
387
+ args=args,
388
+ kwargs=kwargs,
389
+ deserializer=deserializer,
390
+ serializer=serializer,
391
+ ctx=ctx,
392
+ )
393
+
394
+ if after_register_callback is not None:
395
+ widget_state = after_register_callback(widget_state)
396
+
397
+ if widget_state.value_changed:
398
+ proto.value[:] = serializer(widget_state.value)
399
+ proto.set_value = True
400
+
401
+ if ctx:
402
+ save_for_app_testing(ctx, widget_id, format_func)
403
+
404
+ self.dg._enqueue(widget_name, proto)
405
+
406
+ return widget_state
407
+
408
+ @property
409
+ def dg(self) -> DeltaGenerator:
410
+ """Get our DeltaGenerator."""
411
+ return cast("DeltaGenerator", self)