Source code for discord.ui.section

from __future__ import annotations

from functools import partial
from typing import TYPE_CHECKING, ClassVar, Iterator, TypeVar

from ..components import Section as SectionComponent
from ..components import _component_factory
from ..enums import ComponentType
from ..utils import find
from .button import Button
from .item import Item, ItemCallbackType
from .text_display import TextDisplay
from .thumbnail import Thumbnail

__all__ = ("Section",)

if TYPE_CHECKING:
    from typing_extensions import Self

    from ..types.components import SectionComponent as SectionComponentPayload
    from .view import View


S = TypeVar("S", bound="Section")
V = TypeVar("V", bound="View", covariant=True)


[docs] class Section(Item[V]): """Represents a UI section. Sections must have 1-3 (inclusive) items and an accessory set. .. versionadded:: 2.7 Parameters ---------- *items: :class:`Item` The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay`. Sections must have at least 1 item before being sent. accessory: Optional[:class:`Item`] The section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`. Sections must have an accessory attached before being sent. id: Optional[:class:`int`] The section's ID. """ __item_repr_attributes__: tuple[str, ...] = ( "items", "accessory", "id", ) __section_accessory_item__: ClassVar[ItemCallbackType] = [] def __init_subclass__(cls) -> None: accessory: list[ItemCallbackType] = [] for base in reversed(cls.__mro__): for member in base.__dict__.values(): if hasattr(member, "__discord_ui_model_type__"): accessory.append(member) cls.__section_accessory_item__ = accessory def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): super().__init__() self.items: list[Item] = [] self.accessory: Item | None = None self._underlying = SectionComponent._raw_construct( type=ComponentType.section, id=id, components=[], accessory=None, ) for func in self.__section_accessory_item__: item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) item.callback = partial(func, self, item) self.set_accessory(item) setattr(self, func.__name__, item) if accessory: self.set_accessory(accessory) for i in items: self.add_item(i) def _add_component_from_item(self, item: Item): self._underlying.components.append(item._underlying) def _set_components(self, items: list[Item]): self._underlying.components.clear() for item in items: self._add_component_from_item(item)
[docs] def add_item(self, item: Item) -> Self: """Adds an item to the section. Parameters ---------- item: :class:`Item` The item to add to the section. Raises ------ TypeError An :class:`Item` was not passed. ValueError Maximum number of items has been exceeded (3). """ if len(self.items) >= 3: raise ValueError("maximum number of children exceeded") if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") item.parent = self self.items.append(item) self._add_component_from_item(item) return self
[docs] def remove_item(self, item: Item | str | int) -> Self: """Removes an item from the section. If an :class:`int` or :class:`str` is passed, the item will be removed by Item ``id`` or ``custom_id`` respectively. Parameters ---------- item: Union[:class:`Item`, :class:`int`, :class:`str`] The item, item ``id``, or item ``custom_id`` to remove from the section. """ if isinstance(item, (str, int)): item = self.get_item(item) try: self.items.remove(item) except ValueError: pass return self
[docs] def get_item(self, id: int | str) -> Item | None: """Get an item from this section. Alias for `utils.get(section.walk_items(), ...)`. If an ``int`` is provided, it will be retrieved by ``id``, otherwise it will check the accessory's ``custom_id``. Parameters ---------- id: Union[:class:`str`, :class:`int`] The id or custom_id of the item to get. Returns ------- Optional[:class:`Item`] The item with the matching ``id`` if it exists. """ if not id: return None attr = "id" if isinstance(id, int) else "custom_id" if self.accessory and id == getattr(self.accessory, attr, None): return self.accessory child = find(lambda i: getattr(i, attr, None) == id, self.items) return child
[docs] def add_text(self, content: str, *, id: int | None = None) -> Self: """Adds a :class:`TextDisplay` to the section. Parameters ---------- content: :class:`str` The content of the text display. id: Optional[:class:`int`] The text display's ID. Raises ------ ValueError Maximum number of items has been exceeded (3). """ if len(self.items) >= 3: raise ValueError("maximum number of children exceeded") text = TextDisplay(content, id=id) return self.add_item(text)
[docs] def set_accessory(self, item: Item) -> Self: """Set an item as the section's :attr:`accessory`. Parameters ---------- item: :class:`Item` The item to set as accessory. Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`. Raises ------ TypeError An :class:`Item` was not passed. """ if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") if self.view: item._view = self.view item.parent = self self.accessory = item self._underlying.accessory = item._underlying return self
[docs] def set_thumbnail( self, url: str, *, description: str | None = None, spoiler: bool = False, id: int | None = None, ) -> Self: """Sets a :class:`Thumbnail` with the provided URL as the section's :attr:`accessory`. Parameters ---------- url: :class:`str` The url of the thumbnail. description: Optional[:class:`str`] The thumbnail's description, up to 1024 characters. spoiler: Optional[:class:`bool`] Whether the thumbnail has the spoiler overlay. Defaults to ``False``. id: Optional[:class:`int`] The thumbnail's ID. """ thumbnail = Thumbnail(url, description=description, spoiler=spoiler, id=id) return self.set_accessory(thumbnail)
@Item.view.setter def view(self, value): self._view = value for item in self.walk_items(): item._view = value item.parent = self
[docs] def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this section. Equivalent to the `Copy Text` option on Discord clients. """ return "\n".join(t for i in self.items if (t := i.copy_text()))
@property def type(self) -> ComponentType: return self._underlying.type @property def width(self) -> int: return 5 def is_dispatchable(self) -> bool: return self.accessory and self.accessory.is_dispatchable() def is_persistent(self) -> bool: if not isinstance(self.accessory, Button): return True return self.accessory.is_persistent() def refresh_component(self, component: SectionComponent) -> None: self._underlying = component for x, y in zip(self.items, component.components): x.refresh_component(y) if self.accessory and component.accessory: self.accessory.refresh_component(component.accessory)
[docs] def disable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Disables all buttons and select menus in the section. At the moment, this only disables :attr:`accessory` if it is a button. Parameters ---------- exclusions: Optional[List[:class:`Item`]] A list of items in `self.items` to not disable from the view. """ for item in self.walk_items(): if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): item.disabled = True return self
[docs] def enable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Enables all buttons and select menus in the section. At the moment, this only enables :attr:`accessory` if it is a button. Parameters ---------- exclusions: Optional[List[:class:`Item`]] A list of items in `self.items` to not enable from the view. """ for item in self.walk_items(): if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): item.disabled = False return self
def walk_items(self) -> Iterator[Item]: r = self.items if self.accessory: yield from r + [self.accessory] else: yield from r def to_component_dict(self) -> SectionComponentPayload: self._set_components(self.items) if self.accessory: self.set_accessory(self.accessory) return self._underlying.to_dict() @classmethod def from_component(cls: type[S], component: SectionComponent) -> S: from .view import _component_to_item # noqa: PLC0415 items = [_component_to_item(c) for c in component.components] accessory = _component_to_item(component.accessory) return cls(*items, accessory=accessory, id=component.id) callback = None