diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 5cc41de26..a27e31ca9 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -143,6 +143,10 @@ class ActionRow(Item[V]): __action_row_children_items__: ClassVar[List[ItemCallbackType[Any]]] = [] __discord_ui_action_row__: ClassVar[bool] = True __discord_ui_update_view__: ClassVar[bool] = True + __item_repr_attributes__ = ( + 'row', + 'id', + ) def __init__( self, @@ -176,6 +180,9 @@ class ActionRow(Item[V]): cls.__action_row_children_items__ = list(children.values()) + def __repr__(self) -> str: + return f'{super().__repr__()[:-1]} children={len(self._children)}>' + def _init_children(self) -> List[Item[Any]]: children = [] @@ -303,7 +310,7 @@ class ActionRow(Item[V]): return self - def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: + def get_item(self, id: int, /) -> Optional[Item[V]]: """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if not found. @@ -321,7 +328,7 @@ class ActionRow(Item[V]): Optional[:class:`Item`] The item found, or ``None``. """ - return _utils_get(self._children, id=id) + return _utils_get(self.walk_children(), id=id) def clear_items(self) -> Self: """Removes all items from the row. diff --git a/discord/ui/button.py b/discord/ui/button.py index 46230d480..9d68d411b 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -97,6 +97,7 @@ class Button(Item[V]): 'emoji', 'row', 'sku_id', + 'id', ) def __init__( @@ -269,6 +270,9 @@ class Button(Item[V]): return self.url is not None return super().is_persistent() + def _can_be_dynamic(self) -> bool: + return True + def _refresh_component(self, button: ButtonComponent) -> None: self._underlying = button diff --git a/discord/ui/container.py b/discord/ui/container.py index 8c550816f..93dedfbe1 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -132,6 +132,12 @@ class Container(Item[V]): __container_children_items__: ClassVar[Dict[str, Union[ItemCallbackType[Any], Item[Any]]]] = {} __discord_ui_update_view__: ClassVar[bool] = True __discord_ui_container__: ClassVar[bool] = True + __item_repr_attributes__ = ( + 'accent_colour', + 'spoiler', + 'row', + 'id', + ) def __init__( self, @@ -156,6 +162,9 @@ class Container(Item[V]): self.row = row self.id = id + def __repr__(self) -> str: + return f'<{super().__repr__()[:-1]} children={len(self._children)}>' + def _add_dispatchable(self, item: Item[Any]) -> None: self.__dispatchable.append(item) @@ -173,14 +182,13 @@ class Container(Item[V]): if isinstance(raw, Item): item = copy.deepcopy(raw) item._parent = self - if getattr(item, '__discord_ui_action_row__', False): + if getattr(item, '__discord_ui_action_row__', False) and item.is_dispatchable(): if item.is_dispatchable(): self.__dispatchable.extend(item._children) # type: ignore - if getattr(item, '__discord_ui_section__', False): - if item.accessory.is_dispatchable(): # type: ignore - if item.accessory._provided_custom_id is False: # type: ignore - item.accessory.custom_id = os.urandom(16).hex() # type: ignore - self.__dispatchable.append(item.accessory) # type: ignore + if getattr(item, '__discord_ui_section__', False) and item.accessory.is_dispatchable(): # type: ignore + if item.accessory._provided_custom_id is False: # type: ignore + item.accessory.custom_id = os.urandom(16).hex() # type: ignore + self.__dispatchable.append(item.accessory) # type: ignore setattr(self, name, item) children.append(item) @@ -415,7 +423,7 @@ class Container(Item[V]): self._view._total_children -= 1 return self - def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: + def get_item(self, id: int, /) -> Optional[Item[V]]: """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if not found. @@ -433,7 +441,7 @@ class Container(Item[V]): Optional[:class:`Item`] The item found, or ``None``. """ - return _utils_get(self._children, id=id) + return _utils_get(self.walk_children(), id=id) def clear_items(self) -> Self: """Removes all the items from the container. diff --git a/discord/ui/dynamic.py b/discord/ui/dynamic.py index 3c7ea0e48..6a5503364 100644 --- a/discord/ui/dynamic.py +++ b/discord/ui/dynamic.py @@ -107,6 +107,9 @@ class DynamicItem(Generic[BaseT], Item[Union[View, LayoutView]]): if not self.item.is_dispatchable(): raise TypeError('item must be dispatchable, e.g. not a URL button') + if not self.item._can_be_dynamic(): + raise TypeError(f'{self.item.__class__.__name__} cannot be set as a dynamic item') + if not self.template.match(self.custom_id): raise ValueError(f'item custom_id {self.custom_id!r} must match the template {self.template.pattern!r}') diff --git a/discord/ui/file.py b/discord/ui/file.py index 09d557a89..630258cf4 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -77,6 +77,13 @@ class File(Item[V]): The ID of this component. This must be unique across the view. """ + __item_repr_attributes__ = ( + 'media', + 'spoiler', + 'row', + 'id', + ) + def __init__( self, media: Union[str, UnfurledMediaItem], diff --git a/discord/ui/item.py b/discord/ui/item.py index c73d8e762..4e1c7d172 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -66,7 +66,7 @@ class Item(Generic[V]): .. versionadded:: 2.0 """ - __item_repr_attributes__: Tuple[str, ...] = ('row',) + __item_repr_attributes__: Tuple[str, ...] = ('row', 'id') def __init__(self): self._view: Optional[V] = None @@ -162,6 +162,13 @@ class Item(Generic[V]): return can_run + def _can_be_dynamic(self) -> bool: + # if an item can be dynamic then it must override this, this is mainly used + # by DynamicItem's so a user cannot set, for example, a Container with a dispatchable + # button as a dynamic item, and cause errors where Container can't be dispatched + # or lost interactions + return False + async def callback(self, interaction: Interaction[ClientT]) -> Any: """|coro| diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 8cfbfa1e3..166d99b22 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -67,6 +67,12 @@ class MediaGallery(Item[V]): The ID of this component. This must be unique across the view. """ + __item_repr_attributes__ = ( + 'items', + 'row', + 'id', + ) + def __init__( self, *items: MediaGalleryItem, @@ -83,6 +89,9 @@ class MediaGallery(Item[V]): self.row = row self.id = id + def __repr__(self) -> str: + return f'<{super().__repr__()[:-1]} items={len(self._underlying.items)}>' + @property def items(self) -> List[MediaGalleryItem]: """List[:class:`.MediaGalleryItem`]: Returns a read-only list of this gallery's items.""" diff --git a/discord/ui/section.py b/discord/ui/section.py index fcd2002e9..da81fe38d 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -67,6 +67,11 @@ class Section(Item[V]): The ID of this component. This must be unique across the view. """ + __item_repr_attributes__ = ( + 'accessory', + 'row', + 'id', + ) __discord_ui_section__: ClassVar[bool] = True __discord_ui_update_view__: ClassVar[bool] = True @@ -94,6 +99,9 @@ class Section(Item[V]): self.row = row self.id = id + def __repr__(self) -> str: + return f'<{super().__repr__()[:-1]} children={len(self._children)}' + @property def type(self) -> Literal[ComponentType.section]: return ComponentType.section diff --git a/discord/ui/select.py b/discord/ui/select.py index 40b8a26f3..7695f759e 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -217,6 +217,7 @@ class BaseSelect(Item[V]): 'min_values', 'max_values', 'disabled', + 'id', ) __component_attributes__: Tuple[str, ...] = ( 'custom_id', @@ -363,6 +364,9 @@ class BaseSelect(Item[V]): kwrgs = {key: getattr(component, key) for key in constructor.__component_attributes__} return constructor(**kwrgs) + def _can_be_dynamic(self) -> bool: + return True + class Select(BaseSelect[V]): """Represents a UI select menu with a list of custom options. This is represented diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 76c1a5275..f90fbaa4b 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -64,6 +64,13 @@ class Separator(Item[V]): The ID of this component. This must be unique across the view. """ + __item_repr_attributes__ = ( + 'visible', + 'spacing', + 'row', + 'id', + ) + def __init__( self, *, diff --git a/discord/ui/text_input.py b/discord/ui/text_input.py index 86f7373ee..218d7c4d0 100644 --- a/discord/ui/text_input.py +++ b/discord/ui/text_input.py @@ -102,6 +102,7 @@ class TextInput(Item[V]): 'label', 'placeholder', 'required', + 'id', ) def __init__( diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 67f8e4c76..e0fbd3a64 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -66,6 +66,14 @@ class Thumbnail(Item[V]): The ID of this component. This must be unique across the view. """ + __item_repr_attributes__ = ( + 'media', + 'description', + 'spoiler', + 'row', + 'id', + ) + def __init__( self, media: Union[str, UnfurledMediaItem],