From 714b2f6e8f402c3adc476e10bdc63ea321bb275e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 05:40:48 +0100 Subject: [PATCH 001/154] base types and flags --- discord/enums.py | 7 +++++ discord/flags.py | 30 ++++++++++++------- discord/types/components.py | 57 ++++++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 12 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index e1086651e9..0435f7920b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -712,6 +712,13 @@ class ComponentType(Enum): role_select = 6 mentionable_select = 7 channel_select = 8 + section = 9 + text_display = 10 + thumbnail = 11 + media_gallery = 12 + file = 13 + separator = 14 + container = 17 def __int__(self): return self.value diff --git a/discord/flags.py b/discord/flags.py index 7073a56e35..bd6370af05 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -333,22 +333,22 @@ class MessageFlags(BaseFlags): @flag_value def crossposted(self): """:class:`bool`: Returns ``True`` if the message is the original crossposted message.""" - return 1 + return 1 << 0 @flag_value def is_crossposted(self): """:class:`bool`: Returns ``True`` if the message was crossposted from another channel.""" - return 2 + return 1 << 1 @flag_value def suppress_embeds(self): """:class:`bool`: Returns ``True`` if the message's embeds have been suppressed.""" - return 4 + return 1 << 2 @flag_value def source_message_deleted(self): """:class:`bool`: Returns ``True`` if the source message for this crosspost has been deleted.""" - return 8 + return 1 << 3 @flag_value def urgent(self): @@ -356,7 +356,7 @@ def urgent(self): An urgent message is one sent by Discord Trust and Safety. """ - return 16 + return 1 << 4 @flag_value def has_thread(self): @@ -364,7 +364,7 @@ def has_thread(self): .. versionadded:: 2.0 """ - return 32 + return 1 << 5 @flag_value def ephemeral(self): @@ -372,7 +372,7 @@ def ephemeral(self): .. versionadded:: 2.0 """ - return 64 + return 1 << 6 @flag_value def loading(self): @@ -382,7 +382,7 @@ def loading(self): .. versionadded:: 2.0 """ - return 128 + return 1 << 7 @flag_value def failed_to_mention_some_roles_in_thread(self): @@ -390,7 +390,7 @@ def failed_to_mention_some_roles_in_thread(self): .. versionadded:: 2.0 """ - return 256 + return 1 << 8 @flag_value def suppress_notifications(self): @@ -401,7 +401,7 @@ def suppress_notifications(self): .. versionadded:: 2.4 """ - return 4096 + return 1 << 12 @flag_value def is_voice_message(self): @@ -409,7 +409,15 @@ def is_voice_message(self): .. versionadded:: 2.5 """ - return 8192 + return 1 << 13 + + @flag_value + def is_components_v2(self): + """:class:`bool`: Returns ``True`` if this message has v2 components. This flag disables sending `content` and `embeds`. + + .. versionadded:: 2.7 + """ + return 1 << 15 @fill_with_flags() diff --git a/discord/types/components.py b/discord/types/components.py index 7b05f8bf08..3c59c322e4 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -33,9 +33,10 @@ from .emoji import PartialEmoji from .snowflake import Snowflake -ComponentType = Literal[1, 2, 3, 4] +ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] InputTextStyle = Literal[1, 2] +SeparatorSpacingSize = Literal[1, 2] class ActionRow(TypedDict): @@ -85,4 +86,58 @@ class SelectMenu(TypedDict): custom_id: str +class TextDisplay(TypedDict): + type: Literal[10] + content: str + + +class UnfurledMediaItem: + url: str + + +class MediaGalleryItem(TypedDict): + media: UnfurledMediaItem + description: NotRequired[str] + spoiler: NotRequired[bool] + + +class SectionComponent(TypedDict): + type: Literal[9] + components: list[TextDisplayComponent, ButtonComponent] + + +class ThumbnailComponent(TypedDict): + type: Literal[11] + media: UnfurledMediaItem + description: NotRequired[str] + spoiler: NotRequired[bool] + + +class MediaGalleryComponent(TypedDict): + type: Literal[12] + items: list[MediaGalleryItem] + + +class FileComponent(TypedDict): + type: Literal[13] + file: UnfurledMediaItem + spoiler: NotRequired[bool] + + +class SeparatorComponent(TypedDict): + type: Literal[14] + divider: NotRequired[bool] + spacing: NotRequired[SeparatorSpacingSize] + + +ContainerComponents = Union[ActionRow, TextDisplayComponent, MediaGalleryComponent, FileComponent, SeparatorComponent, SectionComponent] + + +class ContainerComponent(TypedDict): + type: Literal[17] + accent_color: NotRequired[int] + spoiler: NotRequired[bool] + components: list[ContainerComponents] + + Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] From 468f996e29671d582fc6e68e84536e283676de41 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 04:42:42 +0000 Subject: [PATCH 002/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/types/components.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/discord/types/components.py b/discord/types/components.py index 3c59c322e4..3bca251724 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -130,7 +130,14 @@ class SeparatorComponent(TypedDict): spacing: NotRequired[SeparatorSpacingSize] -ContainerComponents = Union[ActionRow, TextDisplayComponent, MediaGalleryComponent, FileComponent, SeparatorComponent, SectionComponent] +ContainerComponents = Union[ + ActionRow, + TextDisplayComponent, + MediaGalleryComponent, + FileComponent, + SeparatorComponent, + SectionComponent, +] class ContainerComponent(TypedDict): From 905b9ff5e2d8fbc736ba6a6954fe58343a21cc0f Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 05:56:19 +0100 Subject: [PATCH 003/154] textdisplayComponent --- discord/types/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/components.py b/discord/types/components.py index 3bca251724..4360798bdc 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -86,7 +86,7 @@ class SelectMenu(TypedDict): custom_id: str -class TextDisplay(TypedDict): +class TextDisplayComponent(TypedDict): type: Literal[10] content: str From 49080e7d4727f4dd7d700a1d8d1e34674778c07e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 06:10:05 +0100 Subject: [PATCH 004/154] more --- discord/enums.py | 7 +++++ discord/types/components.py | 59 ++++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 0435f7920b..16d4aa9e07 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1070,6 +1070,13 @@ class SubscriptionStatus(Enum): inactive = 2 +class SeparatorSpacingSize(Enum): + """A separator component's spacing size.""" + + small = 1 + large = 2 + + T = TypeVar("T") diff --git a/discord/types/components.py b/discord/types/components.py index 4360798bdc..5a275a6f34 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -39,12 +39,17 @@ SeparatorSpacingSize = Literal[1, 2] -class ActionRow(TypedDict): +class BaseComponent(TypedDict): + type: ComponentType + id: NotRequired[int] + + +class ActionRow(BaseComponent): type: Literal[1] components: list[Component] -class ButtonComponent(TypedDict): +class ButtonComponent(BaseComponent): custom_id: NotRequired[str] url: NotRequired[str] disabled: NotRequired[bool] @@ -55,7 +60,7 @@ class ButtonComponent(TypedDict): sku_id: Snowflake -class InputText(TypedDict): +class InputText(BaseComponent): min_length: NotRequired[int] max_length: NotRequired[int] required: NotRequired[bool] @@ -75,7 +80,7 @@ class SelectOption(TypedDict): default: bool -class SelectMenu(TypedDict): +class SelectMenu(BaseComponent): placeholder: NotRequired[str] min_values: NotRequired[int] max_values: NotRequired[int] @@ -86,50 +91,60 @@ class SelectMenu(TypedDict): custom_id: str -class TextDisplayComponent(TypedDict): +class TextDisplayComponent(BaseComponent): type: Literal[10] content: str -class UnfurledMediaItem: +class SectionComponent(BaseComponent): + type: Literal[9] + components: list[TextDisplayComponent, ButtonComponent] + + +class UnfurledMediaItem(TypedDict): url: str -class MediaGalleryItem(TypedDict): +class ThumbnailComponent(BaseComponent): + type: Literal[11] media: UnfurledMediaItem description: NotRequired[str] spoiler: NotRequired[bool] -class SectionComponent(TypedDict): - type: Literal[9] - components: list[TextDisplayComponent, ButtonComponent] - - -class ThumbnailComponent(TypedDict): - type: Literal[11] +class MediaGalleryItem(TypedDict): media: UnfurledMediaItem description: NotRequired[str] spoiler: NotRequired[bool] -class MediaGalleryComponent(TypedDict): +class MediaGalleryComponent(BaseComponent): type: Literal[12] items: list[MediaGalleryItem] -class FileComponent(TypedDict): +class FileComponent(BaseComponent): type: Literal[13] file: UnfurledMediaItem spoiler: NotRequired[bool] -class SeparatorComponent(TypedDict): +class SeparatorComponent(BaseComponent): type: Literal[14] divider: NotRequired[bool] spacing: NotRequired[SeparatorSpacingSize] +class ContainerComponent(BaseComponent): + type: Literal[17] + accent_color: NotRequired[int] + spoiler: NotRequired[bool] + components: list[ContainerComponents] + + +Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] + + ContainerComponents = Union[ ActionRow, TextDisplayComponent, @@ -138,13 +153,3 @@ class SeparatorComponent(TypedDict): SeparatorComponent, SectionComponent, ] - - -class ContainerComponent(TypedDict): - type: Literal[17] - accent_color: NotRequired[int] - spoiler: NotRequired[bool] - components: list[ContainerComponents] - - -Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] From e3e7aba78321ef8f3a0d56503207020dcc9ca5c5 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 06:57:39 +0100 Subject: [PATCH 005/154] Section, TextDisplay --- discord/components.py | 120 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 11 deletions(-) diff --git a/discord/components.py b/discord/components.py index c80eb5a57c..448a65f562 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,7 +27,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar -from .enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle, try_enum +from .enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle, SeparatorSpacingSize, try_enum from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots @@ -36,9 +36,19 @@ from .types.components import ActionRow as ActionRowPayload from .types.components import ButtonComponent as ButtonComponentPayload from .types.components import Component as ComponentPayload + from .types.components import BaseComponent as BaseComponentPayload from .types.components import InputText as InputTextComponentPayload from .types.components import SelectMenu as SelectMenuPayload from .types.components import SelectOption as SelectOptionPayload + from .types.components import TextDisplayComponent as TextDisplayComponentPayload + from .types.components import SectionComponent as SectionComponentPayload + from .types.components import UnfurledMediaItem as UnfurledMediaItemPayload + from .types.components import ThumbnailComponent as ThumbnailComponentPayload + from .types.components import MediaGalleryItem as MediaGalleryItemPayload + from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload + from .types.components import FileComponent as FileComponentPayload + from .types.components import SeparatorComponent as SeparatorComponentPayload + from .types.components import ContainerComponent as ContainerComponentPayload __all__ = ( "Component", @@ -47,11 +57,12 @@ "SelectMenu", "SelectOption", "InputText", + "Section", + "TextDisplay", ) C = TypeVar("C", bound="Component") - class Component: """Represents a Discord Bot UI Kit Component. @@ -69,12 +80,15 @@ class Component: ---------- type: :class:`ComponentType` The type of component. + id: :class:`str` + The component's ID. """ - __slots__: tuple[str, ...] = ("type",) + __slots__: tuple[str, ...] = ("type", "id") __repr_info__: ClassVar[tuple[str, ...]] type: ComponentType + id: str def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) @@ -494,16 +508,100 @@ def to_dict(self) -> SelectOptionPayload: return payload +class Section(Component): + """Represents a Section from Components V2. + + This is a component that contains other components such as :class:`TextDisplay` and :class:`Thumbnail`. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + components: List[:class:`Component`] + The components contained in this section. + accessory: Optional[:class:`Component`] + The accessory attached to this Section. + """ + + __slots__: tuple[str, ...] = ("components", "accessory") + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: SectionComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.components: list[Component] = [_component_factory(d) for d in data.get("components", [])] + self.accessory: Component | None = None + if _accessory := data.get("accessory"): + self.accessory = _component_factory(_accessory) + + def to_dict(self) -> SectionComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "components": [c.to_dict() for c in self.components] + } + if self.accessory: + payload["accessory"] = self.accessory.to_dict() + return payload + + +class TextDisplay(Component): + """Represents a Text Display from Components V2. + + This is a component that displays text. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + content: :class:`str` + The component's text content. + """ + + __slots__: tuple[str, ...] = ("content",) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: TextDisplayComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.content: str = data.get("content") + + def to_dict(self) -> TextDisplayComponentPayload: + return { + "type": int(self.type), + "id": self.id, + "content": self.content + } + + +COMPONENT_MAPPINGS = { + 1: ActionRow, + 2: Button, + 3: SelectMenu, + 4: InputText, + 5: SelectMenu, + 6: SelectMenu, + 7: SelectMenu, + 8: SelectMenu, + 9: Section, + 10: TextDisplay, + 11: None, + 12: None, + 13: None, + 14: None, + 17: None, +} + def _component_factory(data: ComponentPayload) -> Component: component_type = data["type"] - if component_type == 1: - return ActionRow(data) - elif component_type == 2: - return Button(data) # type: ignore - elif component_type == 4: - return InputText(data) # type: ignore - elif component_type in (3, 5, 6, 7, 8): - return SelectMenu(data) # type: ignore + if cls := COMPONENT_MAPPINGS.get(component_type): + return cls(data) else: as_enum = try_enum(ComponentType, component_type) return Component._raw_construct(type=as_enum) From e961db542e8d2309a9318293f19d503fe1fcab0c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 05:58:03 +0000 Subject: [PATCH 006/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/discord/components.py b/discord/components.py index 448a65f562..d8e49a789d 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,28 +27,35 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar -from .enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle, SeparatorSpacingSize, try_enum +from .enums import ( + ButtonStyle, + ChannelType, + ComponentType, + InputTextStyle, + SeparatorSpacingSize, + try_enum, +) from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots if TYPE_CHECKING: from .emoji import AppEmoji, GuildEmoji from .types.components import ActionRow as ActionRowPayload + from .types.components import BaseComponent as BaseComponentPayload from .types.components import ButtonComponent as ButtonComponentPayload from .types.components import Component as ComponentPayload - from .types.components import BaseComponent as BaseComponentPayload + from .types.components import ContainerComponent as ContainerComponentPayload + from .types.components import FileComponent as FileComponentPayload from .types.components import InputText as InputTextComponentPayload + from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload + from .types.components import MediaGalleryItem as MediaGalleryItemPayload + from .types.components import SectionComponent as SectionComponentPayload from .types.components import SelectMenu as SelectMenuPayload from .types.components import SelectOption as SelectOptionPayload + from .types.components import SeparatorComponent as SeparatorComponentPayload from .types.components import TextDisplayComponent as TextDisplayComponentPayload - from .types.components import SectionComponent as SectionComponentPayload - from .types.components import UnfurledMediaItem as UnfurledMediaItemPayload from .types.components import ThumbnailComponent as ThumbnailComponentPayload - from .types.components import MediaGalleryItem as MediaGalleryItemPayload - from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload - from .types.components import FileComponent as FileComponentPayload - from .types.components import SeparatorComponent as SeparatorComponentPayload - from .types.components import ContainerComponent as ContainerComponentPayload + from .types.components import UnfurledMediaItem as UnfurledMediaItemPayload __all__ = ( "Component", @@ -63,6 +70,7 @@ C = TypeVar("C", bound="Component") + class Component: """Represents a Discord Bot UI Kit Component. @@ -532,7 +540,9 @@ class Section(Component): def __init__(self, data: SectionComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.components: list[Component] = [_component_factory(d) for d in data.get("components", [])] + self.components: list[Component] = [ + _component_factory(d) for d in data.get("components", []) + ] self.accessory: Component | None = None if _accessory := data.get("accessory"): self.accessory = _component_factory(_accessory) @@ -541,7 +551,7 @@ def to_dict(self) -> SectionComponentPayload: payload = { "type": int(self.type), "id": self.id, - "components": [c.to_dict() for c in self.components] + "components": [c.to_dict() for c in self.components], } if self.accessory: payload["accessory"] = self.accessory.to_dict() @@ -573,11 +583,7 @@ def __init__(self, data: TextDisplayComponentPayload): self.content: str = data.get("content") def to_dict(self) -> TextDisplayComponentPayload: - return { - "type": int(self.type), - "id": self.id, - "content": self.content - } + return {"type": int(self.type), "id": self.id, "content": self.content} COMPONENT_MAPPINGS = { @@ -598,6 +604,7 @@ def to_dict(self) -> TextDisplayComponentPayload: 17: None, } + def _component_factory(data: ComponentPayload) -> Component: component_type = data["type"] if cls := COMPONENT_MAPPINGS.get(component_type): From 947890d6fc72c0c22dee8eb99ebf45d5769404ed Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:40:37 +0100 Subject: [PATCH 007/154] remaining classes --- discord/components.py | 224 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 219 insertions(+), 5 deletions(-) diff --git a/discord/components.py b/discord/components.py index d8e49a789d..a0aa713243 100644 --- a/discord/components.py +++ b/discord/components.py @@ -35,6 +35,7 @@ SeparatorSpacingSize, try_enum, ) +from .colour import Colour from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots @@ -586,6 +587,219 @@ def to_dict(self) -> TextDisplayComponentPayload: return {"type": int(self.type), "id": self.id, "content": self.content} +class UnfurledMediaItem: + + def __init__(self, data: UnfurledMediaItemPayload): + self.url = data.get("url") + # need to test this more + + def to_dict(self): + return {"url": self.url} + + +class Thumbnail(Component): + """Represents a Thumbnail from Components V2. + + This is a component that displays media such as images and videos. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + media: :class:`UnfurledMediaItem` + The component's media URL. + description: Optional[:class:`str`] + The thumbnail's description, up to 1024 characters. + spoiler: Optional[:class:`bool`] + Whether the thumbnail is a spoiler. + """ + + __slots__: tuple[str, ...] = ("media", "description", "spoiler", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: ThumbnailComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.description: str | None = data.get("description") + self.spoiler: bool | None = data.get("spoiler") + + def to_dict(self) -> ThumbnailComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "media": self.media.to_dict() + } + if self.description: + payload["description"] = self.description + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class MediaGalleryItem: + + def __init__(self, data: MediaGalleryItemPayload): + self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.description: str | None = data.get("description") + self.spoiler: bool | None = data.get("spoiler") + + def to_dict(self): + payload = { + "media": self.media.to_dict() + } + if self.description: + payload["description"] = self.description + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class MediaGallery(Component): + """Represents a Media Gallery from Components V2. + + This is a component that displays up to 10 different :class:`MediaGalleryItem`s. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The media this gallery contains. + """ + + __slots__: tuple[str, ...] = ("items", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: MediaGalleryComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.items: list[MediaGalleryItem] = [MediaGalleryItem(d) for d in data.get("items", [])] + + def to_dict(self) -> MediaGalleryComponentPayload: + return { + "type": int(self.type), + "id": self.id, + "items": [i.to_dict() for i in self.items] + } + + +class FileComponent(Component): + """Represents a File from Components V2. + + This is a component that displays some file (elaborate?). + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + file: :class:`UnfurledMediaItem` + The file's media URL. + spoiler: Optional[:class:`bool`] + Whether the file is a spoiler. + """ + + __slots__: tuple[str, ...] = ("file", "spoiler", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: FileComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.spoiler: bool | None = data.get("spoiler") + + def to_dict(self) -> FileComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "file": self.file.to_dict() + } + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class Separator(Component): + """Represents a Separator from Components V2. + + This is a component that separates components. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + divider: :class:`bool` + Whether the separator is a divider (provide example?) + spacing: Optional[:class:`SeparatorSpacingSize`] + The separator's spacing size. + """ + + __slots__: tuple[str, ...] = ("divider", "spacing",) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: SeparatorComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.divider: bool = data.get("divider") + self.spacing: SeparatorSpacingSize = try_enum(SeparatorSpacingSize, data.get("spacing", 1)) + + def to_dict(self) -> SeparatorComponentPayload: + return {"type": int(self.type), "id": self.id, "divider": self.divider, "spacing": int(self.spacing)} + + +class Container(Component): + """Represents a Container from Components V2. + + This is a component that contains up to 10 different :class:`Component`s. + It may only contain :class:`ActionRow`, :class:`TextDisplay`, :class:`Section`, :class:`MediaGallery`, :class:`Separator`, and :class:`FileComponent`. + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The media this gallery contains. + """ + + __slots__: tuple[str, ...] = ("accent_color", "spoiler", "components", ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + + def __init__(self, data: ContainerComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") + self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour(c) # at this point, not adding alternative spelling + self.spoiler: bool | None = data.get("spoiler") + self.components: list[Component] = [ + _component_factory(d) for d in data.get("components", []) + ] + + def to_dict(self) -> ContainerComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "components": [c.to_dict() for c in self.components], + } + if self.accent_color: + payload["accent_color"] = self.accent_color.value + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + COMPONENT_MAPPINGS = { 1: ActionRow, 2: Button, @@ -597,11 +811,11 @@ def to_dict(self) -> TextDisplayComponentPayload: 8: SelectMenu, 9: Section, 10: TextDisplay, - 11: None, - 12: None, - 13: None, - 14: None, - 17: None, + 11: Thumbnail, + 12: MediaGallery, + 13: FileComponent, + 14: Separator, + 17: Container, } From 6e7dde99b7ee10dae26d73762189f48181d3ac0b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 06:41:30 +0000 Subject: [PATCH 008/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 77 +++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/discord/components.py b/discord/components.py index a0aa713243..2d6fe407c6 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from .colour import Colour from .enums import ( ButtonStyle, ChannelType, @@ -35,7 +36,6 @@ SeparatorSpacingSize, try_enum, ) -from .colour import Colour from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots @@ -616,23 +616,25 @@ class Thumbnail(Component): Whether the thumbnail is a spoiler. """ - __slots__: tuple[str, ...] = ("media", "description", "spoiler", ) + __slots__: tuple[str, ...] = ( + "media", + "description", + "spoiler", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: ThumbnailComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.media: UnfurledMediaItem = ( + umi := data.get("media") + ) and UnfurledMediaItem(umi) self.description: str | None = data.get("description") self.spoiler: bool | None = data.get("spoiler") def to_dict(self) -> ThumbnailComponentPayload: - payload = { - "type": int(self.type), - "id": self.id, - "media": self.media.to_dict() - } + payload = {"type": int(self.type), "id": self.id, "media": self.media.to_dict()} if self.description: payload["description"] = self.description if self.spoiler is not None: @@ -641,16 +643,16 @@ def to_dict(self) -> ThumbnailComponentPayload: class MediaGalleryItem: - + def __init__(self, data: MediaGalleryItemPayload): - self.media: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.media: UnfurledMediaItem = ( + umi := data.get("media") + ) and UnfurledMediaItem(umi) self.description: str | None = data.get("description") self.spoiler: bool | None = data.get("spoiler") def to_dict(self): - payload = { - "media": self.media.to_dict() - } + payload = {"media": self.media.to_dict()} if self.description: payload["description"] = self.description if self.spoiler is not None: @@ -673,20 +675,22 @@ class MediaGallery(Component): The media this gallery contains. """ - __slots__: tuple[str, ...] = ("items", ) + __slots__: tuple[str, ...] = ("items",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: MediaGalleryComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.items: list[MediaGalleryItem] = [MediaGalleryItem(d) for d in data.get("items", [])] + self.items: list[MediaGalleryItem] = [ + MediaGalleryItem(d) for d in data.get("items", []) + ] def to_dict(self) -> MediaGalleryComponentPayload: return { "type": int(self.type), "id": self.id, - "items": [i.to_dict() for i in self.items] + "items": [i.to_dict() for i in self.items], } @@ -707,22 +711,23 @@ class FileComponent(Component): Whether the file is a spoiler. """ - __slots__: tuple[str, ...] = ("file", "spoiler", ) + __slots__: tuple[str, ...] = ( + "file", + "spoiler", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: FileComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem(umi) + self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem( + umi + ) self.spoiler: bool | None = data.get("spoiler") def to_dict(self) -> FileComponentPayload: - payload = { - "type": int(self.type), - "id": self.id, - "file": self.file.to_dict() - } + payload = {"type": int(self.type), "id": self.id, "file": self.file.to_dict()} if self.spoiler is not None: payload["spoiler"] = self.spoiler return payload @@ -745,17 +750,27 @@ class Separator(Component): The separator's spacing size. """ - __slots__: tuple[str, ...] = ("divider", "spacing",) + __slots__: tuple[str, ...] = ( + "divider", + "spacing", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: SeparatorComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.divider: bool = data.get("divider") - self.spacing: SeparatorSpacingSize = try_enum(SeparatorSpacingSize, data.get("spacing", 1)) + self.spacing: SeparatorSpacingSize = try_enum( + SeparatorSpacingSize, data.get("spacing", 1) + ) def to_dict(self) -> SeparatorComponentPayload: - return {"type": int(self.type), "id": self.id, "divider": self.divider, "spacing": int(self.spacing)} + return { + "type": int(self.type), + "id": self.id, + "divider": self.divider, + "spacing": int(self.spacing), + } class Container(Component): @@ -774,14 +789,20 @@ class Container(Component): The media this gallery contains. """ - __slots__: tuple[str, ...] = ("accent_color", "spoiler", "components", ) + __slots__: tuple[str, ...] = ( + "accent_color", + "spoiler", + "components", + ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ def __init__(self, data: ContainerComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour(c) # at this point, not adding alternative spelling + self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour( + c + ) # at this point, not adding alternative spelling self.spoiler: bool | None = data.get("spoiler") self.components: list[Component] = [ _component_factory(d) for d in data.get("components", []) From d18a9c48091672110c9bf2aac0a3f0cf6cf7d224 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 08:52:29 +0100 Subject: [PATCH 009/154] basic view support start --- discord/types/components.py | 1 + discord/ui/section.py | 102 ++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 discord/ui/section.py diff --git a/discord/types/components.py b/discord/types/components.py index 5a275a6f34..d532a2b590 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -99,6 +99,7 @@ class TextDisplayComponent(BaseComponent): class SectionComponent(BaseComponent): type: Literal[9] components: list[TextDisplayComponent, ButtonComponent] + accessory: NotRequired[Component] class UnfurledMediaItem(TypedDict): diff --git a/discord/ui/section.py b/discord/ui/section.py new file mode 100644 index 0000000000..272f74d085 --- /dev/null +++ b/discord/ui/section.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from ..components import Section as SectionComponent +from ..enums import ComponentType +from .item import Item + +__all__ = ("InputText",) + +if TYPE_CHECKING: + from ..types.components import SectionComponent as SectionComponentPayload + + +class Section: + """Represents a UI section. + + .. versionadded:: 2.7 + + Parameters + ---------- + *items: :class:`Item` + The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. + accessory: Optional[:class:`Item`] + This section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. + """ + + def __init__( + self, + *items: Item, + accessory: Item = None + ): + super().__init__() + + self.items = items + self.accessory = accessory + components = [] + + self._underlying = SectionComponent._raw_construct( + type=ComponentType.section, + components=components, + accessory=accessory, + ) + + def add_item(self, item: Item) -> None: + """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 (10). + """ + + 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}") + + self.items.append(item) + + def add_text(self, content: str) -> None: + """Adds a :class:`TextDisplay` to the section. + + Parameters + ---------- + content: :class:`str` + The content of the TextDisplay + + 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") + + text = ... + + self.items.append(text) + + def add_button(self, label: str, ) -> None: + """finish""" + pass + + @property + def type(self) -> ComponentType: + return self._underlying.type + + def to_component_dict(self) -> SectionComponentPayload: + return self._underlying.to_dict() From 5ab45fd41d9d32dd8a0fc3717302455263f83e16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:52:51 +0000 Subject: [PATCH 010/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 272f74d085..ff54bb1e30 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os from typing import TYPE_CHECKING from ..components import Section as SectionComponent @@ -26,11 +25,7 @@ class Section: This section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. """ - def __init__( - self, - *items: Item, - accessory: Item = None - ): + def __init__(self, *items: Item, accessory: Item = None): super().__init__() self.items = items @@ -85,14 +80,16 @@ def add_text(self, content: str) -> None: if len(self.items) >= 3: raise ValueError("maximum number of children exceeded") - + text = ... self.items.append(text) - - def add_button(self, label: str, ) -> None: + + def add_button( + self, + label: str, + ) -> None: """finish""" - pass @property def type(self) -> ComponentType: From fb8d13d37db86bbd7749a013ec92889fa863fbfe Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:23:15 +0100 Subject: [PATCH 011/154] flag clarification --- discord/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/flags.py b/discord/flags.py index bd6370af05..1406bdcea0 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -413,7 +413,7 @@ def is_voice_message(self): @flag_value def is_components_v2(self): - """:class:`bool`: Returns ``True`` if this message has v2 components. This flag disables sending `content` and `embeds`. + """:class:`bool`: Returns ``True`` if this message has v2 components. This flag disables sending `content`, `embed`, and `embeds`. .. versionadded:: 2.7 """ From 65dc63d78289010059b233c05b3a3594a3242864 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 7 Feb 2025 05:53:33 +0100 Subject: [PATCH 012/154] complete models --- discord/components.py | 11 ++++++++++- discord/enums.py | 1 + discord/types/components.py | 11 ++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 2d6fe407c6..89504a1ff6 100644 --- a/discord/components.py +++ b/discord/components.py @@ -38,6 +38,7 @@ ) from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots +from .flags import AttachmentFlags if TYPE_CHECKING: from .emoji import AppEmoji, GuildEmoji @@ -591,7 +592,15 @@ class UnfurledMediaItem: def __init__(self, data: UnfurledMediaItemPayload): self.url = data.get("url") - # need to test this more + self.proxy_url: str = data.get("proxy_url") + self.height: int | None = data.get("height") + self.width: int | None = data.get("width") + self.content_type: str | None = data.get("content_type") + self.flags: AttachmentFlags = AttachmentFlags._from_value(data.get("flags", 0)) + self.placeholder: str = data.get("placeholder") + self.placeholder_version: int = data.get("placeholder_version") + self.loading_state: int = data.get("loading_state") + self.src_is_animated: bool = data.get("src_is_animated") def to_dict(self): return {"url": self.url} diff --git a/discord/enums.py b/discord/enums.py index 16d4aa9e07..c52f3ee2ab 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -718,6 +718,7 @@ class ComponentType(Enum): media_gallery = 12 file = 13 separator = 14 + content_inventory_entry = 16 container = 17 def __int__(self): diff --git a/discord/types/components.py b/discord/types/components.py index d532a2b590..a689c09d0d 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -33,7 +33,7 @@ from .emoji import PartialEmoji from .snowflake import Snowflake -ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17] +ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] InputTextStyle = Literal[1, 2] SeparatorSpacingSize = Literal[1, 2] @@ -104,6 +104,15 @@ class SectionComponent(BaseComponent): class UnfurledMediaItem(TypedDict): url: str + proxy_url: str + height: NotRequired[int | None] + width: NotRequired[int | None] + content_type: NotRequired[str] + src_is_animated: NotRequired[bool] + placeholder: str + placeholder_version: int + loading_state: int + flags: NotRequired[int] class ThumbnailComponent(BaseComponent): From f86f7078398caebe90963fa12e245139cba88b0f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 04:54:15 +0000 Subject: [PATCH 013/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 89504a1ff6..5a6cbfb01f 100644 --- a/discord/components.py +++ b/discord/components.py @@ -36,9 +36,9 @@ SeparatorSpacingSize, try_enum, ) +from .flags import AttachmentFlags from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, get_slots -from .flags import AttachmentFlags if TYPE_CHECKING: from .emoji import AppEmoji, GuildEmoji From 3e03e842e6766ffa2933bb8d02f4d8c96607753f Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:21:52 +0100 Subject: [PATCH 014/154] fix --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index ff54bb1e30..49b3de3f45 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -6,7 +6,7 @@ from ..enums import ComponentType from .item import Item -__all__ = ("InputText",) +__all__ = ("Section",) if TYPE_CHECKING: from ..types.components import SectionComponent as SectionComponentPayload From 0f8c20a1e379fc3c498310717f632e0d4ab67aec Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:24:22 +0100 Subject: [PATCH 015/154] fix2 --- discord/types/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/types/components.py b/discord/types/components.py index a689c09d0d..3bc75ef01d 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -149,13 +149,13 @@ class ContainerComponent(BaseComponent): type: Literal[17] accent_color: NotRequired[int] spoiler: NotRequired[bool] - components: list[ContainerComponents] + components: list[AllowedContainerComponents] Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText] -ContainerComponents = Union[ +AllowedContainerComponents = Union[ ActionRow, TextDisplayComponent, MediaGalleryComponent, From bf60ffcc437f29e1c70c5ad618dfb32548325eca Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 9 Feb 2025 05:07:14 +0100 Subject: [PATCH 016/154] update loading_state --- discord/components.py | 3 ++- discord/enums.py | 9 +++++++++ discord/types/components.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 5a6cbfb01f..571e20d87c 100644 --- a/discord/components.py +++ b/discord/components.py @@ -34,6 +34,7 @@ ComponentType, InputTextStyle, SeparatorSpacingSize, + MediaItemLoadingState, try_enum, ) from .flags import AttachmentFlags @@ -599,7 +600,7 @@ def __init__(self, data: UnfurledMediaItemPayload): self.flags: AttachmentFlags = AttachmentFlags._from_value(data.get("flags", 0)) self.placeholder: str = data.get("placeholder") self.placeholder_version: int = data.get("placeholder_version") - self.loading_state: int = data.get("loading_state") + self.loading_state: MediaItemLoadingState = try_enum(MediaItemLoadingState, data.get("loading_state")) self.src_is_animated: bool = data.get("src_is_animated") def to_dict(self): diff --git a/discord/enums.py b/discord/enums.py index c52f3ee2ab..d8cd1f3aec 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1078,6 +1078,15 @@ class SeparatorSpacingSize(Enum): large = 2 +class MediaItemLoadingState(Enum): + """An :class:`~discord.UnfurledMediaItem`'s ``loading_state``.""" + + unknown = 0 + loading = 1 + loaded_success = 2 + loaded_not_found = 3 + + T = TypeVar("T") diff --git a/discord/types/components.py b/discord/types/components.py index 3bc75ef01d..67af33f48f 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -37,6 +37,7 @@ ButtonStyle = Literal[1, 2, 3, 4, 5, 6] InputTextStyle = Literal[1, 2] SeparatorSpacingSize = Literal[1, 2] +LoadingState = Literal[0, 1, 2, 3] class BaseComponent(TypedDict): @@ -111,7 +112,7 @@ class UnfurledMediaItem(TypedDict): src_is_animated: NotRequired[bool] placeholder: str placeholder_version: int - loading_state: int + loading_state: LoadingState flags: NotRequired[int] From 5080ff7dfd8f9bc87a54e3819e83123cd5f951ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2025 04:07:37 +0000 Subject: [PATCH 017/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 571e20d87c..85ae05bdd4 100644 --- a/discord/components.py +++ b/discord/components.py @@ -33,8 +33,8 @@ ChannelType, ComponentType, InputTextStyle, - SeparatorSpacingSize, MediaItemLoadingState, + SeparatorSpacingSize, try_enum, ) from .flags import AttachmentFlags @@ -600,7 +600,9 @@ def __init__(self, data: UnfurledMediaItemPayload): self.flags: AttachmentFlags = AttachmentFlags._from_value(data.get("flags", 0)) self.placeholder: str = data.get("placeholder") self.placeholder_version: int = data.get("placeholder_version") - self.loading_state: MediaItemLoadingState = try_enum(MediaItemLoadingState, data.get("loading_state")) + self.loading_state: MediaItemLoadingState = try_enum( + MediaItemLoadingState, data.get("loading_state") + ) self.src_is_animated: bool = data.get("src_is_animated") def to_dict(self): From 813812eba8876498db066d23f0e3b5642edb74f5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 23:49:13 +0000 Subject: [PATCH 018/154] style(pre-commit): auto fixes from pre-commit.com hooks --- docs/_static/css/custom.css | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index cd2b41d9a2..57ef457b9e 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -8,8 +8,9 @@ font-display: swap; src: url(https://fonts.gstatic.com/s/saira/v8/memWYa2wxmKQyPMrZX79wwYZQMhsyuShhKMjjbU9uXuA773FksAxljYm.woff2) format("woff2"); - unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, - U+01AF-01B0, U+1EA0-1EF9, U+20AB; + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, + U+1EA0-1EF9, U+20AB; } /* latin-ext */ @font-face { @@ -20,8 +21,9 @@ font-display: swap; src: url(https://fonts.gstatic.com/s/saira/v8/memWYa2wxmKQyPMrZX79wwYZQMhsyuShhKMjjbU9uXuA773FksExljYm.woff2) format("woff2"); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, - U+2113, U+2C60-2C7F, U+A720-A7FF; + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, + U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { @@ -32,8 +34,9 @@ font-display: swap; src: url(https://fonts.gstatic.com/s/saira/v8/memWYa2wxmKQyPMrZX79wwYZQMhsyuShhKMjjbU9uXuA773Fks8xlg.woff2) format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, + U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* latin */ @font-face { @@ -43,8 +46,9 @@ font-display: swap; src: url(https://fonts.gstatic.com/s/outfit/v4/QGYyz_MVcBeNP4NjuGObqx1XmO1I4W61O4a0Ew.woff2) format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, + U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* latin */ @font-face { @@ -54,8 +58,9 @@ font-display: swap; src: url(https://fonts.gstatic.com/s/outfit/v4/QGYyz_MVcBeNP4NjuGObqx1XmO1I4deyO4a0Ew.woff2) format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, + U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* attribute tables */ From 066d8ca4aa6bdea508e6852e69be128ae2d639f5 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:55:19 +0100 Subject: [PATCH 019/154] finish section fr fr --- discord/components.py | 16 +++++++++++++++- discord/ui/section.py | 44 +++++++++++++++++++++++++++++++++++++++---- discord/ui/view.py | 14 +++++++++++++- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/discord/components.py b/discord/components.py index 85ae05bdd4..27b35936f7 100644 --- a/discord/components.py +++ b/discord/components.py @@ -75,7 +75,7 @@ class Component: - """Represents a Discord Bot UI Kit Component. + """Represents a Discord Bot UI Kit V1 Component. Currently, the only components supported by Discord are: @@ -100,6 +100,7 @@ class Component: __repr_info__: ClassVar[tuple[str, ...]] type: ComponentType id: str + versions: tuple[int, ...] def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) @@ -120,6 +121,8 @@ def _raw_construct(cls: type[C], **kwargs) -> C: def to_dict(self) -> dict[str, Any]: raise NotImplementedError + def is_v2(self) -> bool: + return self.versions and 1 not in self.versions class ActionRow(Component): """Represents a Discord Bot UI Kit Action Row. @@ -141,6 +144,7 @@ class ActionRow(Component): __slots__: tuple[str, ...] = ("children",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2, ) def __init__(self, data: ComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -193,6 +197,7 @@ class InputText(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2, ) def __init__(self, data: InputTextComponentPayload): self.type = ComponentType.input_text @@ -274,6 +279,7 @@ class Button(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2, ) def __init__(self, data: ButtonComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -366,6 +372,7 @@ class SelectMenu(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2, ) def __init__(self, data: SelectMenuPayload): self.type = try_enum(ComponentType, data["type"]) @@ -539,6 +546,7 @@ class Section(Component): __slots__: tuple[str, ...] = ("components", "accessory") __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2, ) def __init__(self, data: SectionComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -579,6 +587,7 @@ class TextDisplay(Component): __slots__: tuple[str, ...] = ("content",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2, ) def __init__(self, data: TextDisplayComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -635,6 +644,7 @@ class Thumbnail(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2, ) def __init__(self, data: ThumbnailComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -690,6 +700,7 @@ class MediaGallery(Component): __slots__: tuple[str, ...] = ("items",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2, ) def __init__(self, data: MediaGalleryComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -729,6 +740,7 @@ class FileComponent(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2, ) def __init__(self, data: FileComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -768,6 +780,7 @@ class Separator(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2, ) def __init__(self, data: SeparatorComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -808,6 +821,7 @@ class Container(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2, ) def __init__(self, data: ContainerComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) diff --git a/discord/ui/section.py b/discord/ui/section.py index 49b3de3f45..b61e7f3c38 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, TypeVar from ..components import Section as SectionComponent +from ..components import _component_factory from ..enums import ComponentType from .item import Item @@ -12,7 +13,10 @@ from ..types.components import SectionComponent as SectionComponentPayload -class Section: +S = TypeVar("S", bound="Section") +V = TypeVar("V", bound="View", covariant=True) + +class Section(Item[V]): """Represents a UI section. .. versionadded:: 2.7 @@ -30,12 +34,12 @@ def __init__(self, *items: Item, accessory: Item = None): self.items = items self.accessory = accessory - components = [] + components = [i._underlying for i in items] self._underlying = SectionComponent._raw_construct( type=ComponentType.section, components=components, - accessory=accessory, + accessory=accessory._underlying, ) def add_item(self, item: Item) -> None: @@ -91,9 +95,41 @@ def add_button( ) -> None: """finish""" + def set_accessory(self, item: Item) -> None: + """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.Thumbnail` and :class:`~discord.ui.Button`. + + Raises + ------ + TypeError + An :class:`Item` was not passed. + """ + + if not isinstance(item, Item): + raise TypeError(f"expected Item not {item.__class__!r}") + + self.accessory = item + @property def type(self) -> ComponentType: return self._underlying.type def to_component_dict(self) -> SectionComponentPayload: return self._underlying.to_dict() + + @classmethod + def from_component(cls: type[S], component: SectionComponent) -> S: + from .view import _component_to_item + + items = [_component_to_item(c) for c in component.components] + accessory = _component_to_item(component.accessory) + return cls( + *items, + accessory = accessory + ) + + callback = None diff --git a/discord/ui/view.py b/discord/ui/view.py index c54cb58f13..8b3cef33fa 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -37,12 +37,13 @@ from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent from ..components import Component +from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType -__all__ = ("View",) +__all__ = ("View", "_component_to_item") if TYPE_CHECKING: @@ -69,6 +70,10 @@ def _component_to_item(component: Component) -> Item: from .select import Select return Select.from_component(component) + if isinstance(component, SectionComponent): + from .section import Section + + return Section.from_component(component) return Item.from_component(component) @@ -515,6 +520,13 @@ def is_persistent(self) -> bool: item.is_persistent() for item in self.children ) + def is_v2(self) -> bool: + """Whether the view contains V2 components. + + A view containing V2 components may not be sent alongside message content or embeds. + """ + return any([item._underlying.is_v2() for item in self.children]) + async def wait(self) -> bool: """Waits until the view has finished interacting. From a054ecfee93f2920ec49da9ff7e5f27c0ef31a18 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 06:55:43 +0000 Subject: [PATCH 020/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 35 ++++++++++++++++++++++++----------- discord/ui/section.py | 12 +++++------- discord/ui/view.py | 2 +- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/discord/components.py b/discord/components.py index 27b35936f7..00ffb8e1b2 100644 --- a/discord/components.py +++ b/discord/components.py @@ -124,6 +124,7 @@ def to_dict(self) -> dict[str, Any]: def is_v2(self) -> bool: return self.versions and 1 not in self.versions + class ActionRow(Component): """Represents a Discord Bot UI Kit Action Row. @@ -144,7 +145,10 @@ class ActionRow(Component): __slots__: tuple[str, ...] = ("children",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (1, 2, ) + versions: tuple[int, ...] = ( + 1, + 2, + ) def __init__(self, data: ComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -197,7 +201,10 @@ class InputText(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (1, 2, ) + versions: tuple[int, ...] = ( + 1, + 2, + ) def __init__(self, data: InputTextComponentPayload): self.type = ComponentType.input_text @@ -279,7 +286,10 @@ class Button(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (1, 2, ) + versions: tuple[int, ...] = ( + 1, + 2, + ) def __init__(self, data: ButtonComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -372,7 +382,10 @@ class SelectMenu(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (1, 2, ) + versions: tuple[int, ...] = ( + 1, + 2, + ) def __init__(self, data: SelectMenuPayload): self.type = try_enum(ComponentType, data["type"]) @@ -546,7 +559,7 @@ class Section(Component): __slots__: tuple[str, ...] = ("components", "accessory") __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (2, ) + versions: tuple[int, ...] = (2,) def __init__(self, data: SectionComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -587,7 +600,7 @@ class TextDisplay(Component): __slots__: tuple[str, ...] = ("content",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (2, ) + versions: tuple[int, ...] = (2,) def __init__(self, data: TextDisplayComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -644,7 +657,7 @@ class Thumbnail(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (2, ) + versions: tuple[int, ...] = (2,) def __init__(self, data: ThumbnailComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -700,7 +713,7 @@ class MediaGallery(Component): __slots__: tuple[str, ...] = ("items",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (2, ) + versions: tuple[int, ...] = (2,) def __init__(self, data: MediaGalleryComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -740,7 +753,7 @@ class FileComponent(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (2, ) + versions: tuple[int, ...] = (2,) def __init__(self, data: FileComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -780,7 +793,7 @@ class Separator(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (2, ) + versions: tuple[int, ...] = (2,) def __init__(self, data: SeparatorComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -821,7 +834,7 @@ class Container(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (2, ) + versions: tuple[int, ...] = (2,) def __init__(self, data: ContainerComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) diff --git a/discord/ui/section.py b/discord/ui/section.py index b61e7f3c38..21c6880477 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, TypeVar +from typing import TYPE_CHECKING, TypeVar from ..components import Section as SectionComponent from ..components import _component_factory @@ -16,6 +16,7 @@ S = TypeVar("S", bound="Section") V = TypeVar("V", bound="View", covariant=True) + class Section(Item[V]): """Represents a UI section. @@ -124,12 +125,9 @@ def to_component_dict(self) -> SectionComponentPayload: @classmethod def from_component(cls: type[S], component: SectionComponent) -> S: from .view import _component_to_item - + items = [_component_to_item(c) for c in component.components] accessory = _component_to_item(component.accessory) - return cls( - *items, - accessory = accessory - ) - + return cls(*items, accessory=accessory) + callback = None diff --git a/discord/ui/view.py b/discord/ui/view.py index 8b3cef33fa..43f7f17f43 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -522,7 +522,7 @@ def is_persistent(self) -> bool: def is_v2(self) -> bool: """Whether the view contains V2 components. - + A view containing V2 components may not be sent alongside message content or embeds. """ return any([item._underlying.is_v2() for item in self.children]) From 8c568adee8ffee3ca4dcbf9ef5d6c6b2e5235281 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:04:02 +0100 Subject: [PATCH 021/154] small docs --- discord/components.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/discord/components.py b/discord/components.py index 00ffb8e1b2..6da5e2870d 100644 --- a/discord/components.py +++ b/discord/components.py @@ -69,19 +69,31 @@ "InputText", "Section", "TextDisplay", + "Thumbnail", + "MediaGallery", + "FileComponent", + "Separator", + "Container" ) C = TypeVar("C", bound="Component") class Component: - """Represents a Discord Bot UI Kit V1 Component. + """Represents a Discord Bot UI Kit Component. - Currently, the only components supported by Discord are: + The components supported by Discord in messages are as follows: - :class:`ActionRow` - :class:`Button` - :class:`SelectMenu` + - :class:`Section` + - :class:`TextDisplay` + - :class:`Thumbnail` + - :class:`MediaGallery` + - :class:`FileComponent` + - :class:`Separator` + - :class:`Container` This class is abstract and cannot be instantiated. @@ -122,6 +134,7 @@ def to_dict(self) -> dict[str, Any]: raise NotImplementedError def is_v2(self) -> bool: + """Whether this component was introduced in Components V2.""" return self.versions and 1 not in self.versions @@ -145,10 +158,7 @@ class ActionRow(Component): __slots__: tuple[str, ...] = ("children",) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = ( - 1, - 2, - ) + versions: tuple[int, ...] = (1, 2) def __init__(self, data: ComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -201,10 +211,7 @@ class InputText(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = ( - 1, - 2, - ) + versions: tuple[int, ...] = (1, 2) def __init__(self, data: InputTextComponentPayload): self.type = ComponentType.input_text @@ -286,10 +293,7 @@ class Button(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = ( - 1, - 2, - ) + versions: tuple[int, ...] = (1, 2) def __init__(self, data: ButtonComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -382,10 +386,7 @@ class SelectMenu(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = ( - 1, - 2, - ) + versions: tuple[int, ...] = (1, 2) def __init__(self, data: SelectMenuPayload): self.type = try_enum(ComponentType, data["type"]) From 6b440f40b4a9b24e3bb43044badc2736c8d4a30c Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:04:27 +0100 Subject: [PATCH 022/154] section import --- discord/ui/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index fa1767d220..fba15ac891 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -13,4 +13,5 @@ from .item import * from .modal import * from .select import * +from .section import * from .view import * From bce9af3a0a71d5ffd5a7cef9e64a5c60e24d62b0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:04:55 +0000 Subject: [PATCH 023/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 2 +- discord/ui/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 6da5e2870d..3f0e682564 100644 --- a/discord/components.py +++ b/discord/components.py @@ -73,7 +73,7 @@ "MediaGallery", "FileComponent", "Separator", - "Container" + "Container", ) C = TypeVar("C", bound="Component") diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index fba15ac891..8c8dd1531d 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -12,6 +12,6 @@ from .input_text import * from .item import * from .modal import * -from .select import * from .section import * +from .select import * from .view import * From 61dd2c92a360f0980e31f27c0860139eeba6b73e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:16:27 +0100 Subject: [PATCH 024/154] construct id --- discord/ui/section.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/section.py b/discord/ui/section.py index 21c6880477..2197513b04 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -41,6 +41,7 @@ def __init__(self, *items: Item, accessory: Item = None): type=ComponentType.section, components=components, accessory=accessory._underlying, + id=None ) def add_item(self, item: Item) -> None: From 00eeaa2d8958849a4343f46016d6459bf138e8b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:16:51 +0000 Subject: [PATCH 025/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 2197513b04..ae237e4ac8 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -41,7 +41,7 @@ def __init__(self, *items: Item, accessory: Item = None): type=ComponentType.section, components=components, accessory=accessory._underlying, - id=None + id=None, ) def add_item(self, item: Item) -> None: From 9de50417baeee9a8178b7cd1836bd2ed84439979 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:22:02 +0100 Subject: [PATCH 026/154] id...? --- discord/components.py | 5 ++++- discord/ui/section.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 3f0e682564..fa3d7e98c4 100644 --- a/discord/components.py +++ b/discord/components.py @@ -111,7 +111,6 @@ class Component: __repr_info__: ClassVar[tuple[str, ...]] type: ComponentType - id: str versions: tuple[int, ...] def __repr__(self) -> str: @@ -162,6 +161,7 @@ class ActionRow(Component): def __init__(self, data: ComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") self.children: list[Component] = [ _component_factory(d) for d in data.get("components", []) ] @@ -215,6 +215,7 @@ class InputText(Component): def __init__(self, data: InputTextComponentPayload): self.type = ComponentType.input_text + self.id: str = data.get("id") self.style: InputTextStyle = try_enum(InputTextStyle, data["style"]) self.custom_id = data["custom_id"] self.label: str = data.get("label", None) @@ -297,6 +298,7 @@ class Button(Component): def __init__(self, data: ButtonComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") self.style: ButtonStyle = try_enum(ButtonStyle, data["style"]) self.custom_id: str | None = data.get("custom_id") self.url: str | None = data.get("url") @@ -390,6 +392,7 @@ class SelectMenu(Component): def __init__(self, data: SelectMenuPayload): self.type = try_enum(ComponentType, data["type"]) + self.id: str = data.get("id") self.custom_id: str = data["custom_id"] self.placeholder: str | None = data.get("placeholder") self.min_values: int = data.get("min_values", 1) diff --git a/discord/ui/section.py b/discord/ui/section.py index ae237e4ac8..21c6880477 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -41,7 +41,6 @@ def __init__(self, *items: Item, accessory: Item = None): type=ComponentType.section, components=components, accessory=accessory._underlying, - id=None, ) def add_item(self, item: Item) -> None: From 3bd4b5b817c9b7866d7c41780900c9e774ae19ae Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:23:27 +0100 Subject: [PATCH 027/154] bound --- discord/ui/section.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/section.py b/discord/ui/section.py index 21c6880477..e8a04e2da9 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from ..types.components import SectionComponent as SectionComponentPayload + from .view import View S = TypeVar("S", bound="Section") From f890a014b123e182ac5cf0a84c19f0061b940cab Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:28:31 +0100 Subject: [PATCH 028/154] accessory fix --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index e8a04e2da9..32c1ba0db1 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -41,7 +41,7 @@ def __init__(self, *items: Item, accessory: Item = None): self._underlying = SectionComponent._raw_construct( type=ComponentType.section, components=components, - accessory=accessory._underlying, + accessory=accessory and accessory._underlying, ) def add_item(self, item: Item) -> None: From 6bf9cc01e15e3085a689cb00a2c5321bc9e297a4 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:32:30 +0100 Subject: [PATCH 029/154] once more --- discord/ui/section.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/section.py b/discord/ui/section.py index 32c1ba0db1..1f2e55f6dc 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -42,6 +42,7 @@ def __init__(self, *items: Item, accessory: Item = None): type=ComponentType.section, components=components, accessory=accessory and accessory._underlying, + id=None, ) def add_item(self, item: Item) -> None: From 30e2902320f0f7b7a9fe3635817e025dfe291ba3 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:49:05 +0100 Subject: [PATCH 030/154] flags and with_components --- discord/abc.py | 2 ++ discord/interactions.py | 2 ++ discord/message.py | 9 +++++++-- discord/webhook/async_.py | 20 +++++++++++++++----- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 04ec85fbe9..779a56e40d 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1601,6 +1601,8 @@ async def send( ) components = view.to_components() + if view.is_v2(): + flags.is_components_v2 = True else: components = None diff --git a/discord/interactions.py b/discord/interactions.py index 57628f4691..44fa84eae6 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -964,6 +964,8 @@ async def send_message( if view is not None: payload["components"] = view.to_components() + if view.is_v2(): + flags.is_components_v2 = True if poll is not None: payload["poll"] = poll.to_dict() diff --git a/discord/message.py b/discord/message.py index 55a6bfd6b4..00f0627311 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1575,11 +1575,11 @@ async def edit( payload["embeds"] = [] if embed is None else [embed.to_dict()] elif embeds is not MISSING: payload["embeds"] = [e.to_dict() for e in embeds] + + flags = MessageFlags._from_value(self.flags.value) if suppress is not MISSING: - flags = MessageFlags._from_value(self.flags.value) flags.suppress_embeds = suppress - payload["flags"] = flags.value if allowed_mentions is MISSING: if ( @@ -1601,8 +1601,13 @@ async def edit( if view is not MISSING: self._state.prevent_view_updates_for(self.id) payload["components"] = view.to_components() if view else [] + if view and view.is_v2(): + flags.is_components_v2 = True if file is not MISSING and files is not MISSING: raise InvalidArgument("cannot pass both file and files parameter to edit()") + + if flags.value != self.flags.value: + payload["flags"] = flags.value if file is not MISSING or files is not MISSING: if file is not MISSING: diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 4b87a7e0f3..06f4abf69b 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -339,10 +339,13 @@ def execute_webhook( thread_id: int | None = None, thread_name: str | None = None, wait: bool = False, + with_components: bool = False, ) -> Response[MessagePayload | None]: params = {"wait": int(wait)} if thread_id: params["thread_id"] = thread_id + if with_components: + params["with_components"] = with_components if thread_name: payload["thread_name"] = thread_name @@ -653,8 +656,15 @@ def handle_message_parameters( if attachments is not MISSING: _attachments = [a.to_dict() for a in attachments] + flags = MessageFlags( + suppress_embeds=suppress, + ephemeral=ephemeral, + ) + if view is not MISSING: payload["components"] = view.to_components() if view is not None else [] + if view and view.is_v2(): + flags.is_components_v2 = True if poll is not MISSING: payload["poll"] = poll.to_dict() payload["tts"] = tts @@ -663,11 +673,6 @@ def handle_message_parameters( if username: payload["username"] = username - flags = MessageFlags( - suppress_embeds=suppress, - ephemeral=ephemeral, - ) - if applied_tags is not MISSING: payload["applied_tags"] = applied_tags @@ -1781,6 +1786,8 @@ async def send( if application_webhook: wait = True + + with_components = False if view is not MISSING: if isinstance(self._state, _WebhookState): @@ -1789,6 +1796,8 @@ async def send( ) if ephemeral is True and view.timeout is None: view.timeout = 15 * 60.0 + if not application_webhook: + with_components = True if poll is None: poll = MISSING @@ -1826,6 +1835,7 @@ async def send( thread_id=thread_id, thread_name=thread_name, wait=wait, + with_components=with_components, ) msg = None From 9009ca10824925ee07f1c4b98aac3a6466682e37 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 07:49:30 +0000 Subject: [PATCH 031/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/message.py | 4 ++-- discord/webhook/async_.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/message.py b/discord/message.py index 00f0627311..29a80ea185 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1575,7 +1575,7 @@ async def edit( payload["embeds"] = [] if embed is None else [embed.to_dict()] elif embeds is not MISSING: payload["embeds"] = [e.to_dict() for e in embeds] - + flags = MessageFlags._from_value(self.flags.value) if suppress is not MISSING: @@ -1605,7 +1605,7 @@ async def edit( flags.is_components_v2 = True if file is not MISSING and files is not MISSING: raise InvalidArgument("cannot pass both file and files parameter to edit()") - + if flags.value != self.flags.value: payload["flags"] = flags.value diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 06f4abf69b..211534bd57 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1786,7 +1786,7 @@ async def send( if application_webhook: wait = True - + with_components = False if view is not MISSING: From 25deb2245812bead774badfe6ca128526271d4ac Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:13:44 +0100 Subject: [PATCH 032/154] TextDisplay --- discord/ui/section.py | 10 ++----- discord/ui/text_display.py | 54 ++++++++++++++++++++++++++++++++++++++ discord/ui/view.py | 5 ++++ 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 discord/ui/text_display.py diff --git a/discord/ui/section.py b/discord/ui/section.py index 1f2e55f6dc..9c7c269c51 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -26,9 +26,9 @@ class Section(Item[V]): Parameters ---------- *items: :class:`Item` - The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. + The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay`. accessory: Optional[:class:`Item`] - This section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.TextDisplay` and :class:`~discord.ui.Button`. + This section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.Thumbnail` and :class:`~discord.ui.Button`. """ def __init__(self, *items: Item, accessory: Item = None): @@ -92,12 +92,6 @@ def add_text(self, content: str) -> None: self.items.append(text) - def add_button( - self, - label: str, - ) -> None: - """finish""" - def set_accessory(self, item: Item) -> None: """Set an item as the section's :attr:`accessory`. diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py new file mode 100644 index 0000000000..925fb68fd4 --- /dev/null +++ b/discord/ui/text_display.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from ..components import TextDisplay as TextDisplayComponent +from ..components import _component_factory +from ..enums import ComponentType +from .item import Item + +__all__ = ("TextDisplay",) + +if TYPE_CHECKING: + from ..types.components import TextDisplayComponent as TextDisplayComponentPayload + from .view import View + + +T = TypeVar("T", bound="TextDisplay") +V = TypeVar("V", bound="View", covariant=True) + + +class TextDisplay(Item[V]): + """Represents a UI TextDisplay. + + .. versionadded:: 2.7 + + Parameters + ---------- + content: :class:`str` + The text display's content. + """ + + def __init__(self, content: str): + super().__init__() + + self.content = content + + self._underlying = TextDisplayComponent._raw_construct( + type=ComponentType.text_display, + id=None, + content=content, + ) + + @property + def type(self) -> ComponentType: + return self._underlying.type + + def to_component_dict(self) -> TextDisplayPayload: + return self._underlying.to_dict() + + @classmethod + def from_component(cls: type[S], component: TextDisplayComponent) -> T: + return cls(component.content) + + callback = None diff --git a/discord/ui/view.py b/discord/ui/view.py index 43f7f17f43..65e6f91fbb 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -39,6 +39,7 @@ from ..components import Component from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent +from ..components import TextDisplay as TextDisplayComponent from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType @@ -74,6 +75,10 @@ def _component_to_item(component: Component) -> Item: from .section import Section return Section.from_component(component) + if isinstance(component, TextDisplayComponent): + from .text_display import TextDisplay + + return TextDisplay.from_component(component) return Item.from_component(component) From bbe4f1dd0a934162da7df3341e8069041ed07a15 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:15:06 +0100 Subject: [PATCH 033/154] fix --- discord/ui/section.py | 2 +- discord/ui/text_display.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 9c7c269c51..856a97b335 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -40,9 +40,9 @@ def __init__(self, *items: Item, accessory: Item = None): self._underlying = SectionComponent._raw_construct( type=ComponentType.section, + id=None, components=components, accessory=accessory and accessory._underlying, - id=None, ) def add_item(self, item: Item) -> None: diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 925fb68fd4..9584a08a49 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -48,7 +48,7 @@ def to_component_dict(self) -> TextDisplayPayload: return self._underlying.to_dict() @classmethod - def from_component(cls: type[S], component: TextDisplayComponent) -> T: + def from_component(cls: type[T], component: TextDisplayComponent) -> T: return cls(component.content) callback = None From 48fc1c67cd09a80db81dfb20621d431d7ed227c9 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:16:08 +0100 Subject: [PATCH 034/154] fix type --- discord/ui/text_display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 9584a08a49..15a659e86a 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -44,7 +44,7 @@ def __init__(self, content: str): def type(self) -> ComponentType: return self._underlying.type - def to_component_dict(self) -> TextDisplayPayload: + def to_component_dict(self) -> TextDisplayComponentPayload: return self._underlying.to_dict() @classmethod From f643b5e723d9e3a5203f0f81ae09378f80706322 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:17:56 +0100 Subject: [PATCH 035/154] imports :( --- discord/ui/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index 8c8dd1531d..e795fa33c1 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -14,4 +14,5 @@ from .modal import * from .section import * from .select import * +from .text_display import * from .view import * From 605beb58cb000fcb5ef37ab07aca30b8573c3ab8 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:20:26 +0100 Subject: [PATCH 036/154] v2 view parsing --- discord/ui/view.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 65e6f91fbb..f423196ebd 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -226,19 +226,22 @@ def to_components(self) -> list[dict[str, Any]]: def key(item: Item) -> int: return item._rendered_row or 0 - children = sorted(self.children, key=key) - components: list[dict[str, Any]] = [] - for _, group in groupby(children, key=key): - children = [item.to_component_dict() for item in group] - if not children: - continue - - components.append( - { - "type": 1, - "components": children, - } - ) + if self.is_v2(): + components = [item.to_component_dict() for item in self.children] + else: + children = sorted(self.children, key=key) + components: list[dict[str, Any]] = [] + for _, group in groupby(children, key=key): + children = [item.to_component_dict() for item in group] + if not children: + continue + + components.append( + { + "type": 1, + "components": children, + } + ) return components From 3bb966c49447acea450026c0c800d1b00caa4bfa Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:29:21 +0100 Subject: [PATCH 037/154] patch underlying --- discord/ui/section.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 856a97b335..90f43a0c3b 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -6,6 +6,7 @@ from ..components import _component_factory from ..enums import ComponentType from .item import Item +from .text_display import TextDisplay __all__ = ("Section",) @@ -34,7 +35,7 @@ class Section(Item[V]): def __init__(self, *items: Item, accessory: Item = None): super().__init__() - self.items = items + self.items = items or [] self.accessory = accessory components = [i._underlying for i in items] @@ -68,6 +69,7 @@ def add_item(self, item: Item) -> None: raise TypeError(f"expected Item not {item.__class__!r}") self.items.append(item) + self._underlying.components.append(_component_factory(item)) def add_text(self, content: str) -> None: """Adds a :class:`TextDisplay` to the section. @@ -88,9 +90,9 @@ def add_text(self, content: str) -> None: if len(self.items) >= 3: raise ValueError("maximum number of children exceeded") - text = ... + text = TextDisplay(content) - self.items.append(text) + self.add_item(text) def set_accessory(self, item: Item) -> None: """Set an item as the section's :attr:`accessory`. @@ -110,6 +112,7 @@ def set_accessory(self, item: Item) -> None: raise TypeError(f"expected Item not {item.__class__!r}") self.accessory = item + self._underlying.accessory = accessory._underlying @property def type(self) -> ComponentType: From cccca7745c1d63d4b2ce88843aa272afe0166c51 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:30:43 +0100 Subject: [PATCH 038/154] set_text --- discord/ui/text_display.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 15a659e86a..1f2a45c7f6 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -44,6 +44,11 @@ def __init__(self, content: str): def type(self) -> ComponentType: return self._underlying.type + def set_text(self, content): + """Update this component's content.""" + self.content = content + self._underlying.content = content + def to_component_dict(self) -> TextDisplayComponentPayload: return self._underlying.to_dict() From 429f6587dfff2cb83bf56a8bfc35c939ceb5869a Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:31:52 +0100 Subject: [PATCH 039/154] fix --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 90f43a0c3b..3f1a120387 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -112,7 +112,7 @@ def set_accessory(self, item: Item) -> None: raise TypeError(f"expected Item not {item.__class__!r}") self.accessory = item - self._underlying.accessory = accessory._underlying + self._underlying.accessory = item._underlying @property def type(self) -> ComponentType: From 75e0b3d6684f76cc612f33ab7d07f34d1aae0ea7 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:35:20 +0100 Subject: [PATCH 040/154] underlying raghhhh --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 3f1a120387..2a4be3f9d5 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -69,7 +69,7 @@ def add_item(self, item: Item) -> None: raise TypeError(f"expected Item not {item.__class__!r}") self.items.append(item) - self._underlying.components.append(_component_factory(item)) + self._underlying.components.append(item._underlying) def add_text(self, content: str) -> None: """Adds a :class:`TextDisplay` to the section. From 4198e005d973030598a7e1b29cc17fe6a2a75645 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:44:00 +0100 Subject: [PATCH 041/154] bypass tuple --- discord/ui/section.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 2a4be3f9d5..1e0f9729e6 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -35,9 +35,9 @@ class Section(Item[V]): def __init__(self, *items: Item, accessory: Item = None): super().__init__() - self.items = items or [] - self.accessory = accessory + self.items = [i for i in items] components = [i._underlying for i in items] + self.accessory = accessory self._underlying = SectionComponent._raw_construct( type=ComponentType.section, From 40b1a791e92588c01b57ed64f653062b78505773 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:56:26 +0100 Subject: [PATCH 042/154] attempt v1-v2 compatability --- discord/ui/section.py | 8 ++++++-- discord/ui/text_display.py | 4 ++++ discord/ui/view.py | 23 +++++++++++++---------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 1e0f9729e6..7f82089d7d 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -20,7 +20,7 @@ class Section(Item[V]): - """Represents a UI section. + """Represents a UI section. Sections must have 1-3 items and an accessory set. .. versionadded:: 2.7 @@ -82,7 +82,7 @@ def add_text(self, content: str) -> None: Raises ------ TypeError - An :class:`Item` was not passed. + A :class:`str` was not passed. ValueError Maximum number of items has been exceeded (3). """ @@ -118,6 +118,10 @@ def set_accessory(self, item: Item) -> None: def type(self) -> ComponentType: return self._underlying.type + @property + def width(self) -> int: + return 5 + def to_component_dict(self) -> SectionComponentPayload: return self._underlying.to_dict() diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 1f2a45c7f6..4ad9d11e7c 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -44,6 +44,10 @@ def __init__(self, content: str): def type(self) -> ComponentType: return self._underlying.type + @property + def width(self) -> int: + return 5 + def set_text(self, content): """Update this component's content.""" self.content = content diff --git a/discord/ui/view.py b/discord/ui/view.py index f423196ebd..134bf97ee3 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -226,16 +226,19 @@ def to_components(self) -> list[dict[str, Any]]: def key(item: Item) -> int: return item._rendered_row or 0 - if self.is_v2(): - components = [item.to_component_dict() for item in self.children] - else: - children = sorted(self.children, key=key) - components: list[dict[str, Any]] = [] - for _, group in groupby(children, key=key): - children = [item.to_component_dict() for item in group] - if not children: - continue - + # if self.is_v2(): + # components = [item.to_component_dict() for item in self.children] + # else: + children = sorted(self.children, key=key) + components: list[dict[str, Any]] = [] + for _, group in groupby(children, key=key): + children = [item.to_component_dict() for item in group] + if not children: + continue + + if any([i._underlying.is_v2() for i in group]): + components += children + else: components.append( { "type": 1, From 1018de2d2d2f98854d4211eb9c0cc9eb11b30853 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:15:03 +0100 Subject: [PATCH 043/154] i hate groupby --- discord/ui/view.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 134bf97ee3..36fce52482 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -232,11 +232,12 @@ def key(item: Item) -> int: children = sorted(self.children, key=key) components: list[dict[str, Any]] = [] for _, group in groupby(children, key=key): - children = [item.to_component_dict() for item in group] + items = list(group) + children = [item.to_component_dict() for item in items] if not children: continue - if any([i._underlying.is_v2() for i in group]): + if any([i._underlying.is_v2() for i in items]): components += children else: components.append( From 5256726b55e32598190424caeafe528c9a3794b5 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:44:30 +0100 Subject: [PATCH 044/154] Thumbnail --- discord/components.py | 10 ++++- discord/ui/__init__.py | 1 + discord/ui/thumbnail.py | 91 +++++++++++++++++++++++++++++++++++++++++ discord/ui/view.py | 5 +++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 discord/ui/thumbnail.py diff --git a/discord/components.py b/discord/components.py index fa3d7e98c4..7da4d11865 100644 --- a/discord/components.py +++ b/discord/components.py @@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from .asset import AssetMixin from .colour import Colour from .enums import ( ButtonStyle, @@ -615,10 +616,10 @@ def to_dict(self) -> TextDisplayComponentPayload: return {"type": int(self.type), "id": self.id, "content": self.content} -class UnfurledMediaItem: +class UnfurledMediaItem(AssetMixin): def __init__(self, data: UnfurledMediaItemPayload): - self.url = data.get("url") + self._url = data.get("url") self.proxy_url: str = data.get("proxy_url") self.height: int | None = data.get("height") self.width: int | None = data.get("width") @@ -631,6 +632,11 @@ def __init__(self, data: UnfurledMediaItemPayload): ) self.src_is_animated: bool = data.get("src_is_animated") + @property + def url(self) -> str: + """Returns the underlying URL of this media.""" + return self._url + def to_dict(self): return {"url": self.url} diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index e795fa33c1..76eb325035 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -16,3 +16,4 @@ from .select import * from .text_display import * from .view import * +from .thumbnail import * \ No newline at end of file diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py new file mode 100644 index 0000000000..6424732b3c --- /dev/null +++ b/discord/ui/thumbnail.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from ..components import Thumbnail as ThumbnailComponent +from ..components import _component_factory, UnfurledMediaItem +from ..enums import ComponentType +from .item import Item + +__all__ = ("Thumbnail",) + +if TYPE_CHECKING: + from ..types.components import ThumbnailComponent as ThumbnailComponentPayload + from .view import View + + +T = TypeVar("T", bound="Thumbnail") +V = TypeVar("V", bound="View", covariant=True) + + +class Thumbnail(Item[V]): + """Represents a UI Thumbnail. + + .. versionadded:: 2.7 + + Parameters + ---------- + """ + + def __init__(self, url: str, *, description: str = None, spoiler: bool = False): + super().__init__() + + media = UnfurledMediaItem({"url": url}) + self._url = url + self._description: str | None = description + self._spoiler: bool = spoiler + + self._underlying = ThumbnailComponent._raw_construct( + type=ComponentType.thumbnail, + id=None, + media=self.media, + description=description, + spoiler=spoiler, + ) + + @property + def type(self) -> ComponentType: + return self._underlying.type + + @property + def width(self) -> int: + return 5 + + @property + def url(self) -> str: + """The URL of this thumbnail's media. This can either be an arbitrary URL or an ``attachment://`` URL.""" + return self._url + + @url.setter + def url(self, value: str) -> None: + self._url = value + self._underlying.media.url = value + + @property + def description(self) -> str | None: + """The thumbnail's description, up to 1024 characters.""" + return self._description + + @description.setter + def description(self, description: str | None) -> None: + self._description = description + self._underlying.description = description + + @property + def spoiler(self) -> bool: + """Whether the thumbnail is a spoiler. Defaults to ``False``.""" + return self.spoiler + + @spoiler.setter + def spoiler(self, spoiler: bool) -> None: + self._spoiler = spoiler + self._underlying.spoiler = spoiler + + def to_component_dict(self) -> ThumbnailComponentPayload: + return self._underlying.to_dict() + + @classmethod + def from_component(cls: type[T], component: ThumbnailComponent) -> T: + return cls(component.content) + + callback = None diff --git a/discord/ui/view.py b/discord/ui/view.py index 36fce52482..ba71702ec0 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -40,6 +40,7 @@ from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent from ..components import TextDisplay as TextDisplayComponent +from ..components import Thumbnail as ThumbnailComponent from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType @@ -79,6 +80,10 @@ def _component_to_item(component: Component) -> Item: from .text_display import TextDisplay return TextDisplay.from_component(component) + if isinstance(component, ThumbnailComponent): + from .thumbnail import Thumbnail + + return Thumbnail.from_component(component) return Item.from_component(component) From aebf5100ed3463ef940f693229e6453cbbf0447f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:44:55 +0000 Subject: [PATCH 045/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/__init__.py | 2 +- discord/ui/thumbnail.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index 76eb325035..3a23e4c9c1 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -15,5 +15,5 @@ from .section import * from .select import * from .text_display import * +from .thumbnail import * from .view import * -from .thumbnail import * \ No newline at end of file diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 6424732b3c..b1618a35b7 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, TypeVar from ..components import Thumbnail as ThumbnailComponent -from ..components import _component_factory, UnfurledMediaItem +from ..components import UnfurledMediaItem, _component_factory from ..enums import ComponentType from .item import Item @@ -55,7 +55,7 @@ def width(self) -> int: def url(self) -> str: """The URL of this thumbnail's media. This can either be an arbitrary URL or an ``attachment://`` URL.""" return self._url - + @url.setter def url(self, value: str) -> None: self._url = value From a6516cc3d59d87a9982c5448363e216341b7d0a6 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:49:23 +0100 Subject: [PATCH 046/154] minor fixes --- discord/ui/thumbnail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index b1618a35b7..1a85eaf5b5 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -38,7 +38,7 @@ def __init__(self, url: str, *, description: str = None, spoiler: bool = False): self._underlying = ThumbnailComponent._raw_construct( type=ComponentType.thumbnail, id=None, - media=self.media, + media=media, description=description, spoiler=spoiler, ) @@ -86,6 +86,6 @@ def to_component_dict(self) -> ThumbnailComponentPayload: @classmethod def from_component(cls: type[T], component: ThumbnailComponent) -> T: - return cls(component.content) + return cls(component.media and component.media.url, description=component.description, spoiler=component.spoiler) callback = None From 661a2a48690262caea128d0329ee8c51daec4772 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:49:48 +0000 Subject: [PATCH 047/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/thumbnail.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 1a85eaf5b5..910dc62ce8 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -86,6 +86,10 @@ def to_component_dict(self) -> ThumbnailComponentPayload: @classmethod def from_component(cls: type[T], component: ThumbnailComponent) -> T: - return cls(component.media and component.media.url, description=component.description, spoiler=component.spoiler) + return cls( + component.media and component.media.url, + description=component.description, + spoiler=component.spoiler, + ) callback = None From 4bf9bfe63958b63e5b3db1e57c2bd4e958790d1b Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 04:42:18 +0100 Subject: [PATCH 048/154] state? --- discord/components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/components.py b/discord/components.py index 7da4d11865..831917e3d3 100644 --- a/discord/components.py +++ b/discord/components.py @@ -619,6 +619,7 @@ def to_dict(self) -> TextDisplayComponentPayload: class UnfurledMediaItem(AssetMixin): def __init__(self, data: UnfurledMediaItemPayload): + self._state = None self._url = data.get("url") self.proxy_url: str = data.get("proxy_url") self.height: int | None = data.get("height") From 31af377ecb0e519553acf364777b9224a4f26f75 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 07:40:34 +0100 Subject: [PATCH 049/154] rough state support on received components --- discord/components.py | 23 +++++++++++++---------- discord/message.py | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/discord/components.py b/discord/components.py index 831917e3d3..7bfd59a438 100644 --- a/discord/components.py +++ b/discord/components.py @@ -618,8 +618,8 @@ def to_dict(self) -> TextDisplayComponentPayload: class UnfurledMediaItem(AssetMixin): - def __init__(self, data: UnfurledMediaItemPayload): - self._state = None + def __init__(self, data: UnfurledMediaItemPayload, state=None): + self._state = state self._url = data.get("url") self.proxy_url: str = data.get("proxy_url") self.height: int | None = data.get("height") @@ -690,10 +690,10 @@ def to_dict(self) -> ThumbnailComponentPayload: class MediaGalleryItem: - def __init__(self, data: MediaGalleryItemPayload): + def __init__(self, data: MediaGalleryItemPayload, state=None): self.media: UnfurledMediaItem = ( umi := data.get("media") - ) and UnfurledMediaItem(umi) + ) and UnfurledMediaItem(umi, state=state) self.description: str | None = data.get("description") self.spoiler: bool | None = data.get("spoiler") @@ -726,11 +726,11 @@ class MediaGallery(Component): __repr_info__: ClassVar[tuple[str, ...]] = __slots__ versions: tuple[int, ...] = (2,) - def __init__(self, data: MediaGalleryComponentPayload): + def __init__(self, data: MediaGalleryComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") self.items: list[MediaGalleryItem] = [ - MediaGalleryItem(d) for d in data.get("items", []) + MediaGalleryItem(d, state=state) for d in data.get("items", []) ] def to_dict(self) -> MediaGalleryComponentPayload: @@ -766,11 +766,11 @@ class FileComponent(Component): __repr_info__: ClassVar[tuple[str, ...]] = __slots__ versions: tuple[int, ...] = (2,) - def __init__(self, data: FileComponentPayload): + def __init__(self, data: FileComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem( - umi + umi, state=state ) self.spoiler: bool | None = data.get("spoiler") @@ -890,10 +890,13 @@ def to_dict(self) -> ContainerComponentPayload: } -def _component_factory(data: ComponentPayload) -> Component: +def _component_factory(data: ComponentPayload, state=None) -> Component: component_type = data["type"] if cls := COMPONENT_MAPPINGS.get(component_type): - return cls(data) + if cls in (Thumbnail, MediaGallery, FileComponent): + return cls(data, state=state) + else: + return cls(data) else: as_enum = try_enum(ComponentType, component_type) return Component._raw_construct(type=as_enum) diff --git a/discord/message.py b/discord/message.py index 29a80ea185..6b4d9f3ecc 100644 --- a/discord/message.py +++ b/discord/message.py @@ -870,7 +870,7 @@ def __init__( StickerItem(data=d, state=state) for d in data.get("sticker_items", []) ] self.components: list[Component] = [ - _component_factory(d) for d in data.get("components", []) + _component_factory(d, state=state) for d in data.get("components", []) ] try: From 403b34fd8cf6d50162ed73291eaa88a376d66486 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 07:49:09 +0100 Subject: [PATCH 050/154] thumbnail too --- discord/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 7bfd59a438..8f11741016 100644 --- a/discord/components.py +++ b/discord/components.py @@ -670,12 +670,12 @@ class Thumbnail(Component): __repr_info__: ClassVar[tuple[str, ...]] = __slots__ versions: tuple[int, ...] = (2,) - def __init__(self, data: ThumbnailComponentPayload): + def __init__(self, data: ThumbnailComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") self.media: UnfurledMediaItem = ( umi := data.get("media") - ) and UnfurledMediaItem(umi) + ) and UnfurledMediaItem(umi, state=state) self.description: str | None = data.get("description") self.spoiler: bool | None = data.get("spoiler") From 835138ac15d57f563f387d675d105eebcb58b1f3 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 08:21:10 +0100 Subject: [PATCH 051/154] fix states --- discord/components.py | 12 ++++++------ discord/message.py | 2 +- discord/ui/view.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/components.py b/discord/components.py index 8f11741016..7c68b068bf 100644 --- a/discord/components.py +++ b/discord/components.py @@ -566,15 +566,15 @@ class Section(Component): __repr_info__: ClassVar[tuple[str, ...]] = __slots__ versions: tuple[int, ...] = (2,) - def __init__(self, data: SectionComponentPayload): + def __init__(self, data: SectionComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") self.components: list[Component] = [ - _component_factory(d) for d in data.get("components", []) + _component_factory(d, state=state) for d in data.get("components", []) ] self.accessory: Component | None = None if _accessory := data.get("accessory"): - self.accessory = _component_factory(_accessory) + self.accessory = _component_factory(_accessory, state=state) def to_dict(self) -> SectionComponentPayload: payload = { @@ -847,7 +847,7 @@ class Container(Component): __repr_info__: ClassVar[tuple[str, ...]] = __slots__ versions: tuple[int, ...] = (2,) - def __init__(self, data: ContainerComponentPayload): + def __init__(self, data: ContainerComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour( @@ -855,7 +855,7 @@ def __init__(self, data: ContainerComponentPayload): ) # at this point, not adding alternative spelling self.spoiler: bool | None = data.get("spoiler") self.components: list[Component] = [ - _component_factory(d) for d in data.get("components", []) + _component_factory(d, state=state) for d in data.get("components", []) ] def to_dict(self) -> ContainerComponentPayload: @@ -893,7 +893,7 @@ def to_dict(self) -> ContainerComponentPayload: def _component_factory(data: ComponentPayload, state=None) -> Component: component_type = data["type"] if cls := COMPONENT_MAPPINGS.get(component_type): - if cls in (Thumbnail, MediaGallery, FileComponent): + if cls in (Section, Container, Thumbnail, MediaGallery, FileComponent): return cls(data, state=state) else: return cls(data) diff --git a/discord/message.py b/discord/message.py index 6b4d9f3ecc..f3f78e13a4 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1120,7 +1120,7 @@ def _handle_mention_roles(self, role_mentions: list[int]) -> None: self.role_mentions.append(role) def _handle_components(self, components: list[ComponentPayload]): - self.components = [_component_factory(d) for d in components] + self.components = [_component_factory(d, state=self._state) for d in components] def _rebind_cached_references( self, new_guild: Guild, new_channel: TextChannel | Thread diff --git a/discord/ui/view.py b/discord/ui/view.py index ba71702ec0..e3fa073797 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -665,4 +665,4 @@ def remove_message_tracking(self, message_id: int) -> View | None: def update_from_message(self, message_id: int, components: list[ComponentPayload]): # pre-req: is_message_tracked == true view = self._synced_message_views[message_id] - view.refresh([_component_factory(d) for d in components]) + view.refresh([_component_factory(d, state=self._state) for d in components]) From 5e45569c5e30410d589c692b5e932b0fc4bf9492 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:10:23 +0000 Subject: [PATCH 052/154] adjust with_components defaults --- discord/webhook/async_.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 211534bd57..cde827aecc 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -338,18 +338,19 @@ def execute_webhook( files: list[File] | None = None, thread_id: int | None = None, thread_name: str | None = None, + with_components: bool | None = None, wait: bool = False, - with_components: bool = False, ) -> Response[MessagePayload | None]: params = {"wait": int(wait)} if thread_id: params["thread_id"] = thread_id - if with_components: - params["with_components"] = with_components if thread_name: payload["thread_name"] = thread_name + if with_components is not None: + params["with_components"] = with_components + route = Route( "POST", "/webhooks/{webhook_id}/{webhook_token}", From 4fd6fe7e36ece078fb6eaf809f6fa86fae4a86f1 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:17:08 +0000 Subject: [PATCH 053/154] MediaGallery --- discord/components.py | 91 +++++++++++++++++++++-------- discord/ui/__init__.py | 1 + discord/ui/media_gallery.py | 110 ++++++++++++++++++++++++++++++++++++ discord/ui/thumbnail.py | 2 +- discord/ui/view.py | 5 ++ 5 files changed, 184 insertions(+), 25 deletions(-) create mode 100644 discord/ui/media_gallery.py diff --git a/discord/components.py b/discord/components.py index 7c68b068bf..e08fe292eb 100644 --- a/discord/components.py +++ b/discord/components.py @@ -72,6 +72,8 @@ "TextDisplay", "Thumbnail", "MediaGallery", + "MediaGalleryItem", + "UnfurledMediaItem", "FileComponent", "Separator", "Container", @@ -618,26 +620,42 @@ def to_dict(self) -> TextDisplayComponentPayload: class UnfurledMediaItem(AssetMixin): - def __init__(self, data: UnfurledMediaItemPayload, state=None): - self._state = state - self._url = data.get("url") - self.proxy_url: str = data.get("proxy_url") - self.height: int | None = data.get("height") - self.width: int | None = data.get("width") - self.content_type: str | None = data.get("content_type") - self.flags: AttachmentFlags = AttachmentFlags._from_value(data.get("flags", 0)) - self.placeholder: str = data.get("placeholder") - self.placeholder_version: int = data.get("placeholder_version") - self.loading_state: MediaItemLoadingState = try_enum( - MediaItemLoadingState, data.get("loading_state") - ) - self.src_is_animated: bool = data.get("src_is_animated") + def __init__(self, url: str): + self._state = None + self._url: str = url + self.proxy_url: str | None = None + self.height: int | None = None + self.width: int | None = None + self.content_type: str | None = None + self.flags: AttachmentFlags | None = None + self.placeholder: str | None = None + self.placeholder_version: int | None = None + self.loading_state: MediaItemLoadingState | None = None + self.src_is_animated: bool | None = None @property def url(self) -> str: """Returns the underlying URL of this media.""" return self._url + @classmethod + def from_dict(cls, data: UnfurledMediaItemPayload, state=None) -> UnfurledMediaItem: + + r = cls(data.get("url")) + r.proxy_url = data.get("proxy_url") + r.height = data.get("height") + r.width = data.get("width") + r.content_type = data.get("content_type") + r.flags = AttachmentFlags._from_value(data.get("flags", 0)) + r.placeholder = data.get("placeholder") + r.placeholder_version = data.get("placeholder_version") + r.loading_state = try_enum( + MediaItemLoadingState, data.get("loading_state") + ) + r.src_is_animated = data.get("src_is_animated") + r._state = state + return r + def to_dict(self): return {"url": self.url} @@ -654,7 +672,7 @@ class Thumbnail(Component): Attributes ---------- media: :class:`UnfurledMediaItem` - The component's media URL. + The component's media object. description: Optional[:class:`str`] The thumbnail's description, up to 1024 characters. spoiler: Optional[:class:`bool`] @@ -675,10 +693,15 @@ def __init__(self, data: ThumbnailComponentPayload, state=None): self.id: str = data.get("id") self.media: UnfurledMediaItem = ( umi := data.get("media") - ) and UnfurledMediaItem(umi, state=state) + ) and UnfurledMediaItem.from_dict(umi, state=state) self.description: str | None = data.get("description") self.spoiler: bool | None = data.get("spoiler") + @property + def url(self) -> str: + """Returns the underlying URL of this thumbnail.""" + return self.media.url + def to_dict(self) -> ThumbnailComponentPayload: payload = {"type": int(self.type), "id": self.id, "media": self.media.to_dict()} if self.description: @@ -690,12 +713,31 @@ def to_dict(self) -> ThumbnailComponentPayload: class MediaGalleryItem: - def __init__(self, data: MediaGalleryItemPayload, state=None): - self.media: UnfurledMediaItem = ( - umi := data.get("media") - ) and UnfurledMediaItem(umi, state=state) - self.description: str | None = data.get("description") - self.spoiler: bool | None = data.get("spoiler") + def __init__(self, url, *, description=None, spoiler=False): + self._state = None + self.media: UnfurledMediaItem = UnfurledMediaItem(url) + self.description: str | None = description + self.spoiler: bool = spoiler + + @property + def url(self) -> str: + """Returns the underlying URL of this gallery item.""" + return self.media.url + + @classmethod + def from_dict(cls, data: MediaGalleryItemPayload, state=None) -> MediaGalleryItem: + media = (umi := data.get("media")) and UnfurledMediaItem.from_dict(umi, state=state) + description = data.get("description") + spoiler = data.get("spoiler", False) + + r = cls( + media=media.url, + description=description, + spoiler=spoiler, + ) + r._state = state + r.media = media + return r def to_dict(self): payload = {"media": self.media.to_dict()} @@ -730,7 +772,7 @@ def __init__(self, data: MediaGalleryComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") self.items: list[MediaGalleryItem] = [ - MediaGalleryItem(d, state=state) for d in data.get("items", []) + MediaGalleryItem.from_dict(d, state=state) for d in data.get("items", []) ] def to_dict(self) -> MediaGalleryComponentPayload: @@ -769,7 +811,7 @@ class FileComponent(Component): def __init__(self, data: FileComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem( + self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem.from_dict( umi, state=state ) self.spoiler: bool | None = data.get("spoiler") @@ -780,6 +822,7 @@ def to_dict(self) -> FileComponentPayload: payload["spoiler"] = self.spoiler return payload +# Alternate idea - subclass above components as UnfurledMedia? class Separator(Component): """Represents a Separator from Components V2. diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index 3a23e4c9c1..5d76f16a9b 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -17,3 +17,4 @@ from .text_display import * from .thumbnail import * from .view import * +from .media_gallery import * diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py new file mode 100644 index 0000000000..260ab585f5 --- /dev/null +++ b/discord/ui/media_gallery.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from ..components import MediaGallery as MediaGalleryComponent +from ..components import MediaGalleryItem +from ..enums import ComponentType +from .item import Item + +__all__ = ("MediaGallery",) + +if TYPE_CHECKING: + from ..types.components import MediaGalleryComponent as MediaGalleryComponentPayload + from .view import View + + +M = TypeVar("M", bound="MediaGallery") +V = TypeVar("V", bound="View", covariant=True) + + +class MediaGallery(Item[V]): + """Represents a UI Media Gallery. Galleries may contain up to 10 :class:`MediaGalleryItem`s. + + .. versionadded:: 2.7 + + Parameters + ---------- + *items: :class:`MediaGalleryItem` + The initial items contained in this gallery, up to 10. + """ + + def __init__(self, *items: MediaGalleryItem): + super().__init__() + + self.items = [i for i in items] + + self._underlying = MediaGalleryComponent._raw_construct( + type=ComponentType.media_gallery, + id=None, + items=self.items + ) + + def add_item(self, item: MediaGalleryItem) -> None: + """Adds a :attr:`MediaGalleryItem` to the gallery. + + Parameters + ---------- + item: :class:`MediaGalleryItem` + The gallery item to add to the gallery. + + Raises + ------ + TypeError + A :class:`MediaGalleryItem` was not passed. + ValueError + Maximum number of items has been exceeded (10). + """ + + if len(self.items) >= 10: + raise ValueError("maximum number of children exceeded") + + if not isinstance(item, MediaGalleryItem): + raise TypeError(f"expected MediaGalleryItem not {item.__class__!r}") + + self.items.append(item) + self._underlying.items.append(item) + + def add_media(self, url: str, *, description: str = None, spoiler: bool = False) -> None: + """Adds new media to the gallery. + + Parameters + ---------- + url: :class:`str` + The URL of this item's media. This can either be an arbitrary URL or an ``attachment://`` URL. + description: Optional[:class:`str`] + The item's description, up to 1024 characters. + spoiler: Optional[:class:`bool`] + Whether the item is a spoiler. + + Raises + ------ + TypeError + A :class:`str` was not passed. + ValueError + Maximum number of items has been exceeded (10). + """ + + if len(self.items) >= 10: + raise ValueError("maximum number of children exceeded") + + item = MediaGalleryItem(url, description=description, spoiler=spoiler) + + self.add_item(item) + + @property + def type(self) -> ComponentType: + return self._underlying.type + + @property + def width(self) -> int: + return 5 + + def to_component_dict(self) -> MediaGalleryComponentPayload: + return self._underlying.to_dict() + + @classmethod + def from_component(cls: type[M], component: MediaGalleryComponent) -> M: + return cls(*component.items) + + callback = None diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 910dc62ce8..82b75f160e 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -30,7 +30,7 @@ class Thumbnail(Item[V]): def __init__(self, url: str, *, description: str = None, spoiler: bool = False): super().__init__() - media = UnfurledMediaItem({"url": url}) + media = UnfurledMediaItem(url) self._url = url self._description: str | None = description self._spoiler: bool = spoiler diff --git a/discord/ui/view.py b/discord/ui/view.py index e3fa073797..bbf9b97450 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -41,6 +41,7 @@ from ..components import SelectMenu as SelectComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent +from ..components import MediaGallery as MediaGalleryComponent from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType @@ -84,6 +85,10 @@ def _component_to_item(component: Component) -> Item: from .thumbnail import Thumbnail return Thumbnail.from_component(component) + if isinstance(component, MediaGalleryComponent): + from .media_gallery import MediaGallery + + return MediaGallery.from_component(component) return Item.from_component(component) From 11f30c5e4f6a86008b5edbc49a70399499af1330 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:17:34 +0000 Subject: [PATCH 054/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 16 +++++++++------- discord/ui/__init__.py | 2 +- discord/ui/media_gallery.py | 8 ++++---- discord/ui/view.py | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/discord/components.py b/discord/components.py index e08fe292eb..97a8dabc36 100644 --- a/discord/components.py +++ b/discord/components.py @@ -649,9 +649,7 @@ def from_dict(cls, data: UnfurledMediaItemPayload, state=None) -> UnfurledMediaI r.flags = AttachmentFlags._from_value(data.get("flags", 0)) r.placeholder = data.get("placeholder") r.placeholder_version = data.get("placeholder_version") - r.loading_state = try_enum( - MediaItemLoadingState, data.get("loading_state") - ) + r.loading_state = try_enum(MediaItemLoadingState, data.get("loading_state")) r.src_is_animated = data.get("src_is_animated") r._state = state return r @@ -726,7 +724,9 @@ def url(self) -> str: @classmethod def from_dict(cls, data: MediaGalleryItemPayload, state=None) -> MediaGalleryItem: - media = (umi := data.get("media")) and UnfurledMediaItem.from_dict(umi, state=state) + media = (umi := data.get("media")) and UnfurledMediaItem.from_dict( + umi, state=state + ) description = data.get("description") spoiler = data.get("spoiler", False) @@ -811,9 +811,9 @@ class FileComponent(Component): def __init__(self, data: FileComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") - self.file: UnfurledMediaItem = (umi := data.get("media")) and UnfurledMediaItem.from_dict( - umi, state=state - ) + self.file: UnfurledMediaItem = ( + umi := data.get("media") + ) and UnfurledMediaItem.from_dict(umi, state=state) self.spoiler: bool | None = data.get("spoiler") def to_dict(self) -> FileComponentPayload: @@ -822,8 +822,10 @@ def to_dict(self) -> FileComponentPayload: payload["spoiler"] = self.spoiler return payload + # Alternate idea - subclass above components as UnfurledMedia? + class Separator(Component): """Represents a Separator from Components V2. diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index 5d76f16a9b..2c3d3992a1 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -11,10 +11,10 @@ from .button import * from .input_text import * from .item import * +from .media_gallery import * from .modal import * from .section import * from .select import * from .text_display import * from .thumbnail import * from .view import * -from .media_gallery import * diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 260ab585f5..a3f33e471b 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -35,9 +35,7 @@ def __init__(self, *items: MediaGalleryItem): self.items = [i for i in items] self._underlying = MediaGalleryComponent._raw_construct( - type=ComponentType.media_gallery, - id=None, - items=self.items + type=ComponentType.media_gallery, id=None, items=self.items ) def add_item(self, item: MediaGalleryItem) -> None: @@ -65,7 +63,9 @@ def add_item(self, item: MediaGalleryItem) -> None: self.items.append(item) self._underlying.items.append(item) - def add_media(self, url: str, *, description: str = None, spoiler: bool = False) -> None: + def add_media( + self, url: str, *, description: str = None, spoiler: bool = False + ) -> None: """Adds new media to the gallery. Parameters diff --git a/discord/ui/view.py b/discord/ui/view.py index bbf9b97450..ab1c85d3d1 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -37,11 +37,11 @@ from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent from ..components import Component +from ..components import MediaGallery as MediaGalleryComponent from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent -from ..components import MediaGallery as MediaGalleryComponent from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType From a883573087fcb188967734c2d0cec856c563875d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:32:06 +0000 Subject: [PATCH 055/154] gallery fixes --- discord/components.py | 2 +- discord/ui/media_gallery.py | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/discord/components.py b/discord/components.py index 97a8dabc36..438539bf3e 100644 --- a/discord/components.py +++ b/discord/components.py @@ -731,7 +731,7 @@ def from_dict(cls, data: MediaGalleryItemPayload, state=None) -> MediaGalleryIte spoiler = data.get("spoiler", False) r = cls( - media=media.url, + url=media.url, description=description, spoiler=spoiler, ) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index a3f33e471b..a59d18105f 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -32,13 +32,15 @@ class MediaGallery(Item[V]): def __init__(self, *items: MediaGalleryItem): super().__init__() - self.items = [i for i in items] - self._underlying = MediaGalleryComponent._raw_construct( type=ComponentType.media_gallery, id=None, items=self.items ) + + @property + def items(self): + return self._underlying.items - def add_item(self, item: MediaGalleryItem) -> None: + def append_item(self, item: MediaGalleryItem) -> None: """Adds a :attr:`MediaGalleryItem` to the gallery. Parameters @@ -60,27 +62,22 @@ def add_item(self, item: MediaGalleryItem) -> None: if not isinstance(item, MediaGalleryItem): raise TypeError(f"expected MediaGalleryItem not {item.__class__!r}") - self.items.append(item) self._underlying.items.append(item) - def add_media( - self, url: str, *, description: str = None, spoiler: bool = False - ) -> None: - """Adds new media to the gallery. + def add_item(self, url: str, *, description: str = None, spoiler: bool = False) -> None: + """Adds a new media item to the gallery. Parameters ---------- url: :class:`str` - The URL of this item's media. This can either be an arbitrary URL or an ``attachment://`` URL. + The URL of the media item. This can either be an arbitrary URL or an ``attachment://`` URL. description: Optional[:class:`str`] - The item's description, up to 1024 characters. + The media item's description, up to 1024 characters. spoiler: Optional[:class:`bool`] - Whether the item is a spoiler. + Whether the media item is a spoiler. Raises ------ - TypeError - A :class:`str` was not passed. ValueError Maximum number of items has been exceeded (10). """ From 0faeab865e9bac888d20318e0358a6259ef58c3b Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:32:30 +0000 Subject: [PATCH 056/154] append --- discord/ui/media_gallery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index a59d18105f..52c8c4e431 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -87,7 +87,7 @@ def add_item(self, url: str, *, description: str = None, spoiler: bool = False) item = MediaGalleryItem(url, description=description, spoiler=spoiler) - self.add_item(item) + self.append_item(item) @property def type(self) -> ComponentType: From 554b4b8e24e4eed4f2a63b4d6a268ec43a2758b5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:32:58 +0000 Subject: [PATCH 057/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/media_gallery.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 52c8c4e431..e04ee00c08 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -35,7 +35,7 @@ def __init__(self, *items: MediaGalleryItem): self._underlying = MediaGalleryComponent._raw_construct( type=ComponentType.media_gallery, id=None, items=self.items ) - + @property def items(self): return self._underlying.items @@ -64,7 +64,9 @@ def append_item(self, item: MediaGalleryItem) -> None: self._underlying.items.append(item) - def add_item(self, url: str, *, description: str = None, spoiler: bool = False) -> None: + def add_item( + self, url: str, *, description: str = None, spoiler: bool = False + ) -> None: """Adds a new media item to the gallery. Parameters From 6f133e0e99736f4bdc2a077a7e59099506143161 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:42:24 +0000 Subject: [PATCH 058/154] notuple --- discord/ui/media_gallery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index e04ee00c08..d02d1a1d31 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -33,7 +33,7 @@ def __init__(self, *items: MediaGalleryItem): super().__init__() self._underlying = MediaGalleryComponent._raw_construct( - type=ComponentType.media_gallery, id=None, items=self.items + type=ComponentType.media_gallery, id=None, items=[i for i in items] ) @property From 1c308533216513b871d0c6f6989d5d02454a1463 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:18:48 +0000 Subject: [PATCH 059/154] Update discord/ui/section.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/section.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 7f82089d7d..3f38497c00 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -27,9 +27,11 @@ class Section(Item[V]): Parameters ---------- *items: :class:`Item` - The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay`. + The initial items contained in this section, up to 3. + Currently only supports :class:`~discord.ui.TextDisplay`. accessory: Optional[:class:`Item`] - This section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.Thumbnail` and :class:`~discord.ui.Button`. + This 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`. """ def __init__(self, *items: Item, accessory: Item = None): From 40dbb8c792e5939aeb7aa786c0692945f238b846 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:19:19 +0000 Subject: [PATCH 060/154] Update discord/ui/section.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/section.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 3f38497c00..3a2ade37f1 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -102,7 +102,9 @@ def set_accessory(self, item: Item) -> None: Parameters ---------- item: :class:`Item` - The item to set as accessory. Currently only supports :class:`~discord.ui.Thumbnail` and :class:`~discord.ui.Button`. + The item to set as accessory. + Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`. + Raises ------ From f9096a938b702f28cd1f7c94b3c3b407a1834a43 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:19:45 +0000 Subject: [PATCH 061/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 3a2ade37f1..1f5838c05d 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -105,7 +105,6 @@ def set_accessory(self, item: Item) -> None: The item to set as accessory. Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`. - Raises ------ TypeError From 0be3e31e475a7bebd3362c7e5919bacd8d50cd85 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 06:31:43 +0000 Subject: [PATCH 062/154] file --- discord/ui/file.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++ discord/ui/view.py | 5 +++ 2 files changed, 87 insertions(+) create mode 100644 discord/ui/file.py diff --git a/discord/ui/file.py b/discord/ui/file.py new file mode 100644 index 0000000000..e38e1fff0c --- /dev/null +++ b/discord/ui/file.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from ..components import FileComponent +from ..components import UnfurledMediaItem, _component_factory +from ..enums import ComponentType +from .item import Item + +__all__ = ("File",) + +if TYPE_CHECKING: + from ..types.components import FileComponent as FileComponentPayload + from .view import View + + +F = TypeVar("F", bound="File") +V = TypeVar("V", bound="View", covariant=True) + + +class File(Item[V]): + """Represents a UI File. + + .. versionadded:: 2.7 + + Parameters + ---------- + """ + + def __init__(self, url: str, *, spoiler: bool = False): + super().__init__() + + media = UnfurledMediaItem(url) + self._url = url + self._spoiler: bool = spoiler + + self._underlying = FileComponent._raw_construct( + type=ComponentType.file, + id=None, + file=media, + spoiler=spoiler, + ) + + @property + def type(self) -> ComponentType: + return self._underlying.type + + @property + def width(self) -> int: + return 5 + + @property + def url(self) -> str: + """The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`.""" + return self._url + + @url.setter + def url(self, value: str) -> None: + self._url = value + self._underlying.media.url = value + + @property + def spoiler(self) -> bool: + """Whether the file is a spoiler. Defaults to ``False``.""" + return self.spoiler + + @spoiler.setter + def spoiler(self, spoiler: bool) -> None: + self._spoiler = spoiler + self._underlying.spoiler = spoiler + + def to_component_dict(self) -> FileComponentPayload: + return self._underlying.to_dict() + + @classmethod + def from_component(cls: type[F], component: FileComponent) -> F: + return cls( + component.media and component.media.url, + spoiler=component.spoiler, + ) + + callback = None diff --git a/discord/ui/view.py b/discord/ui/view.py index ab1c85d3d1..ff50d38c67 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -42,6 +42,7 @@ from ..components import SelectMenu as SelectComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent +from ..components import FileComponent from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType @@ -89,6 +90,10 @@ def _component_to_item(component: Component) -> Item: from .media_gallery import MediaGallery return MediaGallery.from_component(component) + if isinstance(component, FileComponent): + from .file import File + + return File.from_component(component) return Item.from_component(component) From 044a0521687e54af300f73c7ebf13a2aef264acf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 06:32:06 +0000 Subject: [PATCH 063/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/file.py | 3 +-- discord/ui/view.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/discord/ui/file.py b/discord/ui/file.py index e38e1fff0c..a282a4f6ad 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING, TypeVar -from ..components import FileComponent -from ..components import UnfurledMediaItem, _component_factory +from ..components import FileComponent, UnfurledMediaItem, _component_factory from ..enums import ComponentType from .item import Item diff --git a/discord/ui/view.py b/discord/ui/view.py index ff50d38c67..ec85150392 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -36,13 +36,12 @@ from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent -from ..components import Component +from ..components import Component, FileComponent from ..components import MediaGallery as MediaGalleryComponent from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent -from ..components import FileComponent from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType From 8fd981d92cb714f085576fc67c43f6d92ab8e5bc Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Thu, 27 Feb 2025 07:39:28 +0100 Subject: [PATCH 064/154] Update discord/ui/view.py applying suggestion because is_v2 is not describing enough. Users might be like "huh, v2 views?" Co-authored-by: Paillat Signed-off-by: Lala Sabathil --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index ec85150392..462fc325b5 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -546,7 +546,7 @@ def is_persistent(self) -> bool: item.is_persistent() for item in self.children ) - def is_v2(self) -> bool: + def is_components_v2(self) -> bool: """Whether the view contains V2 components. A view containing V2 components may not be sent alongside message content or embeds. From 032fb00a87094e1f744d674d944cce23741d33c1 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 06:44:12 +0000 Subject: [PATCH 065/154] Separator --- discord/abc.py | 2 +- discord/enums.py | 4 +++ discord/interactions.py | 2 +- discord/message.py | 2 +- discord/ui/separator.py | 62 +++++++++++++++++++++++++++++++++++++++ discord/ui/view.py | 3 -- discord/webhook/async_.py | 2 +- 7 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 discord/ui/separator.py diff --git a/discord/abc.py b/discord/abc.py index 779a56e40d..ed3c417690 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1601,7 +1601,7 @@ async def send( ) components = view.to_components() - if view.is_v2(): + if view.is_components_v2(): flags.is_components_v2 = True else: components = None diff --git a/discord/enums.py b/discord/enums.py index d8cd1f3aec..73fdb0365a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -76,6 +76,10 @@ "EntitlementOwnerType", "IntegrationType", "InteractionContextType", + "PollLayoutType", + "SubscriptionStatus", + "SeparatorSpacingSize", + "MediaItemLoadingState", ) diff --git a/discord/interactions.py b/discord/interactions.py index 44fa84eae6..7c0ecaaac6 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -964,7 +964,7 @@ async def send_message( if view is not None: payload["components"] = view.to_components() - if view.is_v2(): + if view.is_components_v2(): flags.is_components_v2 = True if poll is not None: diff --git a/discord/message.py b/discord/message.py index f3f78e13a4..dff8ba1ef1 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1601,7 +1601,7 @@ async def edit( if view is not MISSING: self._state.prevent_view_updates_for(self.id) payload["components"] = view.to_components() if view else [] - if view and view.is_v2(): + if view and view.is_components_v2(): flags.is_components_v2 = True if file is not MISSING and files is not MISSING: raise InvalidArgument("cannot pass both file and files parameter to edit()") diff --git a/discord/ui/separator.py b/discord/ui/separator.py new file mode 100644 index 0000000000..740c842d80 --- /dev/null +++ b/discord/ui/separator.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from ..components import Separator as SeparatorComponent +from ..components import _component_factory +from ..enums import ComponentType, SeparatorSpacingSize +from .item import Item + +__all__ = ("Separator",) + +if TYPE_CHECKING: + from ..types.components import SeparatorComponent as SeparatorComponentPayload + from .view import View + + +S = TypeVar("S", bound="Separator") +V = TypeVar("V", bound="View", covariant=True) + + +class Separator(Item[V]): + """Represents a UI Separator. + + .. versionadded:: 2.7 + + Parameters + ---------- + divider: :class:`bool` + Whether the separator is a divider. Defaults to ``True``. + spacing: :class:`~discord.SeparatorSpacingSize` + The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`. + """ + + def __init__(self, *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small): + super().__init__() + + self.divider = divider + self.spacing = spacing + + self._underlying = SeparatorComponent._raw_construct( + type=ComponentType.text_display, + id=None, + divider=divider, + spacing=spacing, + ) + + @property + def type(self) -> ComponentType: + return self._underlying.type + + @property + def width(self) -> int: + return 5 + + def to_component_dict(self) -> SeparatorComponentPayload: + return self._underlying.to_dict() + + @classmethod + def from_component(cls: type[S], component: SeparatorComponent) -> S: + return cls(component.content) + + callback = None diff --git a/discord/ui/view.py b/discord/ui/view.py index 462fc325b5..dde94a23ce 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -240,9 +240,6 @@ def to_components(self) -> list[dict[str, Any]]: def key(item: Item) -> int: return item._rendered_row or 0 - # if self.is_v2(): - # components = [item.to_component_dict() for item in self.children] - # else: children = sorted(self.children, key=key) components: list[dict[str, Any]] = [] for _, group in groupby(children, key=key): diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index cde827aecc..38dde0e254 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -664,7 +664,7 @@ def handle_message_parameters( if view is not MISSING: payload["components"] = view.to_components() if view is not None else [] - if view and view.is_v2(): + if view and view.is_components_v2(): flags.is_components_v2 = True if poll is not MISSING: payload["poll"] = poll.to_dict() From ea29f72af70366e395e90b7459557ac25b5258d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 06:44:38 +0000 Subject: [PATCH 066/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/separator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 740c842d80..94540788d0 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -31,7 +31,12 @@ class Separator(Item[V]): The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`. """ - def __init__(self, *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small): + def __init__( + self, + *, + divider: bool = True, + spacing: SeparatorSpacingSize = SeparatorSpacingSize.small, + ): super().__init__() self.divider = divider From a9671ae47c9c56fc40188776705ad6f3d32c58a1 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:59:52 +0000 Subject: [PATCH 067/154] container --- discord/components.py | 15 +++ discord/ui/__init__.py | 3 + discord/ui/container.py | 249 ++++++++++++++++++++++++++++++++++++++++ discord/ui/file.py | 2 +- discord/ui/section.py | 2 +- discord/ui/thumbnail.py | 2 +- discord/ui/view.py | 13 ++- 7 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 discord/ui/container.py diff --git a/discord/components.py b/discord/components.py index 438539bf3e..a6f0cb6056 100644 --- a/discord/components.py +++ b/discord/components.py @@ -169,12 +169,27 @@ def __init__(self, data: ComponentPayload): _component_factory(d) for d in data.get("components", []) ] + @property + def width(self): + """Return the total item width used by this action row.""" + t = 0 + for item in self.children: + t += 1 if item.type is ComponentType.button else 5 + return t + def to_dict(self) -> ActionRowPayload: return { "type": int(self.type), "components": [child.to_dict() for child in self.children], } # type: ignore + @classmethod + def with_components(cls, *components): + return cls._raw_construct( + type=ComponentType.action_row, + id=None, + children=[c for c in components] + ) class InputText(Component): """Represents an Input Text field from the Discord Bot UI Kit. diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index 2c3d3992a1..ddaee48b88 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -18,3 +18,6 @@ from .text_display import * from .thumbnail import * from .view import * +from .file import * +from .separator import * +from .container import * diff --git a/discord/ui/container.py b/discord/ui/container.py new file mode 100644 index 0000000000..50a8abde52 --- /dev/null +++ b/discord/ui/container.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from ..colour import Colour +from ..components import Container as ContainerComponent +from ..components import _component_factory, ActionRow +from ..enums import ComponentType, SeparatorSpacingSize +from .item import Item +from .text_display import TextDisplay +from .section import Section +from .media_gallery import MediaGallery +from .separator import Separator +from .file import File + +__all__ = ("Container",) + +if TYPE_CHECKING: + from ..types.components import ContainerComponent as ContainerComponentPayload + from .view import View + + +C = TypeVar("C", bound="Container") +V = TypeVar("V", bound="View", covariant=True) + + +class Container(Item[V]): + """Represents a UI Container. Containers may contain up to 10 items. + + The current items supported are: + + - :class:`discord.ui.Button` + - :class:`discord.ui.Select` + - :class:`discord.ui.Section` + - :class:`discord.ui.TextDisplay` + - :class:`discord.ui.MediaGallery` + - :class:`discord.ui.File` + - :class:`discord.ui.Separator` + + + .. versionadded:: 2.7 + + Parameters + ---------- + *items: :class:`Item` + The initial items in this container, up to 10. + colour: Union[:class:`Colour`, :class:`int`] + The accent colour of the container. Aliased to ``color`` as well. + """ + + def __init__( + self, + *items: Item, + colour: int | Colour | None = None, + color: int | Colour | None = None, + spoiler: bool = False + ): + super().__init__() + + self.items = [i for i in items] + components = [i._underlying for i in items] + self._color = colour + + self._underlying = ContainerComponent._raw_construct( + type=ComponentType.section, + id=None, + components=components, + accent_color=colour, + spoiler=spoiler + ) + + def add_item(self, item: Item) -> None: + """Adds an item to the container. + + Parameters + ---------- + item: :class:`Item` + The item to add to the container. + + Raises + ------ + TypeError + An :class:`Item` was not passed. + ValueError + Maximum number of items has been exceeded (10). + """ + + if len(self.items) >= 10: + raise ValueError("maximum number of children exceeded") + + if not isinstance(item, Item): + raise TypeError(f"expected Item not {item.__class__!r}") + + self.items.append(item) + + # reuse weight system? + + if item._underlying.is_v2(): + self._underlying.components.append(item._underlying) + else: + found = False + for i in range(len(self._underlying.components) - 1, 0, -1): + row = self._underlying.components[i] + if isinstance(row, ActionRow) and row.width + item.width <= 5: # If an actionRow exists + row.children.append(item._underlying) + found = True + if not found: + row = ActionRow.with_components(item._underlying) + self._underlying.components.append(row) + + def add_section( + self, + *items: Item, accessory: Item = None + ): + """Adds a :class:`Section` to the container. + + To append a pre-existing :class:`Section` use the + :meth:`add_item` method instead. + + Parameters + ---------- + *items: :class:`Item` + The items contained in this section, up to 3. + Currently only supports :class:`~discord.ui.TextDisplay`. + 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`. + """ + # accept raw strings? + + section = Section( + *items, accessory=accessory + ) + + self.add_item(section) + + def add_text(self, content: str) -> None: + """Adds a :class:`TextDisplay` to the container. + + Parameters + ---------- + content: :class:`str` + The content of the TextDisplay + """ + + text = TextDisplay(content) + + self.add_item(text) + + def add_gallery( + self, + *items: Item, + ): + """Adds a :class:`MediaGallery` to the container. + + To append a pre-existing :class:`MediaGallery` use the + :meth:`add_item` method instead. + + Parameters + ---------- + *items: List[:class:`MediaGalleryItem`] + The media this gallery contains. + """ + # accept raw urls? + + g = MediaGallery( + *items + ) + + self.add_item(g) + + def add_file(self, url: str, spoiler: bool = False) -> None: + """Adds a :class:`TextDisplay` to the container. + + Parameters + ---------- + url: :class:`str` + The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`. + spoiler: Optional[:class:`bool`] + Whether the file is a spoiler. Defaults to ``False``. + """ + + f = File(url, spoiler=spoiler) + + self.add_item(f) + + def add_separator(self, *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small) -> None: + """Adds a :class:`Separator` to the container. + + Parameters + ---------- + divider: :class:`bool` + Whether the separator is a divider. Defaults to ``True``. + spacing: :class:`~discord.SeparatorSpacingSize` + The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`. + """ + + s = Separator(divider=divider, spacing=spacing) + + self.add_item(s) + + @property + def spoiler(self) -> bool: + """Whether the container is a spoiler. Defaults to ``False``.""" + return self._spoiler + + @spoiler.setter + def spoiler(self, spoiler: bool) -> None: + self._spoiler = spoiler + self._underlying.spoiler = spoiler + + @property + def colour(self) -> Colour | None: + return getattr(self, "_colour", None) + + @colour.setter + def colour(self, value: int | Colour | None): # type: ignore + if value is None or isinstance(value, Colour): + self._colour = value + elif isinstance(value, int): + self._colour = Colour(value=value) + else: + raise TypeError( + "Expected discord.Colour, int, or None but received" + f" {value.__class__.__name__} instead." + ) + self._underlying.accent_color = self.colour + + color = colour + + @property + def type(self) -> ComponentType: + return self._underlying.type + + @property + def width(self) -> int: + return 5 + + def to_component_dict(self) -> ContainerComponentPayload: + return self._underlying.to_dict() + + @classmethod + def from_component(cls: type[C], component: ContainerComponent) -> C: + from .view import _component_to_item + + items = [_component_to_item(c) for c in component.components] + return cls(*items, colour=component.accent_color, spoiler=component.spoiler) + + callback = None diff --git a/discord/ui/file.py b/discord/ui/file.py index a282a4f6ad..a368e0abc0 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -61,7 +61,7 @@ def url(self, value: str) -> None: @property def spoiler(self) -> bool: """Whether the file is a spoiler. Defaults to ``False``.""" - return self.spoiler + return self._spoiler @spoiler.setter def spoiler(self, spoiler: bool) -> None: diff --git a/discord/ui/section.py b/discord/ui/section.py index 1f5838c05d..8253680752 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -30,7 +30,7 @@ class Section(Item[V]): The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay`. accessory: Optional[:class:`Item`] - This section's accessory. This is displayed in the top right of the section. + 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`. """ diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 82b75f160e..5e21eade9a 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -74,7 +74,7 @@ def description(self, description: str | None) -> None: @property def spoiler(self) -> bool: """Whether the thumbnail is a spoiler. Defaults to ``False``.""" - return self.spoiler + return self._spoiler @spoiler.setter def spoiler(self, spoiler: bool) -> None: diff --git a/discord/ui/view.py b/discord/ui/view.py index dde94a23ce..0a487b7fea 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -36,13 +36,14 @@ from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent -from ..components import Component, FileComponent from ..components import MediaGallery as MediaGalleryComponent from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent -from ..components import _component_factory +from ..components import Separator as SeparatorComponent +from ..components import Container as ContainerComponent +from ..components import _component_factory, Component, FileComponent from ..utils import get from .item import Item, ItemCallbackType @@ -93,6 +94,14 @@ def _component_to_item(component: Component) -> Item: from .file import File return File.from_component(component) + if isinstance(component, SeparatorComponent): + from .separator import Separator + + return Separator.from_component(component) + if isinstance(component, ContainerComponent): + from .container import Container + + return Container.from_component(component) return Item.from_component(component) From 6c9f3eed1d2e10a383044ec60f0a525cb8eb828f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:00:17 +0000 Subject: [PATCH 068/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 5 ++--- discord/ui/__init__.py | 6 +++--- discord/ui/container.py | 40 ++++++++++++++++++++-------------------- discord/ui/view.py | 8 +++++--- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/discord/components.py b/discord/components.py index a6f0cb6056..59d84ebedb 100644 --- a/discord/components.py +++ b/discord/components.py @@ -186,11 +186,10 @@ def to_dict(self) -> ActionRowPayload: @classmethod def with_components(cls, *components): return cls._raw_construct( - type=ComponentType.action_row, - id=None, - children=[c for c in components] + type=ComponentType.action_row, id=None, children=[c for c in components] ) + class InputText(Component): """Represents an Input Text field from the Discord Bot UI Kit. This inherits from :class:`Component`. diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index ddaee48b88..473ac45563 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -9,15 +9,15 @@ """ from .button import * +from .container import * +from .file import * from .input_text import * from .item import * from .media_gallery import * from .modal import * from .section import * from .select import * +from .separator import * from .text_display import * from .thumbnail import * from .view import * -from .file import * -from .separator import * -from .container import * diff --git a/discord/ui/container.py b/discord/ui/container.py index 50a8abde52..931df23020 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -3,15 +3,16 @@ from typing import TYPE_CHECKING, TypeVar from ..colour import Colour +from ..components import ActionRow from ..components import Container as ContainerComponent -from ..components import _component_factory, ActionRow +from ..components import _component_factory from ..enums import ComponentType, SeparatorSpacingSize +from .file import File from .item import Item -from .text_display import TextDisplay -from .section import Section from .media_gallery import MediaGallery +from .section import Section from .separator import Separator -from .file import File +from .text_display import TextDisplay __all__ = ("Container",) @@ -37,7 +38,6 @@ class Container(Item[V]): - :class:`discord.ui.File` - :class:`discord.ui.Separator` - .. versionadded:: 2.7 Parameters @@ -50,10 +50,10 @@ class Container(Item[V]): def __init__( self, - *items: Item, + *items: Item, colour: int | Colour | None = None, color: int | Colour | None = None, - spoiler: bool = False + spoiler: bool = False, ): super().__init__() @@ -66,7 +66,7 @@ def __init__( id=None, components=components, accent_color=colour, - spoiler=spoiler + spoiler=spoiler, ) def add_item(self, item: Item) -> None: @@ -101,17 +101,16 @@ def add_item(self, item: Item) -> None: found = False for i in range(len(self._underlying.components) - 1, 0, -1): row = self._underlying.components[i] - if isinstance(row, ActionRow) and row.width + item.width <= 5: # If an actionRow exists + if ( + isinstance(row, ActionRow) and row.width + item.width <= 5 + ): # If an actionRow exists row.children.append(item._underlying) found = True if not found: row = ActionRow.with_components(item._underlying) self._underlying.components.append(row) - def add_section( - self, - *items: Item, accessory: Item = None - ): + def add_section(self, *items: Item, accessory: Item = None): """Adds a :class:`Section` to the container. To append a pre-existing :class:`Section` use the @@ -128,9 +127,7 @@ def add_section( """ # accept raw strings? - section = Section( - *items, accessory=accessory - ) + section = Section(*items, accessory=accessory) self.add_item(section) @@ -163,9 +160,7 @@ def add_gallery( """ # accept raw urls? - g = MediaGallery( - *items - ) + g = MediaGallery(*items) self.add_item(g) @@ -184,7 +179,12 @@ def add_file(self, url: str, spoiler: bool = False) -> None: self.add_item(f) - def add_separator(self, *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small) -> None: + def add_separator( + self, + *, + divider: bool = True, + spacing: SeparatorSpacingSize = SeparatorSpacingSize.small, + ) -> None: """Adds a :class:`Separator` to the container. Parameters diff --git a/discord/ui/view.py b/discord/ui/view.py index 0a487b7fea..ea6e00f9dc 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -36,14 +36,16 @@ from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent +from ..components import Component +from ..components import Container as ContainerComponent +from ..components import FileComponent from ..components import MediaGallery as MediaGalleryComponent from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent +from ..components import Separator as SeparatorComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent -from ..components import Separator as SeparatorComponent -from ..components import Container as ContainerComponent -from ..components import _component_factory, Component, FileComponent +from ..components import _component_factory from ..utils import get from .item import Item, ItemCallbackType From 85c15d4541608a95d025d390459c6fc1888ab5a4 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:04:30 +0000 Subject: [PATCH 069/154] nobreak --- discord/ui/container.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 931df23020..b93f970216 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -98,15 +98,14 @@ def add_item(self, item: Item) -> None: if item._underlying.is_v2(): self._underlying.components.append(item._underlying) else: - found = False for i in range(len(self._underlying.components) - 1, 0, -1): row = self._underlying.components[i] if ( isinstance(row, ActionRow) and row.width + item.width <= 5 - ): # If an actionRow exists + ): # If a valid ActionRow exists row.children.append(item._underlying) - found = True - if not found: + break + else: row = ActionRow.with_components(item._underlying) self._underlying.components.append(row) From cb2ec4cc430e814783b1049d1191636c5822029b Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:15:15 +0000 Subject: [PATCH 070/154] docs --- docs/api/data_classes.rst | 10 +++++++++ docs/api/models.rst | 45 +++++++++++++++++++++++++++++++++++++++ docs/api/ui_kit.rst | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 1d891b90cd..2e2e814289 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -26,6 +26,16 @@ dynamic attributes in mind. .. autoclass:: SelectOption :members: +.. attributetable:: MediaGalleryItem + +.. autoclass:: SelectOption + :members: + +.. attributetable:: UnfurledMediaItem + +.. autoclass:: UnfurledMediaItem + :members: + .. attributetable:: Intents .. autoclass:: Intents diff --git a/docs/api/models.rst b/docs/api/models.rst index cb702b2c38..ed8452ec1c 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -368,6 +368,9 @@ Interactions .. autoclass:: AuthorizingIntegrationOwners() :members: +Message Components +------------ + .. attributetable:: Component .. autoclass:: Component() @@ -390,6 +393,48 @@ Interactions :members: :inherited-members: +.. attributetable:: Section + +.. autoclass:: Section() + :members: + :inherited-members: + +.. attributetable:: TextDisplay + +.. autoclass:: TextDisplay() + :members: + :inherited-members: + +.. attributetable:: Thumbnail + +.. autoclass:: Thumbnail() + :members: + :inherited-members: + +.. attributetable:: MediaGallery + +.. autoclass:: MediaGallery() + :members: + :inherited-members: + +.. attributetable:: FileComponent + +.. autoclass:: FileComponent() + :members: + :inherited-members: + +.. attributetable:: Separator + +.. autoclass:: Separator() + :members: + :inherited-members: + +.. attributetable:: Container + +.. autoclass:: Container() + :members: + :inherited-members: + Emoji ----- diff --git a/docs/api/ui_kit.rst b/docs/api/ui_kit.rst index 18bb9de89b..ad2769eb03 100644 --- a/docs/api/ui_kit.rst +++ b/docs/api/ui_kit.rst @@ -55,6 +55,48 @@ Objects :members: :inherited-members: +.. attributetable:: discord.ui.Section + +.. autoclass:: discord.ui.Section + :members: + :inherited-members: + +.. attributetable:: discord.ui.TextDisplay + +.. autoclass:: discord.ui.TextDisplay + :members: + :inherited-members: + +.. attributetable:: discord.ui.Thumbnail + +.. autoclass:: discord.ui.Thumbnail + :members: + :inherited-members: + +.. attributetable:: discord.ui.MediaGallery + +.. autoclass:: discord.ui.MediaGallery + :members: + :inherited-members: + +.. attributetable:: discord.ui.File + +.. autoclass:: discord.ui.File + :members: + :inherited-members: + +.. attributetable:: discord.ui.Separator + +.. autoclass:: discord.ui.Separator + :members: + :inherited-members: + +.. attributetable:: discord.ui.Container + +.. autoclass:: discord.ui.Container + :members: + :inherited-members: + .. attributetable:: discord.ui.Modal .. autoclass:: discord.ui.Modal From 09627f17f0e7b8ff6740fdbdd0a7e9d276082244 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:21:47 +0000 Subject: [PATCH 071/154] int with_components --- discord/webhook/async_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 38dde0e254..539c6aa8a2 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -349,7 +349,7 @@ def execute_webhook( payload["thread_name"] = thread_name if with_components is not None: - params["with_components"] = with_components + params["with_components"] = int(with_components) route = Route( "POST", From 86246a2d98633ac95d6df4e153f7f5cf2159caeb Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:29:40 +0000 Subject: [PATCH 072/154] container --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index b93f970216..432c3d192c 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -62,7 +62,7 @@ def __init__( self._color = colour self._underlying = ContainerComponent._raw_construct( - type=ComponentType.section, + type=ComponentType.container, id=None, components=components, accent_color=colour, From 11ac9dac84d04de9df949a3c6769f4c93be1a6a7 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:32:26 +0000 Subject: [PATCH 073/154] require accessory --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 432c3d192c..b3ac622625 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -109,7 +109,7 @@ def add_item(self, item: Item) -> None: row = ActionRow.with_components(item._underlying) self._underlying.components.append(row) - def add_section(self, *items: Item, accessory: Item = None): + def add_section(self, *items: Item, accessory: Item): """Adds a :class:`Section` to the container. To append a pre-existing :class:`Section` use the From 4bd1647f753bddd1ce68a9d6c1d6d2b9810440bb Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:36:49 +0000 Subject: [PATCH 074/154] int --- discord/enums.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/enums.py b/discord/enums.py index 73fdb0365a..840e84fd06 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1081,6 +1081,9 @@ class SeparatorSpacingSize(Enum): small = 1 large = 2 + def __int__(self): + return self.value + class MediaItemLoadingState(Enum): """An :class:`~discord.UnfurledMediaItem`'s ``loading_state``.""" @@ -1090,6 +1093,9 @@ class MediaItemLoadingState(Enum): loaded_success = 2 loaded_not_found = 3 + def __int__(self): + return self.value + T = TypeVar("T") From 04d748ad8a4e3fe83da27e88ebd0e8b3256eb7f3 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:39:52 +0000 Subject: [PATCH 075/154] sep --- discord/ui/separator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 94540788d0..07b2979f42 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -43,7 +43,7 @@ def __init__( self.spacing = spacing self._underlying = SeparatorComponent._raw_construct( - type=ComponentType.text_display, + type=ComponentType.separator, id=None, divider=divider, spacing=spacing, From 69badd8f115b5b7fe16b67f77a026a030d52d5af Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:22:31 +0000 Subject: [PATCH 076/154] fix separator --- discord/ui/container.py | 6 +----- discord/ui/separator.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index b3ac622625..decb05d4ce 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -85,9 +85,6 @@ def add_item(self, item: Item) -> None: Maximum number of items has been exceeded (10). """ - if len(self.items) >= 10: - raise ValueError("maximum number of children exceeded") - if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") @@ -98,8 +95,7 @@ def add_item(self, item: Item) -> None: if item._underlying.is_v2(): self._underlying.components.append(item._underlying) else: - for i in range(len(self._underlying.components) - 1, 0, -1): - row = self._underlying.components[i] + for row in reversed(self._underlying.components): if ( isinstance(row, ActionRow) and row.width + item.width <= 5 ): # If a valid ActionRow exists diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 07b2979f42..694b599205 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -62,6 +62,6 @@ def to_component_dict(self) -> SeparatorComponentPayload: @classmethod def from_component(cls: type[S], component: SeparatorComponent) -> S: - return cls(component.content) + return cls(divider=component.divider, spacing=component.spacing) callback = None From f6938d75e45a570b8fee685621ef3a4849a9cdf5 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:34:26 +0000 Subject: [PATCH 077/154] fix file --- discord/ui/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/file.py b/discord/ui/file.py index a368e0abc0..1f23319bc3 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -74,7 +74,7 @@ def to_component_dict(self) -> FileComponentPayload: @classmethod def from_component(cls: type[F], component: FileComponent) -> F: return cls( - component.media and component.media.url, + component.file and component.file.url, spoiler=component.spoiler, ) From 7e942578693b5781a2b301aa983421255ecb3f76 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:48:19 +0000 Subject: [PATCH 078/154] handle actionrow in container --- discord/ui/container.py | 9 +++++++-- discord/ui/view.py | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index decb05d4ce..c03798bf7c 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -58,16 +58,21 @@ def __init__( super().__init__() self.items = [i for i in items] - components = [i._underlying for i in items] self._color = colour self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, id=None, - components=components, + components=[], accent_color=colour, spoiler=spoiler, ) + for i in items: + if isinstance(i, ActionRow): + for c in i.children: + self.add_item(c) + else: + self.add_item(i) def add_item(self, item: Item) -> None: """Adds an item to the container. diff --git a/discord/ui/view.py b/discord/ui/view.py index ea6e00f9dc..366de185ae 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -104,6 +104,10 @@ def _component_to_item(component: Component) -> Item: from .container import Container return Container.from_component(component) + if isinstance(component, ActionRow): + # Handle ActionRow.children manually, or design ui.ActionRow? + + return component return Item.from_component(component) From 7daff7d0f7cb54a56526e599a0b14053c0bfbb9d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:49:45 +0000 Subject: [PATCH 079/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 366de185ae..d94d820935 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -106,7 +106,7 @@ def _component_to_item(component: Component) -> Item: return Container.from_component(component) if isinstance(component, ActionRow): # Handle ActionRow.children manually, or design ui.ActionRow? - + return component return Item.from_component(component) From b963d517f5bb3c7ea717d510c6a612970c603000 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:53:22 +0000 Subject: [PATCH 080/154] neater fix --- discord/ui/container.py | 12 ++++-------- discord/ui/view.py | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index c03798bf7c..775d5adfd0 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -13,6 +13,7 @@ from .section import Section from .separator import Separator from .text_display import TextDisplay +from .view import _walk_all_components __all__ = ("Container",) @@ -58,21 +59,16 @@ def __init__( super().__init__() self.items = [i for i in items] + components = [i._underlying for i in items] self._color = colour self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, id=None, - components=[], + components=components, accent_color=colour, spoiler=spoiler, ) - for i in items: - if isinstance(i, ActionRow): - for c in i.children: - self.add_item(c) - else: - self.add_item(i) def add_item(self, item: Item) -> None: """Adds an item to the container. @@ -243,7 +239,7 @@ def to_component_dict(self) -> ContainerComponentPayload: def from_component(cls: type[C], component: ContainerComponent) -> C: from .view import _component_to_item - items = [_component_to_item(c) for c in component.components] + items = [_component_to_item(c) for c in _walk_all_components(component.components)] return cls(*items, colour=component.accent_color, spoiler=component.spoiler) callback = None diff --git a/discord/ui/view.py b/discord/ui/view.py index d94d820935..1deabbc8e8 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -49,7 +49,7 @@ from ..utils import get from .item import Item, ItemCallbackType -__all__ = ("View", "_component_to_item") +__all__ = ("View", "_component_to_item", "_walk_all_components") if TYPE_CHECKING: @@ -104,7 +104,7 @@ def _component_to_item(component: Component) -> Item: from .container import Container return Container.from_component(component) - if isinstance(component, ActionRow): + if isinstance(component, ActionRowComponent): # Handle ActionRow.children manually, or design ui.ActionRow? return component From b5cca6050c3d45daf9eb96a085b0c6bc65d215c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:54:32 +0000 Subject: [PATCH 081/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 775d5adfd0..e595331023 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -239,7 +239,9 @@ def to_component_dict(self) -> ContainerComponentPayload: def from_component(cls: type[C], component: ContainerComponent) -> C: from .view import _component_to_item - items = [_component_to_item(c) for c in _walk_all_components(component.components)] + items = [ + _component_to_item(c) for c in _walk_all_components(component.components) + ] return cls(*items, colour=component.accent_color, spoiler=component.spoiler) callback = None From e64f0b483fb88c7d25dbb31c84bafa8f22e267e5 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:59:40 +0000 Subject: [PATCH 082/154] maybe? --- discord/ui/container.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index e595331023..16c38bd9dd 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -58,17 +58,18 @@ def __init__( ): super().__init__() - self.items = [i for i in items] - components = [i._underlying for i in items] + self.items = [] self._color = colour self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, id=None, - components=components, + components=[], accent_color=colour, spoiler=spoiler, ) + for i in items: + self.add_item(i) def add_item(self, item: Item) -> None: """Adds an item to the container. From 222b5040691d17910eaebf964de2f78230d95233 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:04:27 +0000 Subject: [PATCH 083/154] actual file fix --- discord/components.py | 2 +- discord/ui/file.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/components.py b/discord/components.py index 59d84ebedb..63d59d0a95 100644 --- a/discord/components.py +++ b/discord/components.py @@ -826,7 +826,7 @@ def __init__(self, data: FileComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: str = data.get("id") self.file: UnfurledMediaItem = ( - umi := data.get("media") + umi := data.get("file") ) and UnfurledMediaItem.from_dict(umi, state=state) self.spoiler: bool | None = data.get("spoiler") diff --git a/discord/ui/file.py b/discord/ui/file.py index 1f23319bc3..5b51d01ba7 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -29,14 +29,14 @@ class File(Item[V]): def __init__(self, url: str, *, spoiler: bool = False): super().__init__() - media = UnfurledMediaItem(url) + file = UnfurledMediaItem(url) self._url = url self._spoiler: bool = spoiler self._underlying = FileComponent._raw_construct( type=ComponentType.file, id=None, - file=media, + file=file, spoiler=spoiler, ) @@ -56,7 +56,7 @@ def url(self) -> str: @url.setter def url(self, value: str) -> None: self._url = value - self._underlying.media.url = value + self._underlying.file.url = value @property def spoiler(self) -> bool: From 55aa2af2beabb535432d61d226d09d917d55f3df Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:14:33 +0000 Subject: [PATCH 084/154] handle ui.File from_component case --- discord/ui/file.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/ui/file.py b/discord/ui/file.py index 5b51d01ba7..5c109c1017 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar +from urllib.parse import urlparse from ..components import FileComponent, UnfurledMediaItem, _component_factory from ..enums import ComponentType @@ -73,8 +74,11 @@ def to_component_dict(self) -> FileComponentPayload: @classmethod def from_component(cls: type[F], component: FileComponent) -> F: + url = component.file and component.file.url + if not url.startswith("attachment://"): + url = "attachment://" + urlparse(url).path.rsplit("/", 1)[-1] return cls( - component.file and component.file.url, + url, spoiler=component.spoiler, ) From 87ac602cf382d9997357b58f45029720ff6ca70c Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:51:41 +0000 Subject: [PATCH 085/154] decorator support? --- discord/ui/container.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 16c38bd9dd..e91e6ebafd 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar +from functools import partial +from typing import TYPE_CHECKING, TypeVar, ClassVar from ..colour import Colour from ..components import ActionRow @@ -14,6 +15,7 @@ from .separator import Separator from .text_display import TextDisplay from .view import _walk_all_components +from .item import Item, ItemCallbackType __all__ = ("Container",) @@ -49,6 +51,17 @@ class Container(Item[V]): The accent colour of the container. Aliased to ``color`` as well. """ + __container_children_items__: ClassVar[list[ItemCallbackType]] = [] + + def __init_subclass__(cls) -> None: + children: list[ItemCallbackType] = [] + for base in reversed(cls.__mro__): + for member in base.__dict__.values(): + if hasattr(member, "__discord_ui_model_type__"): + children.append(member) + + cls.__container_children_items__ = children + def __init__( self, *items: Item, @@ -60,6 +73,14 @@ def __init__( self.items = [] self._color = colour + for func in self.__container_children_items__: + item: Item = func.__discord_ui_model_type__( + **func.__discord_ui_model_kwargs__ + ) + item.callback = partial(func, self.view, item) + if self.view: + setattr(self.view, func.__name__, item) + self.add_item(item) self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, @@ -89,6 +110,8 @@ def add_item(self, item: Item) -> None: if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") + + item._view = self.view self.items.append(item) @@ -225,6 +248,12 @@ def colour(self, value: int | Colour | None): # type: ignore color = colour + @view.setter + def view(self, value): + self._view = value + for item in self.items: + item._view = value + @property def type(self) -> ComponentType: return self._underlying.type From 5d769874e425251fe13382ffa4d705f00b6bb69e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:52:30 +0000 Subject: [PATCH 086/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index e91e6ebafd..1b1516094e 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -1,7 +1,7 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, TypeVar, ClassVar +from typing import TYPE_CHECKING, ClassVar, TypeVar from ..colour import Colour from ..components import ActionRow @@ -9,13 +9,12 @@ from ..components import _component_factory from ..enums import ComponentType, SeparatorSpacingSize from .file import File -from .item import Item +from .item import Item, ItemCallbackType from .media_gallery import MediaGallery from .section import Section from .separator import Separator from .text_display import TextDisplay from .view import _walk_all_components -from .item import Item, ItemCallbackType __all__ = ("Container",) @@ -110,7 +109,7 @@ def add_item(self, item: Item) -> None: if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") - + item._view = self.view self.items.append(item) From 4acac2b2263f225e2397b38e18360d2c3e6684db Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:54:55 +0000 Subject: [PATCH 087/154] setter --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 1b1516094e..57da0b7df2 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -247,7 +247,7 @@ def colour(self, value: int | Colour | None): # type: ignore color = colour - @view.setter + @Item.view.setter def view(self, value): self._view = value for item in self.items: From b07825b48a702d7b2715608a22dcdc2c442fe2f4 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:01:01 +0000 Subject: [PATCH 088/154] swap --- discord/ui/container.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 57da0b7df2..1d181da16c 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -72,14 +72,6 @@ def __init__( self.items = [] self._color = colour - for func in self.__container_children_items__: - item: Item = func.__discord_ui_model_type__( - **func.__discord_ui_model_kwargs__ - ) - item.callback = partial(func, self.view, item) - if self.view: - setattr(self.view, func.__name__, item) - self.add_item(item) self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, @@ -88,6 +80,15 @@ def __init__( accent_color=colour, spoiler=spoiler, ) + + for func in self.__container_children_items__: + item: Item = func.__discord_ui_model_type__( + **func.__discord_ui_model_kwargs__ + ) + item.callback = partial(func, self.view, item) + if self.view: + setattr(self.view, func.__name__, item) + self.add_item(item) for i in items: self.add_item(i) From 5608013b48eeff72b7beb468eb9e3e4cb6d96a12 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:01:29 +0000 Subject: [PATCH 089/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 1d181da16c..a6eb22e130 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -80,7 +80,7 @@ def __init__( accent_color=colour, spoiler=spoiler, ) - + for func in self.__container_children_items__: item: Item = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ From dd2c16f3b62e699e8537de7f65eb6d3b8863d45d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:29:04 +0000 Subject: [PATCH 090/154] silly code thank u plun --- discord/ui/container.py | 8 +++++++- discord/ui/view.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index a6eb22e130..1cd7484426 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -85,9 +85,11 @@ def __init__( item: Item = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ ) - item.callback = partial(func, self.view, item) if self.view: + item.callback = partial(func, self.view, item) setattr(self.view, func.__name__, item) + else: + item._tmp_func = func self.add_item(item) for i in items: self.add_item(i) @@ -252,6 +254,10 @@ def colour(self, value: int | Colour | None): # type: ignore def view(self, value): self._view = value for item in self.items: + if getattr(item, "_tmp_func", None): + item.callback = partial(item._tmp_func, self.view, item) + setattr(self.view, item._tmp_func.__name__, item) + delattr(item, "_tmp_func") item._view = value @property diff --git a/discord/ui/view.py b/discord/ui/view.py index 1deabbc8e8..352ecf2c5f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -336,6 +336,8 @@ def add_item(self, item: Item) -> None: self.__weights.add_item(item) item._view = self + if hasattr(item, "items"): + item.view = self self.children.append(item) def remove_item(self, item: Item) -> None: @@ -647,6 +649,16 @@ def add_view(self, view: View, message_id: int | None = None): for item in view.children: if item.is_dispatchable(): self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore + elif hasattr(item, "items"): + for sub_item in item.items: + if sub_item.is_dispatchable(): + self._views[(sub_item.type.value, message_id, sub_item.custom_id)] = (view, sub_item) + elif hasattr(item, "accessory"): + if sub_item.accessory.is_dispatchable(): + self._views[(sub_item.accessory.type.value, message_id, sub_item.accessory.custom_id)] = (view, sub_item.accessory) + elif hasattr(item, "accessory"): + if item.accessory.is_dispatchable(): + self._views[(item.accessory.type.value, message_id, item.accessory.custom_id)] = (view, item.accessory) if message_id is not None: self._synced_message_views[message_id] = view From 1d56b9bee669397f4d1b87e7377a001d0f4850d9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:29:27 +0000 Subject: [PATCH 091/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 352ecf2c5f..768b8f17ba 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -652,13 +652,27 @@ def add_view(self, view: View, message_id: int | None = None): elif hasattr(item, "items"): for sub_item in item.items: if sub_item.is_dispatchable(): - self._views[(sub_item.type.value, message_id, sub_item.custom_id)] = (view, sub_item) + self._views[ + (sub_item.type.value, message_id, sub_item.custom_id) + ] = (view, sub_item) elif hasattr(item, "accessory"): if sub_item.accessory.is_dispatchable(): - self._views[(sub_item.accessory.type.value, message_id, sub_item.accessory.custom_id)] = (view, sub_item.accessory) + self._views[ + ( + sub_item.accessory.type.value, + message_id, + sub_item.accessory.custom_id, + ) + ] = (view, sub_item.accessory) elif hasattr(item, "accessory"): if item.accessory.is_dispatchable(): - self._views[(item.accessory.type.value, message_id, item.accessory.custom_id)] = (view, item.accessory) + self._views[ + ( + item.accessory.type.value, + message_id, + item.accessory.custom_id, + ) + ] = (view, item.accessory) if message_id is not None: self._synced_message_views[message_id] = view From e0b53aea051a70b2ae9d9120e97fcf07de6acfcf Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:39:08 +0000 Subject: [PATCH 092/154] decorator in section --- discord/ui/section.py | 45 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 8253680752..d94e09056e 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -5,7 +5,7 @@ from ..components import Section as SectionComponent from ..components import _component_factory from ..enums import ComponentType -from .item import Item +from .item import Item, ItemCallbackType from .text_display import TextDisplay __all__ = ("Section",) @@ -34,19 +34,44 @@ class Section(Item[V]): Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`. """ + __section_accessory_item__: ClassVar[ItemCallbackType] = None + + def __init_subclass__(cls) -> None: + accessory: ItemCallbackType = None + for base in reversed(cls.__mro__): + for member in base.__dict__.values(): + if hasattr(member, "__discord_ui_model_type__"): + accessory = member + + cls.__section_accessory_item__ = accessory + def __init__(self, *items: Item, accessory: Item = None): super().__init__() - self.items = [i for i in items] + self.items = [] components = [i._underlying for i in items] - self.accessory = accessory + self.accessory = None self._underlying = SectionComponent._raw_construct( type=ComponentType.section, id=None, - components=components, - accessory=accessory and accessory._underlying, + components=[], + accessory=None, ) + if func := self.__section_accessory_item__: + item: Item = func.__discord_ui_model_type__( + **func.__discord_ui_model_kwargs__ + ) + if self.view: + item.callback = partial(func, self.view, item) + setattr(self.view, func.__name__, item) + else: + item._tmp_func = func + self.set_accessory(item) + elif accessory: + self.set_accessory(accessory) + for i in items: + self.add_item(i) def add_item(self, item: Item) -> None: """Adds an item to the section. @@ -117,6 +142,16 @@ def set_accessory(self, item: Item) -> None: self.accessory = item self._underlying.accessory = item._underlying + @Item.view.setter + def view(self, value): + self._view = value + if self.accessory: + if getattr(self.accessory, "_tmp_func", None): + self.accessory.callback = partial(self.accessory._tmp_func, self.view, self.accessory) + setattr(self.view, self.accessory._tmp_func.__name__, self.accessory) + delattr(self.accessory, "_tmp_func") + self.accessory._view = value + @property def type(self) -> ComponentType: return self._underlying.type From 8ea9d3f077cd0b4c60494e98272de20f9d7d15cc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:40:23 +0000 Subject: [PATCH 093/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index d94e09056e..f281badb96 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -49,7 +49,7 @@ def __init__(self, *items: Item, accessory: Item = None): super().__init__() self.items = [] - components = [i._underlying for i in items] + [i._underlying for i in items] self.accessory = None self._underlying = SectionComponent._raw_construct( @@ -147,7 +147,9 @@ def view(self, value): self._view = value if self.accessory: if getattr(self.accessory, "_tmp_func", None): - self.accessory.callback = partial(self.accessory._tmp_func, self.view, self.accessory) + self.accessory.callback = partial( + self.accessory._tmp_func, self.view, self.accessory + ) setattr(self.view, self.accessory._tmp_func.__name__, self.accessory) delattr(self.accessory, "_tmp_func") self.accessory._view = value From d1a3711957877129293192fd817f1bff8861afa4 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:41:16 +0000 Subject: [PATCH 094/154] imports --- discord/ui/section.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index f281badb96..f5c2b94279 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, TypeVar, ClassVar +from functools import partial from ..components import Section as SectionComponent from ..components import _component_factory From 505d431e197de926cef77178da5053c678fd0cce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:42:03 +0000 Subject: [PATCH 095/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index f5c2b94279..63ca03e669 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar, ClassVar from functools import partial +from typing import TYPE_CHECKING, ClassVar, TypeVar from ..components import Section as SectionComponent from ..components import _component_factory From 64a8223cb47ee01ccc8604699c9d4e616c7ab306 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:53:16 +0000 Subject: [PATCH 096/154] extend weight --- discord/ui/view.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 768b8f17ba..13bbf039a8 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -131,6 +131,8 @@ def find_open_space(self, item: Item) -> int: raise ValueError("could not find open space for item") def add_item(self, item: Item) -> None: + if item._underlying.is_v2() and not self.requires_v2(): + self.weights.extend([0,0,0,0,0]) if item.row is not None: total = self.weights[item.row] + item.width if total > 5: @@ -152,6 +154,9 @@ def remove_item(self, item: Item) -> None: def clear(self) -> None: self.weights = [0, 0, 0, 0, 0] + def requires_v2(self) -> bool: + return sum(w > 0 for w in self.weights) > 5 + class View: """Represents a UI view. @@ -565,7 +570,7 @@ def is_components_v2(self) -> bool: A view containing V2 components may not be sent alongside message content or embeds. """ - return any([item._underlying.is_v2() for item in self.children]) + return any([item._underlying.is_v2() for item in self.children]) or self.__weights.requires_v2() async def wait(self) -> bool: """Waits until the view has finished interacting. From 45271d880d3e88c9f76b671c3ef18cbb815d2eef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:53:43 +0000 Subject: [PATCH 097/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 13bbf039a8..313b8b9914 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -132,7 +132,7 @@ def find_open_space(self, item: Item) -> int: def add_item(self, item: Item) -> None: if item._underlying.is_v2() and not self.requires_v2(): - self.weights.extend([0,0,0,0,0]) + self.weights.extend([0, 0, 0, 0, 0]) if item.row is not None: total = self.weights[item.row] + item.width if total > 5: @@ -570,7 +570,10 @@ def is_components_v2(self) -> bool: A view containing V2 components may not be sent alongside message content or embeds. """ - return any([item._underlying.is_v2() for item in self.children]) or self.__weights.requires_v2() + return ( + any([item._underlying.is_v2() for item in self.children]) + or self.__weights.requires_v2() + ) async def wait(self) -> bool: """Waits until the view has finished interacting. From a2490f6e9043fca7900b272506dcadb851175eef Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:59:30 +0000 Subject: [PATCH 098/154] subitem --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 313b8b9914..642d0558f7 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -663,7 +663,7 @@ def add_view(self, view: View, message_id: int | None = None): self._views[ (sub_item.type.value, message_id, sub_item.custom_id) ] = (view, sub_item) - elif hasattr(item, "accessory"): + elif hasattr(sub_item, "accessory"): if sub_item.accessory.is_dispatchable(): self._views[ ( From 5d6f5297f6b743bdc1680020e79657ab7930b47e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:21:12 +0000 Subject: [PATCH 099/154] meh --- discord/ui/view.py | 47 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 642d0558f7..202bee47bc 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -657,30 +657,31 @@ def add_view(self, view: View, message_id: int | None = None): for item in view.children: if item.is_dispatchable(): self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore - elif hasattr(item, "items"): - for sub_item in item.items: - if sub_item.is_dispatchable(): - self._views[ - (sub_item.type.value, message_id, sub_item.custom_id) - ] = (view, sub_item) - elif hasattr(sub_item, "accessory"): - if sub_item.accessory.is_dispatchable(): + else: + if hasattr(item, "items"): + for sub_item in item.items: + if sub_item.is_dispatchable(): self._views[ - ( - sub_item.accessory.type.value, - message_id, - sub_item.accessory.custom_id, - ) - ] = (view, sub_item.accessory) - elif hasattr(item, "accessory"): - if item.accessory.is_dispatchable(): - self._views[ - ( - item.accessory.type.value, - message_id, - item.accessory.custom_id, - ) - ] = (view, item.accessory) + (sub_item.type.value, message_id, sub_item.custom_id) + ] = (view, sub_item) + elif hasattr(sub_item, "accessory"): + if sub_item.accessory.is_dispatchable(): + self._views[ + ( + sub_item.accessory.type.value, + message_id, + sub_item.accessory.custom_id, + ) + ] = (view, sub_item.accessory) + if hasattr(item, "accessory"): + if item.accessory.is_dispatchable(): + self._views[ + ( + item.accessory.type.value, + message_id, + item.accessory.custom_id, + ) + ] = (view, item.accessory) if message_id is not None: self._synced_message_views[message_id] = view From abfa1e649f82ac272c097516ee26f19ffc670a99 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 1 Mar 2025 16:52:05 +0000 Subject: [PATCH 100/154] full ID support and general cleanup --- discord/components.py | 33 ++++++++++++++++------------ discord/types/components.py | 4 ++-- discord/ui/button.py | 5 +++++ discord/ui/container.py | 43 +++++++++++++++++++++---------------- discord/ui/file.py | 13 +++++------ discord/ui/input_text.py | 2 ++ discord/ui/item.py | 20 +++++++++++++++++ discord/ui/media_gallery.py | 8 +++---- discord/ui/section.py | 14 ++++++------ discord/ui/select.py | 15 +++++++++++++ discord/ui/separator.py | 30 ++++++++++++++++++-------- discord/ui/text_display.py | 22 ++++++++++--------- discord/ui/thumbnail.py | 17 ++++++--------- 13 files changed, 144 insertions(+), 82 deletions(-) diff --git a/discord/components.py b/discord/components.py index 63d59d0a95..1af3ff0b79 100644 --- a/discord/components.py +++ b/discord/components.py @@ -106,8 +106,8 @@ class Component: ---------- type: :class:`ComponentType` The type of component. - id: :class:`str` - The component's ID. + id: :class:`int` + The component's ID. If not provided by the user, it's automatically incremented. """ __slots__: tuple[str, ...] = ("type", "id") @@ -164,7 +164,7 @@ class ActionRow(Component): def __init__(self, data: ComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.children: list[Component] = [ _component_factory(d) for d in data.get("components", []) ] @@ -180,13 +180,14 @@ def width(self): def to_dict(self) -> ActionRowPayload: return { "type": int(self.type), + "id": self.id, "components": [child.to_dict() for child in self.children], } # type: ignore @classmethod - def with_components(cls, *components): + def with_components(cls, *components, id=None): return cls._raw_construct( - type=ComponentType.action_row, id=None, children=[c for c in components] + type=ComponentType.action_row, id=id, children=[c for c in components] ) @@ -232,7 +233,7 @@ class InputText(Component): def __init__(self, data: InputTextComponentPayload): self.type = ComponentType.input_text - self.id: str = data.get("id") + self.id: int = data.get("id") self.style: InputTextStyle = try_enum(InputTextStyle, data["style"]) self.custom_id = data["custom_id"] self.label: str = data.get("label", None) @@ -245,6 +246,7 @@ def __init__(self, data: InputTextComponentPayload): def to_dict(self) -> InputTextComponentPayload: payload = { "type": 4, + "id": self.id, "style": self.style.value, "label": self.label, } @@ -315,7 +317,7 @@ class Button(Component): def __init__(self, data: ButtonComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.style: ButtonStyle = try_enum(ButtonStyle, data["style"]) self.custom_id: str | None = data.get("custom_id") self.url: str | None = data.get("url") @@ -331,6 +333,7 @@ def __init__(self, data: ButtonComponentPayload): def to_dict(self) -> ButtonComponentPayload: payload = { "type": 2, + "id": self.id, "style": int(self.style), "label": self.label, "disabled": self.disabled, @@ -409,7 +412,7 @@ class SelectMenu(Component): def __init__(self, data: SelectMenuPayload): self.type = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.custom_id: str = data["custom_id"] self.placeholder: str | None = data.get("placeholder") self.min_values: int = data.get("min_values", 1) @@ -425,6 +428,7 @@ def __init__(self, data: SelectMenuPayload): def to_dict(self) -> SelectMenuPayload: payload: SelectMenuPayload = { "type": self.type.value, + "id": self.id, "custom_id": self.custom_id, "min_values": self.min_values, "max_values": self.max_values, @@ -584,7 +588,7 @@ class Section(Component): def __init__(self, data: SectionComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.components: list[Component] = [ _component_factory(d, state=state) for d in data.get("components", []) ] @@ -625,7 +629,7 @@ class TextDisplay(Component): def __init__(self, data: TextDisplayComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.content: str = data.get("content") def to_dict(self) -> TextDisplayComponentPayload: @@ -702,7 +706,7 @@ class Thumbnail(Component): def __init__(self, data: ThumbnailComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.media: UnfurledMediaItem = ( umi := data.get("media") ) and UnfurledMediaItem.from_dict(umi, state=state) @@ -784,7 +788,7 @@ class MediaGallery(Component): def __init__(self, data: MediaGalleryComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.items: list[MediaGalleryItem] = [ MediaGalleryItem.from_dict(d, state=state) for d in data.get("items", []) ] @@ -824,7 +828,7 @@ class FileComponent(Component): def __init__(self, data: FileComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.file: UnfurledMediaItem = ( umi := data.get("file") ) and UnfurledMediaItem.from_dict(umi, state=state) @@ -867,6 +871,7 @@ class Separator(Component): def __init__(self, data: SeparatorComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = None self.divider: bool = data.get("divider") self.spacing: SeparatorSpacingSize = try_enum( SeparatorSpacingSize, data.get("spacing", 1) @@ -908,7 +913,7 @@ class Container(Component): def __init__(self, data: ContainerComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: str = data.get("id") + self.id: int = data.get("id") self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour( c ) # at this point, not adding alternative spelling diff --git a/discord/types/components.py b/discord/types/components.py index 67af33f48f..380fb59451 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -99,8 +99,8 @@ class TextDisplayComponent(BaseComponent): class SectionComponent(BaseComponent): type: Literal[9] - components: list[TextDisplayComponent, ButtonComponent] - accessory: NotRequired[Component] + components: list[TextDisplayComponent] + accessory: NotRequired[ThumbnailComponent, ButtonComponent] class UnfurledMediaItem(TypedDict): diff --git a/discord/ui/button.py b/discord/ui/button.py index 42d4af8d08..2bf9865613 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -98,6 +98,7 @@ def __init__( emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, sku_id: int | None = None, row: int | None = None, + id: int | None = None, ): super().__init__() if label and len(str(label)) > 80: @@ -145,6 +146,7 @@ def __init__( style=style, emoji=emoji, sku_id=sku_id, + id=id, ) self.row = row @@ -248,6 +250,7 @@ def from_component(cls: type[B], button: ButtonComponent) -> B: emoji=button.emoji, sku_id=button.sku_id, row=None, + id=button.id, ) @property @@ -277,6 +280,7 @@ def button( style: ButtonStyle = ButtonStyle.secondary, emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, row: int | None = None, + id: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A decorator that attaches a button to a component. @@ -326,6 +330,7 @@ def decorator(func: ItemCallbackType) -> ItemCallbackType: "label": label, "emoji": emoji, "row": row, + "id": id, } return func diff --git a/discord/ui/container.py b/discord/ui/container.py index 1cd7484426..34f2fee4fa 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -67,19 +67,20 @@ def __init__( colour: int | Colour | None = None, color: int | Colour | None = None, spoiler: bool = False, + id: int | None = None, ): super().__init__() self.items = [] - self._color = colour self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, - id=None, + id=id, components=[], - accent_color=colour, + accent_color=None, spoiler=spoiler, ) + self.color = colour or color for func in self.__container_children_items__: item: Item = func.__discord_ui_model_type__( @@ -132,7 +133,7 @@ def add_item(self, item: Item) -> None: row = ActionRow.with_components(item._underlying) self._underlying.components.append(row) - def add_section(self, *items: Item, accessory: Item): + def add_section(self, *items: Item, accessory: Item, id: int | None = None,): """Adds a :class:`Section` to the container. To append a pre-existing :class:`Section` use the @@ -146,14 +147,16 @@ def add_section(self, *items: Item, accessory: Item): 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`. + id: Optional[:class:`int`] + The section's ID. """ # accept raw strings? - section = Section(*items, accessory=accessory) + section = Section(*items, accessory=accessory, id=id) self.add_item(section) - def add_text(self, content: str) -> None: + def add_text(self, content: str, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the container. Parameters @@ -162,13 +165,14 @@ def add_text(self, content: str) -> None: The content of the TextDisplay """ - text = TextDisplay(content) + text = TextDisplay(content, id=id) self.add_item(text) def add_gallery( self, *items: Item, + id: int | None = None, ): """Adds a :class:`MediaGallery` to the container. @@ -179,14 +183,16 @@ def add_gallery( ---------- *items: List[:class:`MediaGalleryItem`] The media this gallery contains. + id: Optiona[:class:`int`] + The gallery's ID. """ # accept raw urls? - g = MediaGallery(*items) + g = MediaGallery(*items, id=id) self.add_item(g) - def add_file(self, url: str, spoiler: bool = False) -> None: + def add_file(self, url: str, spoiler: bool = False, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the container. Parameters @@ -195,9 +201,11 @@ def add_file(self, url: str, spoiler: bool = False) -> None: The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`. spoiler: Optional[:class:`bool`] Whether the file is a spoiler. Defaults to ``False``. + id: Optiona[:class:`int`] + The file's ID. """ - f = File(url, spoiler=spoiler) + f = File(url, spoiler=spoiler, id=id) self.add_item(f) @@ -206,6 +214,7 @@ def add_separator( *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small, + id: int | None = None ) -> None: """Adds a :class:`Separator` to the container. @@ -217,36 +226,34 @@ def add_separator( The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`. """ - s = Separator(divider=divider, spacing=spacing) + s = Separator(divider=divider, spacing=spacing, id=id) self.add_item(s) @property def spoiler(self) -> bool: """Whether the container is a spoiler. Defaults to ``False``.""" - return self._spoiler + return self._underlying.spoiler @spoiler.setter def spoiler(self, spoiler: bool) -> None: - self._spoiler = spoiler self._underlying.spoiler = spoiler @property def colour(self) -> Colour | None: - return getattr(self, "_colour", None) + return self._underlying.accent_color @colour.setter def colour(self, value: int | Colour | None): # type: ignore if value is None or isinstance(value, Colour): - self._colour = value + self._underlying.accent_color = value elif isinstance(value, int): - self._colour = Colour(value=value) + self._underlying.accent_color = Colour(value=value) else: raise TypeError( "Expected discord.Colour, int, or None but received" f" {value.__class__.__name__} instead." ) - self._underlying.accent_color = self.colour color = colour @@ -278,6 +285,6 @@ def from_component(cls: type[C], component: ContainerComponent) -> C: items = [ _component_to_item(c) for c in _walk_all_components(component.components) ] - return cls(*items, colour=component.accent_color, spoiler=component.spoiler) + return cls(*items, colour=component.accent_color, spoiler=component.spoiler, id=component.id) callback = None diff --git a/discord/ui/file.py b/discord/ui/file.py index 5c109c1017..9a941df4f4 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -27,16 +27,14 @@ class File(Item[V]): ---------- """ - def __init__(self, url: str, *, spoiler: bool = False): + def __init__(self, url: str, *, spoiler: bool = False, id: int | None = None): super().__init__() file = UnfurledMediaItem(url) - self._url = url - self._spoiler: bool = spoiler self._underlying = FileComponent._raw_construct( type=ComponentType.file, - id=None, + id=id, file=file, spoiler=spoiler, ) @@ -52,21 +50,19 @@ def width(self) -> int: @property def url(self) -> str: """The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`.""" - return self._url + return self._underlying.file and self._underlying.file.url @url.setter def url(self, value: str) -> None: - self._url = value self._underlying.file.url = value @property def spoiler(self) -> bool: """Whether the file is a spoiler. Defaults to ``False``.""" - return self._spoiler + return self._underlying.spoiler @spoiler.setter def spoiler(self, spoiler: bool) -> None: - self._spoiler = spoiler self._underlying.spoiler = spoiler def to_component_dict(self) -> FileComponentPayload: @@ -80,6 +76,7 @@ def from_component(cls: type[F], component: FileComponent) -> F: return cls( url, spoiler=component.spoiler, + id=component.id, ) callback = None diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index dd7438be21..8f0d4b5368 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -60,6 +60,7 @@ def __init__( required: bool | None = True, value: str | None = None, row: int | None = None, + id: int | None = None, ): super().__init__() if len(str(label)) > 45: @@ -88,6 +89,7 @@ def __init__( max_length=max_length, required=required, value=value, + id=id, ) self._input_value = False self.row = row diff --git a/discord/ui/item.py b/discord/ui/item.py index fd8dc747ad..5cdb72d939 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -58,6 +58,7 @@ def __init__(self): self._view: V | None = None self._row: int | None = None self._rendered_row: int | None = None + self._underlying: Component | None = None # This works mostly well but there is a gotcha with # the interaction with from_component, since that technically provides # a custom_id most dispatchable items would get this set to True even though @@ -137,6 +138,25 @@ def width(self) -> int: """ return 1 + @property + def id(self) -> int | None: + """Gets this item's ID. + + This can be set by the user when constructing an Item. If not, Discord will automatically provide one when the View is sent. + + Returns + ------- + Optional[:class:`int`] + The ID of this item, or ``None`` if the user didn't set one. + """ + return self._underlying and self._underlying.id + + @id.setter + def id(self, value) -> None: + if not self._underlying: + return + self._underlying.id = value + @property def view(self) -> V | None: """Gets the parent view associated with this item. diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index d02d1a1d31..fe0c9e0081 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -29,11 +29,11 @@ class MediaGallery(Item[V]): The initial items contained in this gallery, up to 10. """ - def __init__(self, *items: MediaGalleryItem): + def __init__(self, *items: MediaGalleryItem, id: int | None = None): super().__init__() self._underlying = MediaGalleryComponent._raw_construct( - type=ComponentType.media_gallery, id=None, items=[i for i in items] + type=ComponentType.media_gallery, id=id, items=[i for i in items] ) @property @@ -65,7 +65,7 @@ def append_item(self, item: MediaGalleryItem) -> None: self._underlying.items.append(item) def add_item( - self, url: str, *, description: str = None, spoiler: bool = False + self, url: str, *, description: str = None, spoiler: bool = False, ) -> None: """Adds a new media item to the gallery. @@ -104,6 +104,6 @@ def to_component_dict(self) -> MediaGalleryComponentPayload: @classmethod def from_component(cls: type[M], component: MediaGalleryComponent) -> M: - return cls(*component.items) + return cls(*component.items, id=component.id) callback = None diff --git a/discord/ui/section.py b/discord/ui/section.py index 63ca03e669..ec0d59366e 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -46,7 +46,7 @@ def __init_subclass__(cls) -> None: cls.__section_accessory_item__ = accessory - def __init__(self, *items: Item, accessory: Item = None): + def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): super().__init__() self.items = [] @@ -55,7 +55,7 @@ def __init__(self, *items: Item, accessory: Item = None): self._underlying = SectionComponent._raw_construct( type=ComponentType.section, - id=None, + id=id, components=[], accessory=None, ) @@ -99,13 +99,15 @@ def add_item(self, item: Item) -> None: self.items.append(item) self._underlying.components.append(item._underlying) - def add_text(self, content: str) -> None: + def add_text(self, content: str, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the section. Parameters ---------- content: :class:`str` - The content of the TextDisplay + The content of the text display. + id: Optiona[:class:`int`] + The text display's ID. Raises ------ @@ -118,7 +120,7 @@ def add_text(self, content: str) -> None: if len(self.items) >= 3: raise ValueError("maximum number of children exceeded") - text = TextDisplay(content) + text = TextDisplay(content, id=id) self.add_item(text) @@ -172,6 +174,6 @@ def from_component(cls: type[S], component: SectionComponent) -> S: items = [_component_to_item(c) for c in component.components] accessory = _component_to_item(component.accessory) - return cls(*items, accessory=accessory) + return cls(*items, accessory=accessory, id=component.id) callback = None diff --git a/discord/ui/select.py b/discord/ui/select.py index 496446e61c..788cc7002c 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -134,6 +134,7 @@ def __init__( channel_types: list[ChannelType] = None, disabled: bool = False, row: int | None = None, + id: int | None = None, ) -> None: if options and select_type is not ComponentType.string_select: raise InvalidArgument("options parameter is only valid for string selects") @@ -166,6 +167,7 @@ def __init__( disabled=disabled, options=options or [], channel_types=channel_types or [], + id=id, ) self.row = row @@ -427,6 +429,7 @@ def from_component(cls: type[S], component: SelectMenu) -> S: channel_types=component.channel_types, disabled=component.disabled, row=None, + id=component.id, ) @property @@ -457,6 +460,7 @@ def select( channel_types: list[ChannelType] = MISSING, disabled: bool = False, row: int | None = None, + id: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A decorator that attaches a select menu to a component. @@ -531,6 +535,7 @@ def decorator(func: ItemCallbackType) -> ItemCallbackType: "min_values": min_values, "max_values": max_values, "disabled": disabled, + "id": id, } if options: model_kwargs["options"] = options @@ -554,6 +559,7 @@ def string_select( options: list[SelectOption] = MISSING, disabled: bool = False, row: int | None = None, + id: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.string_select`. @@ -568,6 +574,7 @@ def string_select( options=options, disabled=disabled, row=row, + id=id, ) @@ -579,6 +586,7 @@ def user_select( max_values: int = 1, disabled: bool = False, row: int | None = None, + id: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.user_select`. @@ -592,6 +600,7 @@ def user_select( max_values=max_values, disabled=disabled, row=row, + id=id, ) @@ -603,6 +612,7 @@ def role_select( max_values: int = 1, disabled: bool = False, row: int | None = None, + id: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.role_select`. @@ -616,6 +626,7 @@ def role_select( max_values=max_values, disabled=disabled, row=row, + id=id, ) @@ -627,6 +638,7 @@ def mentionable_select( max_values: int = 1, disabled: bool = False, row: int | None = None, + id: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.mentionable_select`. @@ -640,6 +652,7 @@ def mentionable_select( max_values=max_values, disabled=disabled, row=row, + id=id, ) @@ -652,6 +665,7 @@ def channel_select( disabled: bool = False, channel_types: list[ChannelType] = MISSING, row: int | None = None, + id: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A shortcut for :meth:`discord.ui.select` with select type :attr:`discord.ComponentType.channel_select`. @@ -666,4 +680,5 @@ def channel_select( disabled=disabled, channel_types=channel_types, row=row, + id=id ) diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 694b599205..21cd8ebd86 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -25,10 +25,6 @@ class Separator(Item[V]): Parameters ---------- - divider: :class:`bool` - Whether the separator is a divider. Defaults to ``True``. - spacing: :class:`~discord.SeparatorSpacingSize` - The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`. """ def __init__( @@ -36,15 +32,13 @@ def __init__( *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small, + id: int | None = None, ): super().__init__() - self.divider = divider - self.spacing = spacing - self._underlying = SeparatorComponent._raw_construct( type=ComponentType.separator, - id=None, + id=id, divider=divider, spacing=spacing, ) @@ -53,6 +47,24 @@ def __init__( def type(self) -> ComponentType: return self._underlying.type + @property + def divider(self) -> bool: + """Whether the separator is a divider. Defaults to ``True``.""" + return self._underlying.divider + + @divider.setter + def divider(self, value: bool) -> None: + self._underlying.divider = value + + @property + def spacing(self) -> SeparatorSpacingSize: + """The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`.""" + return self._underlying.spacing + + @spacing.setter + def spacing(self, value: SeparatorSpacingSize) -> None: + self._underlying.spacing = value + @property def width(self) -> int: return 5 @@ -62,6 +74,6 @@ def to_component_dict(self) -> SeparatorComponentPayload: @classmethod def from_component(cls: type[S], component: SeparatorComponent) -> S: - return cls(divider=component.divider, spacing=component.spacing) + return cls(divider=component.divider, spacing=component.spacing, id=component.id) callback = None diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 4ad9d11e7c..2b13877d94 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -29,14 +29,12 @@ class TextDisplay(Item[V]): The text display's content. """ - def __init__(self, content: str): + def __init__(self, content: str, id: int | None = None,): super().__init__() - self.content = content - self._underlying = TextDisplayComponent._raw_construct( type=ComponentType.text_display, - id=None, + id=id, content=content, ) @@ -44,20 +42,24 @@ def __init__(self, content: str): def type(self) -> ComponentType: return self._underlying.type + @property + def content(self) -> str: + """The text display's content.""" + return self._underlying.content + + @content.setter + def content(self, value: str) -> None: + self._underlying.content = value + @property def width(self) -> int: return 5 - def set_text(self, content): - """Update this component's content.""" - self.content = content - self._underlying.content = content - def to_component_dict(self) -> TextDisplayComponentPayload: return self._underlying.to_dict() @classmethod def from_component(cls: type[T], component: TextDisplayComponent) -> T: - return cls(component.content) + return cls(component.content, id=component.id) callback = None diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 5e21eade9a..158d6e9619 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -27,17 +27,14 @@ class Thumbnail(Item[V]): ---------- """ - def __init__(self, url: str, *, description: str = None, spoiler: bool = False): + def __init__(self, url: str, *, description: str = None, spoiler: bool = False, id: int = None): super().__init__() media = UnfurledMediaItem(url) - self._url = url - self._description: str | None = description - self._spoiler: bool = spoiler self._underlying = ThumbnailComponent._raw_construct( type=ComponentType.thumbnail, - id=None, + id=id, media=media, description=description, spoiler=spoiler, @@ -54,31 +51,28 @@ def width(self) -> int: @property def url(self) -> str: """The URL of this thumbnail's media. This can either be an arbitrary URL or an ``attachment://`` URL.""" - return self._url + return self._underlying.media and self._underlying.media.url @url.setter def url(self, value: str) -> None: - self._url = value self._underlying.media.url = value @property def description(self) -> str | None: """The thumbnail's description, up to 1024 characters.""" - return self._description + return self._underlying.description @description.setter def description(self, description: str | None) -> None: - self._description = description self._underlying.description = description @property def spoiler(self) -> bool: """Whether the thumbnail is a spoiler. Defaults to ``False``.""" - return self._spoiler + return self._underlying.spoiler @spoiler.setter def spoiler(self, spoiler: bool) -> None: - self._spoiler = spoiler self._underlying.spoiler = spoiler def to_component_dict(self) -> ThumbnailComponentPayload: @@ -90,6 +84,7 @@ def from_component(cls: type[T], component: ThumbnailComponent) -> T: component.media and component.media.url, description=component.description, spoiler=component.spoiler, + id=component.id, ) callback = None From 5744198048835da3d661ff97376f3b676f2fcae0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 16:52:31 +0000 Subject: [PATCH 101/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 16 +++++++++++++--- discord/ui/item.py | 2 +- discord/ui/media_gallery.py | 6 +++++- discord/ui/select.py | 2 +- discord/ui/separator.py | 4 +++- discord/ui/text_display.py | 6 +++++- discord/ui/thumbnail.py | 9 ++++++++- 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 34f2fee4fa..04878971dc 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -133,7 +133,12 @@ def add_item(self, item: Item) -> None: row = ActionRow.with_components(item._underlying) self._underlying.components.append(row) - def add_section(self, *items: Item, accessory: Item, id: int | None = None,): + def add_section( + self, + *items: Item, + accessory: Item, + id: int | None = None, + ): """Adds a :class:`Section` to the container. To append a pre-existing :class:`Section` use the @@ -214,7 +219,7 @@ def add_separator( *, divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small, - id: int | None = None + id: int | None = None, ) -> None: """Adds a :class:`Separator` to the container. @@ -285,6 +290,11 @@ def from_component(cls: type[C], component: ContainerComponent) -> C: items = [ _component_to_item(c) for c in _walk_all_components(component.components) ] - return cls(*items, colour=component.accent_color, spoiler=component.spoiler, id=component.id) + return cls( + *items, + colour=component.accent_color, + spoiler=component.spoiler, + id=component.id, + ) callback = None diff --git a/discord/ui/item.py b/discord/ui/item.py index 5cdb72d939..39490c243c 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -150,7 +150,7 @@ def id(self) -> int | None: The ID of this item, or ``None`` if the user didn't set one. """ return self._underlying and self._underlying.id - + @id.setter def id(self, value) -> None: if not self._underlying: diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index fe0c9e0081..b92ed67d6f 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -65,7 +65,11 @@ def append_item(self, item: MediaGalleryItem) -> None: self._underlying.items.append(item) def add_item( - self, url: str, *, description: str = None, spoiler: bool = False, + self, + url: str, + *, + description: str = None, + spoiler: bool = False, ) -> None: """Adds a new media item to the gallery. diff --git a/discord/ui/select.py b/discord/ui/select.py index 788cc7002c..8388ac76ab 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -680,5 +680,5 @@ def channel_select( disabled=disabled, channel_types=channel_types, row=row, - id=id + id=id, ) diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 21cd8ebd86..c0a9a771e3 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -74,6 +74,8 @@ def to_component_dict(self) -> SeparatorComponentPayload: @classmethod def from_component(cls: type[S], component: SeparatorComponent) -> S: - return cls(divider=component.divider, spacing=component.spacing, id=component.id) + return cls( + divider=component.divider, spacing=component.spacing, id=component.id + ) callback = None diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 2b13877d94..29c46a22e7 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -29,7 +29,11 @@ class TextDisplay(Item[V]): The text display's content. """ - def __init__(self, content: str, id: int | None = None,): + def __init__( + self, + content: str, + id: int | None = None, + ): super().__init__() self._underlying = TextDisplayComponent._raw_construct( diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 158d6e9619..d8214fc658 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -27,7 +27,14 @@ class Thumbnail(Item[V]): ---------- """ - def __init__(self, url: str, *, description: str = None, spoiler: bool = False, id: int = None): + def __init__( + self, + url: str, + *, + description: str = None, + spoiler: bool = False, + id: int = None, + ): super().__init__() media = UnfurledMediaItem(url) From 2ec12560e5c1936e1a6c3a84c19ac6f6fb40f334 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 1 Mar 2025 17:00:20 +0000 Subject: [PATCH 102/154] doc --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 04878971dc..91f1ed2f11 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -186,7 +186,7 @@ def add_gallery( Parameters ---------- - *items: List[:class:`MediaGalleryItem`] + *items: :class:`MediaGalleryItem` The media this gallery contains. id: Optiona[:class:`int`] The gallery's ID. From 2ef567fd6184ccfc62cd91ab01f78a1f7562cb69 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:21:26 +0100 Subject: [PATCH 103/154] adjust container item strategy --- discord/ui/button.py | 1 + discord/ui/container.py | 50 ++++++++++++++++++++++++++++++----------- discord/ui/select.py | 1 + discord/ui/view.py | 9 +++++--- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/discord/ui/button.py b/discord/ui/button.py index 2bf9865613..d4ff5e47d4 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -174,6 +174,7 @@ def custom_id(self, value: str | None): if value and len(value) > 100: raise ValueError("custom_id must be 100 characters or fewer") self._underlying.custom_id = value + self._provided_custom_id = value is not None @property def url(self) -> str | None: diff --git a/discord/ui/container.py b/discord/ui/container.py index 91f1ed2f11..54af112ad2 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -94,6 +94,25 @@ def __init__( self.add_item(item) for i in items: self.add_item(i) + + def _add_component_from_item(self, item: Item): + if item._underlying.is_v2(): + self._underlying.components.append(item._underlying) + else: + for row in reversed(self._underlying.components): + if ( + isinstance(row, ActionRow) and row.width + item.width <= 5 + ): # If a valid ActionRow exists + row.children.append(item._underlying) + break + else: + row = ActionRow.with_components(item._underlying) + self._underlying.components.append(row) + + def _set_components(self, items: list[Item]): + self._underlying.components.clear() + for item in items: + self._add_component_from_item(item) def add_item(self, item: Item) -> None: """Adds an item to the container. @@ -117,21 +136,25 @@ def add_item(self, item: Item) -> None: item._view = self.view self.items.append(item) + self._add_component_from_item(item) - # reuse weight system? + def get_item(self, id: str | int) -> Item | None: + """Get a top-level item from this container. Alias for `utils.get(container.items, ...)`. + If an ``int`` is provided it will retrieve by ``id``, otherwise it will check ``custom_id``. - if item._underlying.is_v2(): - self._underlying.components.append(item._underlying) - else: - for row in reversed(self._underlying.components): - if ( - isinstance(row, ActionRow) and row.width + item.width <= 5 - ): # If a valid ActionRow exists - row.children.append(item._underlying) - break - else: - row = ActionRow.with_components(item._underlying) - self._underlying.components.append(row) + Parameters + ---------- + id: :class:`str` + The id or custom_id of the item to get + + Returns + ------- + Optional[:class:`Item`] + The item with the matching ``id`` or ``custom_id`` if it exists. + """ + if isinstance(id, int): + return get(self.items, id=id) + return get(self.items, custom_id=id) def add_section( self, @@ -281,6 +304,7 @@ def width(self) -> int: return 5 def to_component_dict(self) -> ContainerComponentPayload: + self._set_components(self.items) return self._underlying.to_dict() @classmethod diff --git a/discord/ui/select.py b/discord/ui/select.py index 631c9191e4..982b210693 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -183,6 +183,7 @@ def custom_id(self, value: str): if len(value) > 100: raise ValueError("custom_id must be 100 characters or fewer") self._underlying.custom_id = value + self._provided_custom_id = value is not None @property def placeholder(self) -> str | None: diff --git a/discord/ui/view.py b/discord/ui/view.py index 202bee47bc..a8aeacef38 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -366,8 +366,9 @@ def clear_items(self) -> None: self.children.clear() self.__weights.clear() - def get_item(self, custom_id: str) -> Item | None: - """Get an item from the view with the given custom ID. Alias for `utils.get(view.children, custom_id=custom_id)`. + def get_item(self, custom_id: str | int) -> Item | None: + """Get an item from the view. Alias for `utils.get(view.children, ...)`. + If an ``int`` is provided it will retrieve by ``id``, otherwise it will check ``custom_id``. Parameters ---------- @@ -377,8 +378,10 @@ def get_item(self, custom_id: str) -> Item | None: Returns ------- Optional[:class:`Item`] - The item with the matching ``custom_id`` if it exists. + The item with the matching ``custom_id`` or ``id`` if it exists. """ + if isinstance(custom_id, int): + return get(self.children, id=custom_id) return get(self.children, custom_id=custom_id) async def interaction_check(self, interaction: Interaction) -> bool: From 0c97a21b5a01f70b6b744f6f3beba1aa09ea91c5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:21:58 +0000 Subject: [PATCH 104/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 54af112ad2..192da0f658 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -94,7 +94,7 @@ def __init__( self.add_item(item) for i in items: self.add_item(i) - + def _add_component_from_item(self, item: Item): if item._underlying.is_v2(): self._underlying.components.append(item._underlying) From 16f4c357a15ea77c24f0952b68df897ff5817386 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:24:20 +0100 Subject: [PATCH 105/154] import get --- discord/ui/container.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/container.py b/discord/ui/container.py index 192da0f658..9d39d20556 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -14,6 +14,7 @@ from .section import Section from .separator import Separator from .text_display import TextDisplay +from ..utils import get from .view import _walk_all_components __all__ = ("Container",) From 80499bb2583317413d958bc4a13e5e8e7ea96428 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:24:48 +0000 Subject: [PATCH 106/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 9d39d20556..09db5d767e 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -8,13 +8,13 @@ from ..components import Container as ContainerComponent from ..components import _component_factory from ..enums import ComponentType, SeparatorSpacingSize +from ..utils import get from .file import File from .item import Item, ItemCallbackType from .media_gallery import MediaGallery from .section import Section from .separator import Separator from .text_display import TextDisplay -from ..utils import get from .view import _walk_all_components __all__ = ("Container",) From 68572991e890c42ed38ae1dfa3472f7628561629 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 18:01:56 +0100 Subject: [PATCH 107/154] extend to section --- discord/ui/section.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index ec0d59366e..1a3ca84368 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -50,7 +50,6 @@ def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): super().__init__() self.items = [] - [i._underlying for i in items] self.accessory = None self._underlying = SectionComponent._raw_construct( @@ -74,6 +73,14 @@ def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): 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) + def add_item(self, item: Item) -> None: """Adds an item to the section. @@ -97,7 +104,7 @@ def add_item(self, item: Item) -> None: raise TypeError(f"expected Item not {item.__class__!r}") self.items.append(item) - self._underlying.components.append(item._underlying) + self._add_component_from_item(item) def add_text(self, content: str, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the section. @@ -166,6 +173,8 @@ def width(self) -> int: return 5 def to_component_dict(self) -> SectionComponentPayload: + self._set_components(self.items) + self.set_accessory(self.accessory) return self._underlying.to_dict() @classmethod From 15248c5cd45515d460f78a5f9615653151c809d3 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:23:40 +0100 Subject: [PATCH 108/154] implement copy_text --- discord/ui/container.py | 4 ++++ discord/ui/item.py | 3 +++ discord/ui/section.py | 19 +++++++++++++++++++ discord/ui/text_display.py | 4 ++++ discord/ui/view.py | 4 ++++ 5 files changed, 34 insertions(+) diff --git a/discord/ui/container.py b/discord/ui/container.py index 09db5d767e..4e6ff926d2 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -259,6 +259,10 @@ def add_separator( self.add_item(s) + def copy_text(self) -> str: + """Returns the text of all :class:`~discord.ui.TextDisplay` items in this container. Equivalent to the `Copy Text` option on Discord clients.""" + return "\n".join([i.copy_text() for i in self.items]) + @property def spoiler(self) -> bool: """Whether the container is a spoiler. Defaults to ``False``.""" diff --git a/discord/ui/item.py b/discord/ui/item.py index 39490c243c..662e6d1eb5 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -90,6 +90,9 @@ def is_dispatchable(self) -> bool: def is_persistent(self) -> bool: return self._provided_custom_id + def copy_text(self) -> str: + return "" + def __repr__(self) -> str: attrs = " ".join( f"{key}={getattr(self, key)!r}" for key in self.__item_repr_attributes__ diff --git a/discord/ui/section.py b/discord/ui/section.py index 1a3ca84368..8695f435eb 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -106,6 +106,21 @@ def add_item(self, item: Item) -> None: self.items.append(item) self._add_component_from_item(item) + def get_item(self, id: int) -> Item | None: + """Get an item from this section. Alias for `utils.get(section.items, id=id)`. + + Parameters + ---------- + id: :class:`int` + The id of the item to get + + Returns + ------- + Optional[:class:`Item`] + The item with the matching ``id`` if it exists. + """ + return get(self.items, id=id) + def add_text(self, content: str, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the section. @@ -164,6 +179,10 @@ def view(self, value): delattr(self.accessory, "_tmp_func") self.accessory._view = value + 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(i.text for i in self.items) + @property def type(self) -> ComponentType: return self._underlying.type diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 29c46a22e7..4ca9b0e18b 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -62,6 +62,10 @@ def width(self) -> int: def to_component_dict(self) -> TextDisplayComponentPayload: return self._underlying.to_dict() + def copy_text(self) -> str: + """Returns the content of this TextDisplay. Equivalent to the `Copy Text` option on Discord clients.""" + return self.content + @classmethod def from_component(cls: type[T], component: TextDisplayComponent) -> T: return cls(component.content, id=component.id) diff --git a/discord/ui/view.py b/discord/ui/view.py index a8aeacef38..d70dc3d44e 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -618,6 +618,10 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: if exclusions is None or child not in exclusions: child.disabled = False + def copy_text(self) -> str: + """Returns the text of all :class:`~discord.ui.TextDisplay` items in this View. Equivalent to the `Copy Text` option on Discord clients.""" + return "\n".join([i.copy_text() for i in self.children]) + @property def message(self): return self._message From e7c2399386adcccbd7347ce90398674cbfa14bc9 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:24:53 +0100 Subject: [PATCH 109/154] get --- discord/ui/section.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/section.py b/discord/ui/section.py index 8695f435eb..81039861ac 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -6,6 +6,7 @@ from ..components import Section as SectionComponent from ..components import _component_factory from ..enums import ComponentType +from ..utils import get from .item import Item, ItemCallbackType from .text_display import TextDisplay From b63e224d772c11e7ca8c6170d7d33ef33c96af26 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:28:15 +0100 Subject: [PATCH 110/154] minor fix --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 81039861ac..833e8151d5 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -182,7 +182,7 @@ def view(self, value): 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(i.text for i in self.items) + return "\n".join(i.copy_text() for i in self.items) @property def type(self) -> ComponentType: From f21a4b87a763fcf1da53ad66ef7557b2abbd9dc0 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 20:45:35 +0100 Subject: [PATCH 111/154] get_item works on nested items --- discord/ui/view.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index d70dc3d44e..ea7b794c4c 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -381,7 +381,13 @@ def get_item(self, custom_id: str | int) -> Item | None: The item with the matching ``custom_id`` or ``id`` if it exists. """ if isinstance(custom_id, int): - return get(self.children, id=custom_id) + child = get(self.children, id=custom_id) + if not child: + for i in self.children: + if hasattr(i, "get_item"): + if (child := i.get_item(custom_id)): + return child + return child return get(self.children, custom_id=custom_id) async def interaction_check(self, interaction: Interaction) -> bool: From 97962f03aa04e59ef8795a585153aa83759cedb4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:46:01 +0000 Subject: [PATCH 112/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index ea7b794c4c..747cfff5fa 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -385,7 +385,7 @@ def get_item(self, custom_id: str | int) -> Item | None: if not child: for i in self.children: if hasattr(i, "get_item"): - if (child := i.get_item(custom_id)): + if child := i.get_item(custom_id): return child return child return get(self.children, custom_id=custom_id) From 567858f941107f831d3ea31c2d350eb60be00a4d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 21:36:28 +0100 Subject: [PATCH 113/154] recur in container --- discord/ui/container.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 4e6ff926d2..ce6fc31168 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -154,7 +154,13 @@ def get_item(self, id: str | int) -> Item | None: The item with the matching ``id`` or ``custom_id`` if it exists. """ if isinstance(id, int): - return get(self.items, id=id) + child = get(self.items, id=id) + if not child: + for i in self.items: + if hasattr(i, "get_item"): + if child := i.get_item(custom_id): + return child + return child return get(self.items, custom_id=id) def add_section( From 492f70bff740169adbdfb4cf5e517f6e601cafc0 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 18 Apr 2025 21:41:25 +0100 Subject: [PATCH 114/154] id --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index ce6fc31168..a605598069 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -158,7 +158,7 @@ def get_item(self, id: str | int) -> Item | None: if not child: for i in self.items: if hasattr(i, "get_item"): - if child := i.get_item(custom_id): + if child := i.get_item(id): return child return child return get(self.items, custom_id=id) From 338f8cf950c4149245f338e92f0d019206a65e2e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 19 Apr 2025 15:06:06 +0100 Subject: [PATCH 115/154] media view setter --- discord/ui/media_gallery.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index b92ed67d6f..e1284801ce 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -95,6 +95,10 @@ def add_item( self.append_item(item) + @Item.view.setter + def view(self, value): + self._view = value + @property def type(self) -> ComponentType: return self._underlying.type From c7c911e51dcce63193cf092985c8052a8e1e81cd Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 19 Apr 2025 15:09:14 +0100 Subject: [PATCH 116/154] dispatch --- discord/components.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/components.py b/discord/components.py index 1af3ff0b79..0df0d0ccc8 100644 --- a/discord/components.py +++ b/discord/components.py @@ -739,6 +739,9 @@ def __init__(self, url, *, description=None, spoiler=False): def url(self) -> str: """Returns the underlying URL of this gallery item.""" return self.media.url + + def is_dispatchable(self) -> bool: + return False @classmethod def from_dict(cls, data: MediaGalleryItemPayload, state=None) -> MediaGalleryItem: From 6dd7a2ca47e4a89afeb0cea7874246a23d706368 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 19 Apr 2025 14:10:00 +0000 Subject: [PATCH 117/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 0df0d0ccc8..87a301d120 100644 --- a/discord/components.py +++ b/discord/components.py @@ -739,7 +739,7 @@ def __init__(self, url, *, description=None, spoiler=False): def url(self) -> str: """Returns the underlying URL of this gallery item.""" return self.media.url - + def is_dispatchable(self) -> bool: return False From 9ba1f84e2b62a4e5703c755f970ac73459a81720 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 19 Apr 2025 20:34:50 +0100 Subject: [PATCH 118/154] maybe fixes --- discord/ui/container.py | 12 ++++++++---- discord/ui/section.py | 10 ++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index a605598069..44acbba1b8 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -87,11 +87,11 @@ def __init__( item: Item = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ ) - if self.view: - item.callback = partial(func, self.view, item) - setattr(self.view, func.__name__, item) - else: + item.callback = partial(func, self.view, item) + if not self.view: item._tmp_func = func + else: + setattr(self.view, func.__name__, item) self.add_item(item) for i in items: self.add_item(i) @@ -135,6 +135,8 @@ def add_item(self, item: Item) -> None: raise TypeError(f"expected Item not {item.__class__!r}") item._view = self.view + if hasattr(item, "items"): + item.view = self self.items.append(item) self._add_component_from_item(item) @@ -305,6 +307,8 @@ def view(self, value): setattr(self.view, item._tmp_func.__name__, item) delattr(item, "_tmp_func") item._view = value + if hasattr(item, "items"): + item.view = value @property def type(self) -> ComponentType: diff --git a/discord/ui/section.py b/discord/ui/section.py index 833e8151d5..850a556fdb 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -63,11 +63,11 @@ def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): item: Item = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ ) - if self.view: - item.callback = partial(func, self.view, item) - setattr(self.view, func.__name__, item) - else: + item.callback = partial(func, self.view, item) + if not self.view: item._tmp_func = func + else: + setattr(self.view, func.__name__, item) self.set_accessory(item) elif accessory: self.set_accessory(accessory) @@ -164,6 +164,8 @@ def set_accessory(self, item: Item) -> None: if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") + if self.view: + item._view = self.view self.accessory = item self._underlying.accessory = item._underlying From 09f4e27f10c256b9d92a85ddd71dbe4b9ba982eb Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:43:48 +0100 Subject: [PATCH 119/154] Update discord/ui/section.py Co-authored-by: Ice Wolfy <44532864+Icebluewolf@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/section.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 850a556fdb..99cf6f3170 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -50,8 +50,8 @@ def __init_subclass__(cls) -> None: def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): super().__init__() - self.items = [] - self.accessory = None + self.items: List[Item] = [] + self.accessory: Optional[Item] = None self._underlying = SectionComponent._raw_construct( type=ComponentType.section, From 5ab9dcd141d7ed159a061f2cad084d607dcd626f Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:43:59 +0100 Subject: [PATCH 120/154] Update discord/ui/container.py Co-authored-by: Ice Wolfy <44532864+Icebluewolf@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 44acbba1b8..baf99c88cc 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -72,7 +72,7 @@ def __init__( ): super().__init__() - self.items = [] + self.items: List[Item] = [] self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, From c7740b3d7cd88453d6d3e33ec700266546bbecec Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:44:40 +0100 Subject: [PATCH 121/154] Update discord/ui/section.py Co-authored-by: Ice Wolfy <44532864+Icebluewolf@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/section.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/ui/section.py b/discord/ui/section.py index 99cf6f3170..0b26f0b07b 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -120,8 +120,11 @@ def get_item(self, id: int) -> Item | None: Optional[:class:`Item`] The item with the matching ``id`` if it exists. """ + if self.accessory and self.accessory.id == id: + return self.accessory return get(self.items, id=id) + def add_text(self, content: str, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the section. From e87a2d1323e05b480dc1a9c53aef240623b2cd9f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 11:45:04 +0000 Subject: [PATCH 122/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 0b26f0b07b..54b7c2a3d9 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -121,10 +121,9 @@ def get_item(self, id: int) -> Item | None: The item with the matching ``id`` if it exists. """ if self.accessory and self.accessory.id == id: - return self.accessory + return self.accessory return get(self.items, id=id) - def add_text(self, content: str, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the section. From 96c5792e3d278b2da3ab60062e707be8dd341725 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:53:32 +0100 Subject: [PATCH 123/154] adjust get_item behavior --- discord/ui/container.py | 6 ++++-- discord/ui/section.py | 2 ++ discord/ui/view.py | 8 +++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index baf99c88cc..d886e6a01a 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -8,7 +8,7 @@ from ..components import Container as ContainerComponent from ..components import _component_factory from ..enums import ComponentType, SeparatorSpacingSize -from ..utils import get +from ..utils import get, find from .file import File from .item import Item, ItemCallbackType from .media_gallery import MediaGallery @@ -155,6 +155,8 @@ def get_item(self, id: str | int) -> Item | None: Optional[:class:`Item`] The item with the matching ``id`` or ``custom_id`` if it exists. """ + if not id: + return None if isinstance(id, int): child = get(self.items, id=id) if not child: @@ -163,7 +165,7 @@ def get_item(self, id: str | int) -> Item | None: if child := i.get_item(id): return child return child - return get(self.items, custom_id=id) + return find(lambda i: getattr(i, "custom_id", None) == id, self.items) def add_section( self, diff --git a/discord/ui/section.py b/discord/ui/section.py index 54b7c2a3d9..49f17d217e 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -120,6 +120,8 @@ def get_item(self, id: int) -> Item | None: Optional[:class:`Item`] The item with the matching ``id`` if it exists. """ + if not id: + return None if self.accessory and self.accessory.id == id: return self.accessory return get(self.items, id=id) diff --git a/discord/ui/view.py b/discord/ui/view.py index 747cfff5fa..d2002dbd11 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -46,7 +46,7 @@ from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent from ..components import _component_factory -from ..utils import get +from ..utils import get, find from .item import Item, ItemCallbackType __all__ = ("View", "_component_to_item", "_walk_all_components") @@ -367,7 +367,7 @@ def clear_items(self) -> None: self.__weights.clear() def get_item(self, custom_id: str | int) -> Item | None: - """Get an item from the view. Alias for `utils.get(view.children, ...)`. + """Get an item from the view. Roughly equal to `utils.get(view.children, ...)`. If an ``int`` is provided it will retrieve by ``id``, otherwise it will check ``custom_id``. Parameters @@ -380,6 +380,8 @@ def get_item(self, custom_id: str | int) -> Item | None: Optional[:class:`Item`] The item with the matching ``custom_id`` or ``id`` if it exists. """ + if not custom_id: + return None if isinstance(custom_id, int): child = get(self.children, id=custom_id) if not child: @@ -388,7 +390,7 @@ def get_item(self, custom_id: str | int) -> Item | None: if child := i.get_item(custom_id): return child return child - return get(self.children, custom_id=custom_id) + return find(lambda i: getattr(i, "custom_id", None) == custom_id, self.children) async def interaction_check(self, interaction: Interaction) -> bool: """|coro| From 1314b9e049fae53b7f9f080deeaab7a43fcd4403 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 11:53:56 +0000 Subject: [PATCH 124/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 2 +- discord/ui/view.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index d886e6a01a..fec5b1a327 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -8,7 +8,7 @@ from ..components import Container as ContainerComponent from ..components import _component_factory from ..enums import ComponentType, SeparatorSpacingSize -from ..utils import get, find +from ..utils import find, get from .file import File from .item import Item, ItemCallbackType from .media_gallery import MediaGallery diff --git a/discord/ui/view.py b/discord/ui/view.py index d2002dbd11..60ff913605 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -46,7 +46,7 @@ from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent from ..components import _component_factory -from ..utils import get, find +from ..utils import find, get from .item import Item, ItemCallbackType __all__ = ("View", "_component_to_item", "_walk_all_components") From 28bfe5d054c521ee5abf47129e1e5353b66576c2 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 13:00:47 +0100 Subject: [PATCH 125/154] typing adjustment --- discord/ui/container.py | 2 +- discord/ui/section.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index fec5b1a327..5163159c4f 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -72,7 +72,7 @@ def __init__( ): super().__init__() - self.items: List[Item] = [] + self.items: list[Item] = [] self._underlying = ContainerComponent._raw_construct( type=ComponentType.container, diff --git a/discord/ui/section.py b/discord/ui/section.py index 49f17d217e..695dc0b673 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -50,8 +50,8 @@ def __init_subclass__(cls) -> None: def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): super().__init__() - self.items: List[Item] = [] - self.accessory: Optional[Item] = None + self.items: list[Item] = [] + self.accessory: Item | None = None self._underlying = SectionComponent._raw_construct( type=ComponentType.section, From 397acf7e4dac47ca0692d261f41a0bcc645ac261 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 13:39:49 +0100 Subject: [PATCH 126/154] another adjustment --- discord/ui/container.py | 19 +++++++++---------- discord/ui/view.py | 17 ++++++++--------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 5163159c4f..8ed6a5c74a 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -142,7 +142,7 @@ def add_item(self, item: Item) -> None: self._add_component_from_item(item) def get_item(self, id: str | int) -> Item | None: - """Get a top-level item from this container. Alias for `utils.get(container.items, ...)`. + """Get a top-level item from this container. Roughly equal to `utils.get(container.items, ...)`. If an ``int`` is provided it will retrieve by ``id``, otherwise it will check ``custom_id``. Parameters @@ -157,15 +157,14 @@ def get_item(self, id: str | int) -> Item | None: """ if not id: return None - if isinstance(id, int): - child = get(self.items, id=id) - if not child: - for i in self.items: - if hasattr(i, "get_item"): - if child := i.get_item(id): - return child - return child - return find(lambda i: getattr(i, "custom_id", None) == id, self.items) + attr = "id" if isinstance(id, int) else "custom_id" + child = find(lambda i: getattr(i, attr, None) == id, self.items) + if not child: + for i in self.items: + if hasattr(i, "get_item"): + if child := i.get_item(id): + return child + return child def add_section( self, diff --git a/discord/ui/view.py b/discord/ui/view.py index 60ff913605..b604aa5af1 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -382,15 +382,14 @@ def get_item(self, custom_id: str | int) -> Item | None: """ if not custom_id: return None - if isinstance(custom_id, int): - child = get(self.children, id=custom_id) - if not child: - for i in self.children: - if hasattr(i, "get_item"): - if child := i.get_item(custom_id): - return child - return child - return find(lambda i: getattr(i, "custom_id", None) == custom_id, self.children) + attr = "id" if isinstance(custom_id, int) else "custom_id" + child = find(lambda i: getattr(i, attr, None) == custom_id, self.children) + if not child: + for i in self.children: + if hasattr(i, "get_item"): + if child := i.get_item(custom_id): + return child + return child async def interaction_check(self, interaction: Interaction) -> bool: """|coro| From 64df1495e117784e6a653551677227afae32060a Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 13:41:16 +0100 Subject: [PATCH 127/154] doc clarification --- discord/ui/container.py | 1 + discord/ui/view.py | 1 + 2 files changed, 2 insertions(+) diff --git a/discord/ui/container.py b/discord/ui/container.py index 8ed6a5c74a..c01dc82051 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -144,6 +144,7 @@ def add_item(self, item: Item) -> None: def get_item(self, id: str | int) -> Item | None: """Get a top-level item from this container. Roughly equal to `utils.get(container.items, ...)`. If an ``int`` is provided it will retrieve by ``id``, otherwise it will check ``custom_id``. + This method will also search nested items. Parameters ---------- diff --git a/discord/ui/view.py b/discord/ui/view.py index b604aa5af1..5e2393a353 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -369,6 +369,7 @@ def clear_items(self) -> None: def get_item(self, custom_id: str | int) -> Item | None: """Get an item from the view. Roughly equal to `utils.get(view.children, ...)`. If an ``int`` is provided it will retrieve by ``id``, otherwise it will check ``custom_id``. + This method will also search nested items. Parameters ---------- From 94844e2098186d89e3dab998ee05b9444738bfab Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 15:28:27 +0100 Subject: [PATCH 128/154] fix awkward decorator behavior --- discord/ui/container.py | 11 ++--------- discord/ui/section.py | 11 +---------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index c01dc82051..525e5d51c6 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -87,12 +87,9 @@ def __init__( item: Item = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ ) - item.callback = partial(func, self.view, item) - if not self.view: - item._tmp_func = func - else: - setattr(self.view, func.__name__, item) + item.callback = partial(func, self, item) self.add_item(item) + setattr(self, func.__name__, item) for i in items: self.add_item(i) @@ -304,10 +301,6 @@ def colour(self, value: int | Colour | None): # type: ignore def view(self, value): self._view = value for item in self.items: - if getattr(item, "_tmp_func", None): - item.callback = partial(item._tmp_func, self.view, item) - setattr(self.view, item._tmp_func.__name__, item) - delattr(item, "_tmp_func") item._view = value if hasattr(item, "items"): item.view = value diff --git a/discord/ui/section.py b/discord/ui/section.py index 695dc0b673..cbdfe32e31 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -64,11 +64,8 @@ def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): **func.__discord_ui_model_kwargs__ ) item.callback = partial(func, self.view, item) - if not self.view: - item._tmp_func = func - else: - setattr(self.view, func.__name__, item) self.set_accessory(item) + setattr(self, func.__name__, item) elif accessory: self.set_accessory(accessory) for i in items: @@ -178,12 +175,6 @@ def set_accessory(self, item: Item) -> None: def view(self, value): self._view = value if self.accessory: - if getattr(self.accessory, "_tmp_func", None): - self.accessory.callback = partial( - self.accessory._tmp_func, self.view, self.accessory - ) - setattr(self.view, self.accessory._tmp_func.__name__, self.accessory) - delattr(self.accessory, "_tmp_func") self.accessory._view = value def copy_text(self) -> str: From 05c8e86e677fec21b95e359ad6b4aecfe522885a Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 20 Apr 2025 15:36:27 +0100 Subject: [PATCH 129/154] self --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index cbdfe32e31..d6f08def6a 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -63,7 +63,7 @@ def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): item: Item = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ ) - item.callback = partial(func, self.view, item) + item.callback = partial(func, self, item) self.set_accessory(item) setattr(self, func.__name__, item) elif accessory: From b7fe61668b72316f795cd6f99ecd036e53c2e8f5 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:01:21 +0100 Subject: [PATCH 130/154] add Section.set_thumbnail shortcut --- discord/ui/section.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index d6f08def6a..c11294c965 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -9,6 +9,7 @@ from ..utils import get from .item import Item, ItemCallbackType from .text_display import TextDisplay +from .thumbnail import Thumbnail __all__ = ("Section",) @@ -123,7 +124,7 @@ def get_item(self, id: int) -> Item | None: return self.accessory return get(self.items, id=id) - def add_text(self, content: str, id: int | None = None) -> None: + def add_text(self, content: str, *, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the section. Parameters @@ -135,8 +136,6 @@ def add_text(self, content: str, id: int | None = None) -> None: Raises ------ - TypeError - A :class:`str` was not passed. ValueError Maximum number of items has been exceeded (3). """ @@ -171,6 +170,21 @@ def set_accessory(self, item: Item) -> None: self.accessory = item self._underlying.accessory = item._underlying + def set_thumbnail(self, url: str, *, id: int | None = None) -> None: + """Set a :class:`Thumbnail` with the provided url as the section's :attr:`accessory`. + + Parameters + ---------- + url: :class:`str` + The url of the thumbnail. + id: Optiona[:class:`int`] + The thumbnail's ID. + """ + + thumbnail = Thumbnail(url, id=id) + + self.set_accessory(thumbnail) + @Item.view.setter def view(self, value): self._view = value From d39b31c63e7935ed760e7e53e3558689d1a0dda9 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:09:13 +0100 Subject: [PATCH 131/154] add kwargs --- discord/ui/section.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index c11294c965..6538a53d79 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -170,18 +170,22 @@ def set_accessory(self, item: Item) -> None: self.accessory = item self._underlying.accessory = item._underlying - def set_thumbnail(self, url: str, *, id: int | None = None) -> None: + def set_thumbnail(self, url: str, *, description: str | None = None, spoiler: bool = False, id: int | None = None) -> None: """Set 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 is a spoiler. Defaults to ``False``. id: Optiona[:class:`int`] The thumbnail's ID. """ - thumbnail = Thumbnail(url, id=id) + thumbnail = Thumbnail(url, description=description, spoiler=spoiler, id=id) self.set_accessory(thumbnail) From 794acf88f07aeb0435c83f5ee344dd2cf2b07e6f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:09:42 +0000 Subject: [PATCH 132/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/section.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 6538a53d79..e5663e8ab4 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -170,7 +170,14 @@ def set_accessory(self, item: Item) -> None: self.accessory = item self._underlying.accessory = item._underlying - def set_thumbnail(self, url: str, *, description: str | None = None, spoiler: bool = False, id: int | None = None) -> None: + def set_thumbnail( + self, + url: str, + *, + description: str | None = None, + spoiler: bool = False, + id: int | None = None, + ) -> None: """Set a :class:`Thumbnail` with the provided url as the section's :attr:`accessory`. Parameters From 7e2f8ef7e7d459698d15e1c5a95c446955d8e3c0 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:12:59 +0100 Subject: [PATCH 133/154] basic paginator support --- discord/ext/pages/pagination.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index 24d66c6c46..4bee02e36e 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -155,9 +155,9 @@ def __init__( files: list[discord.File] | None = None, **kwargs, ): - if content is None and embeds is None: + if content is None and embeds is None and custom_view is None: raise discord.InvalidArgument( - "A page cannot have both content and embeds equal to None." + "A page must at least have content, embeds, or custom_view set." ) self._content = content self._embeds = embeds or [] @@ -918,6 +918,8 @@ def get_page_content( return Page(content=None, embeds=[page], files=[]) elif isinstance(page, discord.File): return Page(content=None, embeds=[], files=[page]) + elif isinstance(page, discord.ui.View): + return Page(content=None, embeds=[], files=[], custom_view=page) elif isinstance(page, List): if all(isinstance(x, discord.Embed) for x in page): return Page(content=None, embeds=page, files=[]) @@ -927,7 +929,7 @@ def get_page_content( raise TypeError("All list items must be embeds or files.") else: raise TypeError( - "Page content must be a Page object, string, an embed, a list of" + "Page content must be a Page object, string, an embed, a view, a list of" " embeds, a file, or a list of files." ) From 9acbcc0c1035f6f6a16105ee3a057667e24c0c27 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 23 Apr 2025 01:20:35 +0100 Subject: [PATCH 134/154] Update discord/ui/item.py fair game Co-authored-by: plun1331 Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/item.py b/discord/ui/item.py index 662e6d1eb5..59cf81b11d 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -88,7 +88,7 @@ def is_dispatchable(self) -> bool: return False def is_persistent(self) -> bool: - return self._provided_custom_id + return not self.is_dispatchable() or self._provided_custom_id def copy_text(self) -> str: return "" From a09b8cec5da1fc3bed727612b39474d7af8c2f37 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:31:57 +0100 Subject: [PATCH 135/154] rough example --- examples/views/new_components.py | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 examples/views/new_components.py diff --git a/examples/views/new_components.py b/examples/views/new_components.py new file mode 100644 index 0000000000..e5d7f35319 --- /dev/null +++ b/examples/views/new_components.py @@ -0,0 +1,48 @@ +from discord import ApplicationContext, Bot, ButtonStyle, Color, File, Interaction, SeparatorSpacingSize, User +from discord.ui import View, Container, Section, TextDisplay, Thumbnail, Separator, MediaGallery, Button, Select +from io import BytesIO + +class MyView(View): + def __init__(self, user: User): + super().__init__(timeout=30) + text1 = TextDisplay("### This is a sample `TextDisplay` in a `Section`.") + text2 = TextDisplay("This section is contained in a `Container`.\nTo the right, you can see a `Thumbnail`.") + thumbnail = Thumbnail(user.display_avatar.url) + + section = Section(text1, text2, accessory=thumbnail) + section.add_text("-# Small text") + + container = Container(section, TextDisplay("Another `TextDisplay` separate from the `Section`."), color=Color.blue()) + container.add_separator(divider=True, spacing=SeparatorSpacingSize.large) + container.add_item(Separator()) + container.add_file("attachment://sample.png") + container.add_text("Above is two `Separator`s followed by a `File`.") + + gallery = MediaGallery() + gallery.add_item(user.default_avatar.url) + gallery.add_item(user.avatar.url) + + self.add_item(container) + self.add_item(gallery) + self.add_item(TextDisplay("Above is a `MediaGallery` containing two `MediaGalleryItem`s.")) + + @discord.ui.button(label="Delete Message", style=ButtonStyle.red, id=200) + async def delete_button(self, button: Button, interaction: Interaction): + await interaction.response.defer(invisible=True) + await interaction.delete_original_response() + + async def on_timeout(self): + self.get_item(200).disabled = True + await self.message.edit(view=self) + +bot = Bot() + +@bot.command() +async def show_view(ctx: ApplicationContext): + """Display a sample View showcasing various new components.""" + + f = await ctx.author.display_avatar.read() + file = File(BytesIO(f), filename="sample.png") + await ctx.respond(view=MyView(ctx.author), files=[file]) + +bot.run("TOKEN") \ No newline at end of file From 4dee8def3ae54ff272a61cdcec313f04a2b10ce0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:32:25 +0000 Subject: [PATCH 136/154] style(pre-commit): auto fixes from pre-commit.com hooks --- examples/views/new_components.py | 46 +++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/examples/views/new_components.py b/examples/views/new_components.py index e5d7f35319..d567a8b4a8 100644 --- a/examples/views/new_components.py +++ b/examples/views/new_components.py @@ -1,18 +1,45 @@ -from discord import ApplicationContext, Bot, ButtonStyle, Color, File, Interaction, SeparatorSpacingSize, User -from discord.ui import View, Container, Section, TextDisplay, Thumbnail, Separator, MediaGallery, Button, Select from io import BytesIO +from discord import ( + ApplicationContext, + Bot, + ButtonStyle, + Color, + File, + Interaction, + SeparatorSpacingSize, + User, +) +from discord.ui import ( + Button, + Container, + MediaGallery, + Section, + Select, + Separator, + TextDisplay, + Thumbnail, + View, +) + + class MyView(View): def __init__(self, user: User): super().__init__(timeout=30) text1 = TextDisplay("### This is a sample `TextDisplay` in a `Section`.") - text2 = TextDisplay("This section is contained in a `Container`.\nTo the right, you can see a `Thumbnail`.") + text2 = TextDisplay( + "This section is contained in a `Container`.\nTo the right, you can see a `Thumbnail`." + ) thumbnail = Thumbnail(user.display_avatar.url) - + section = Section(text1, text2, accessory=thumbnail) section.add_text("-# Small text") - container = Container(section, TextDisplay("Another `TextDisplay` separate from the `Section`."), color=Color.blue()) + container = Container( + section, + TextDisplay("Another `TextDisplay` separate from the `Section`."), + color=Color.blue(), + ) container.add_separator(divider=True, spacing=SeparatorSpacingSize.large) container.add_item(Separator()) container.add_file("attachment://sample.png") @@ -24,7 +51,9 @@ def __init__(self, user: User): self.add_item(container) self.add_item(gallery) - self.add_item(TextDisplay("Above is a `MediaGallery` containing two `MediaGalleryItem`s.")) + self.add_item( + TextDisplay("Above is a `MediaGallery` containing two `MediaGalleryItem`s.") + ) @discord.ui.button(label="Delete Message", style=ButtonStyle.red, id=200) async def delete_button(self, button: Button, interaction: Interaction): @@ -35,8 +64,10 @@ async def on_timeout(self): self.get_item(200).disabled = True await self.message.edit(view=self) + bot = Bot() + @bot.command() async def show_view(ctx: ApplicationContext): """Display a sample View showcasing various new components.""" @@ -45,4 +76,5 @@ async def show_view(ctx: ApplicationContext): file = File(BytesIO(f), filename="sample.png") await ctx.respond(view=MyView(ctx.author), files=[file]) -bot.run("TOKEN") \ No newline at end of file + +bot.run("TOKEN") From 0d1e695add818f1bbabdb258cde8775c05a69128 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:52:03 +0100 Subject: [PATCH 137/154] Apply suggestions from code review thank u for proofreading Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/components.py | 14 +++++++------- discord/ui/container.py | 16 +++++++++------- discord/ui/media_gallery.py | 3 ++- discord/ui/section.py | 6 +++--- discord/ui/view.py | 2 +- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/discord/components.py b/discord/components.py index 87a301d120..960487e33c 100644 --- a/discord/components.py +++ b/discord/components.py @@ -107,7 +107,7 @@ class Component: type: :class:`ComponentType` The type of component. id: :class:`int` - The component's ID. If not provided by the user, it's automatically incremented. + The component's ID. If not provided by the user, it is automatically incremented. """ __slots__: tuple[str, ...] = ("type", "id") @@ -171,7 +171,7 @@ def __init__(self, data: ComponentPayload): @property def width(self): - """Return the total item width used by this action row.""" + """Return the total item width that this action row uses.""" t = 0 for item in self.children: t += 1 if item.type is ComponentType.button else 5 @@ -567,7 +567,7 @@ def to_dict(self) -> SelectOptionPayload: class Section(Component): """Represents a Section from Components V2. - This is a component that contains other components such as :class:`TextDisplay` and :class:`Thumbnail`. + This is a component that groups other components together. This inherits from :class:`Component`. @@ -679,7 +679,7 @@ def to_dict(self): class Thumbnail(Component): """Represents a Thumbnail from Components V2. - This is a component that displays media such as images and videos. + This is a component that displays media, such as images and videos. This inherits from :class:`Component`. @@ -772,7 +772,7 @@ def to_dict(self): class MediaGallery(Component): """Represents a Media Gallery from Components V2. - This is a component that displays up to 10 different :class:`MediaGalleryItem`s. + This is a component that displays up to 10 different :class:`MediaGalleryItem` objects. This inherits from :class:`Component`. @@ -850,7 +850,7 @@ def to_dict(self) -> FileComponentPayload: class Separator(Component): """Represents a Separator from Components V2. - This is a component that separates components. + This is a component that separates other components. This inherits from :class:`Component`. @@ -893,7 +893,7 @@ class Container(Component): """Represents a Container from Components V2. This is a component that contains up to 10 different :class:`Component`s. - It may only contain :class:`ActionRow`, :class:`TextDisplay`, :class:`Section`, :class:`MediaGallery`, :class:`Separator`, and :class:`FileComponent`. + It may only contain objects of type :class:`ActionRow`, :class:`TextDisplay`, :class:`Section`, :class:`MediaGallery`, :class:`Separator`, or :class:`FileComponent`. This inherits from :class:`Component`. diff --git a/discord/ui/container.py b/discord/ui/container.py index 525e5d51c6..f49620de08 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -31,7 +31,7 @@ class Container(Item[V]): """Represents a UI Container. Containers may contain up to 10 items. - The current items supported are: + The current items supported are as follows: - :class:`discord.ui.Button` - :class:`discord.ui.Select` @@ -140,13 +140,13 @@ def add_item(self, item: Item) -> None: def get_item(self, id: str | int) -> Item | None: """Get a top-level item from this container. Roughly equal to `utils.get(container.items, ...)`. - If an ``int`` is provided it will retrieve by ``id``, otherwise it will check ``custom_id``. - This method will also search nested items. + If an ``int`` is provided, it will be retrieved by ``id``, otherwise it will check for ``custom_id``. + This method will also search for nested items. Parameters ---------- id: :class:`str` - The id or custom_id of the item to get + The id or custom_id of the item to get. Returns ------- @@ -172,8 +172,9 @@ def add_section( ): """Adds a :class:`Section` to the container. - To append a pre-existing :class:`Section` use the - :meth:`add_item` method instead. + To append a pre-existing :class:`Section`, use the + :meth:`add_item` method, instead. + Parameters ---------- @@ -212,7 +213,8 @@ def add_gallery( ): """Adds a :class:`MediaGallery` to the container. - To append a pre-existing :class:`MediaGallery` use the + To append a pre-existing :class:`MediaGallery`, use the + :meth:`add_item` method instead. Parameters diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index e1284801ce..8a0ca618df 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -19,7 +19,8 @@ class MediaGallery(Item[V]): - """Represents a UI Media Gallery. Galleries may contain up to 10 :class:`MediaGalleryItem`s. + """Represents a UI Media Gallery. Galleries may contain up to 10 :class:`MediaGalleryItem` objects. + .. versionadded:: 2.7 diff --git a/discord/ui/section.py b/discord/ui/section.py index e5663e8ab4..22aae3ab5f 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -23,7 +23,7 @@ class Section(Item[V]): - """Represents a UI section. Sections must have 1-3 items and an accessory set. + """Represents a UI section. Sections must have 1-3 (inclusive) items and an accessory set. .. versionadded:: 2.7 @@ -131,7 +131,7 @@ def add_text(self, content: str, *, id: int | None = None) -> None: ---------- content: :class:`str` The content of the text display. - id: Optiona[:class:`int`] + id: Optional[:class:`int`] The text display's ID. Raises @@ -178,7 +178,7 @@ def set_thumbnail( spoiler: bool = False, id: int | None = None, ) -> None: - """Set a :class:`Thumbnail` with the provided url as the section's :attr:`accessory`. + """Sets a :class:`Thumbnail` with the provided URL as the section's :attr:`accessory`. Parameters ---------- diff --git a/discord/ui/view.py b/discord/ui/view.py index 5e2393a353..3bd5454e06 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -579,7 +579,7 @@ def is_persistent(self) -> bool: def is_components_v2(self) -> bool: """Whether the view contains V2 components. - A view containing V2 components may not be sent alongside message content or embeds. + A view containing V2 components cannot be sent alongside message content or embeds. """ return ( any([item._underlying.is_v2() for item in self.children]) From 19e2f9fc58c3df2c86748362b7529025b1c0d66f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:52:33 +0000 Subject: [PATCH 138/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 1 - discord/ui/media_gallery.py | 1 - 2 files changed, 2 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index f49620de08..10bda73fd1 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -175,7 +175,6 @@ def add_section( To append a pre-existing :class:`Section`, use the :meth:`add_item` method, instead. - Parameters ---------- *items: :class:`Item` diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 8a0ca618df..0d8e2ec2be 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -21,7 +21,6 @@ class MediaGallery(Item[V]): """Represents a UI Media Gallery. Galleries may contain up to 10 :class:`MediaGalleryItem` objects. - .. versionadded:: 2.7 Parameters From d3b78aacf970ee9fc3832622cd22f78cccc9fb92 Mon Sep 17 00:00:00 2001 From: plun1331 Date: Wed, 23 Apr 2025 10:05:18 -0700 Subject: [PATCH 139/154] Apply suggestions from code review Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: plun1331 --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 960487e33c..dfea23601e 100644 --- a/discord/components.py +++ b/discord/components.py @@ -892,7 +892,7 @@ def to_dict(self) -> SeparatorComponentPayload: class Container(Component): """Represents a Container from Components V2. - This is a component that contains up to 10 different :class:`Component`s. + This is a component that contains up to 10 different :class:`Component` objects. It may only contain objects of type :class:`ActionRow`, :class:`TextDisplay`, :class:`Section`, :class:`MediaGallery`, :class:`Separator`, or :class:`FileComponent`. This inherits from :class:`Component`. From 8befa8ba0512771f4eca4773990d5d5a53f39890 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 23 Apr 2025 18:28:10 +0100 Subject: [PATCH 140/154] adjust copy_text and fix example --- discord/ui/container.py | 2 +- discord/ui/section.py | 2 +- discord/ui/view.py | 2 +- examples/views/new_components.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 10bda73fd1..3a99180df6 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -269,7 +269,7 @@ def add_separator( def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this container. Equivalent to the `Copy Text` option on Discord clients.""" - return "\n".join([i.copy_text() for i in self.items]) + return "\n".join([i.copy_text() for i in self.items if i]) @property def spoiler(self) -> bool: diff --git a/discord/ui/section.py b/discord/ui/section.py index 22aae3ab5f..eaf435458d 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -204,7 +204,7 @@ def view(self, value): 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(i.copy_text() for i in self.items) + return "\n".join(i.copy_text() for i in self.items if i) @property def type(self) -> ComponentType: diff --git a/discord/ui/view.py b/discord/ui/view.py index 3bd5454e06..22b57d44c5 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -628,7 +628,7 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this View. Equivalent to the `Copy Text` option on Discord clients.""" - return "\n".join([i.copy_text() for i in self.children]) + return "\n".join(i.copy_text() for i in self.children if i) @property def message(self): diff --git a/examples/views/new_components.py b/examples/views/new_components.py index d567a8b4a8..ceaaf71f50 100644 --- a/examples/views/new_components.py +++ b/examples/views/new_components.py @@ -11,6 +11,7 @@ User, ) from discord.ui import ( + button, Button, Container, MediaGallery, @@ -55,7 +56,7 @@ def __init__(self, user: User): TextDisplay("Above is a `MediaGallery` containing two `MediaGalleryItem`s.") ) - @discord.ui.button(label="Delete Message", style=ButtonStyle.red, id=200) + @button(label="Delete Message", style=ButtonStyle.red, id=200) async def delete_button(self, button: Button, interaction: Interaction): await interaction.response.defer(invisible=True) await interaction.delete_original_response() From a1b7ed4f3a891d7f3d983d421e65d9fef1e3d2ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:28:52 +0000 Subject: [PATCH 141/154] style(pre-commit): auto fixes from pre-commit.com hooks --- examples/views/new_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/views/new_components.py b/examples/views/new_components.py index ceaaf71f50..a8ac571a6c 100644 --- a/examples/views/new_components.py +++ b/examples/views/new_components.py @@ -11,7 +11,6 @@ User, ) from discord.ui import ( - button, Button, Container, MediaGallery, @@ -21,6 +20,7 @@ TextDisplay, Thumbnail, View, + button, ) From 6cfa8cced743dc5cae0c216b94923542b00a9af2 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:19:12 +0100 Subject: [PATCH 142/154] disable_all_items and enable_all_items --- discord/ui/container.py | 26 ++++++++++++++++++++++++++ discord/ui/section.py | 28 ++++++++++++++++++++++++++++ discord/ui/view.py | 12 ++++++++---- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 3a99180df6..2b2763ba91 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -314,6 +314,32 @@ def type(self) -> ComponentType: def width(self) -> int: return 5 + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + """ + Disables all buttons and select menus in the container. + + Parameters + ---------- + exclusions: Optional[List[:class:`Item`]] + A list of items in `self.items` to not disable from the view. + """ + for item in self.items: + if hasattr(item, "disabled") and exclusions is None or item not in exclusions: + item.disabled = True + + def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + """ + Enables all buttons and select menus in the container. + + Parameters + ---------- + exclusions: Optional[List[:class:`Item`]] + A list of items in `self.items` to not enable from the view. + """ + for item in self.items: + if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): + item.disabled = False + def to_component_dict(self) -> ContainerComponentPayload: self._set_components(self.items) return self._underlying.to_dict() diff --git a/discord/ui/section.py b/discord/ui/section.py index eaf435458d..e835486715 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -214,6 +214,34 @@ def type(self) -> ComponentType: def width(self) -> int: return 5 + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + """ + 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.items + [self.accessory]: + if hasattr(item, "disabled") and exclusions is None or item not in exclusions: + item.disabled = True + + def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + """ + Enables all buttons and select menus in the container. + 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.items + [self.accessory]: + if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): + item.disabled = False + def to_component_dict(self) -> SectionComponentPayload: self._set_components(self.items) self.set_accessory(self.accessory) diff --git a/discord/ui/view.py b/discord/ui/view.py index 22b57d44c5..139822c9e2 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -602,7 +602,7 @@ async def wait(self) -> bool: def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ - Disables all items in the view. + Disables all buttons and select menus in the view. Parameters ---------- @@ -610,12 +610,14 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.children` to not disable from the view. """ for child in self.children: - if exclusions is None or child not in exclusions: + if hasattr(item, "disabled") and exclusions is None or item not in exclusions: child.disabled = True + if hasattr(item, "items"): + item.disable_all_items(exclusions=exclusions) def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ - Enables all items in the view. + Enables all buttons and select menus in the view. Parameters ---------- @@ -623,8 +625,10 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.children` to not enable from the view. """ for child in self.children: - if exclusions is None or child not in exclusions: + if hasattr(item, "disabled") and exclusions is None or item not in exclusions: child.disabled = False + if hasattr(item, "items"): + item.enable_all_items(exclusions=exclusions) def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this View. Equivalent to the `Copy Text` option on Discord clients.""" From 9c591c64dd981d25a756769678fa1a038575e8a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:19:40 +0000 Subject: [PATCH 143/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 10 ++++++++-- discord/ui/section.py | 10 ++++++++-- discord/ui/view.py | 12 ++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 2b2763ba91..74f9269125 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -324,7 +324,11 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.items` to not disable from the view. """ for item in self.items: - if hasattr(item, "disabled") and exclusions is None or item not in exclusions: + if ( + hasattr(item, "disabled") + and exclusions is None + or item not in exclusions + ): item.disabled = True def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: @@ -337,7 +341,9 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.items` to not enable from the view. """ for item in self.items: - if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): + if hasattr(item, "disabled") and ( + exclusions is None or item not in exclusions + ): item.disabled = False def to_component_dict(self) -> ContainerComponentPayload: diff --git a/discord/ui/section.py b/discord/ui/section.py index e835486715..1126bdc966 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -225,7 +225,11 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.items` to not disable from the view. """ for item in self.items + [self.accessory]: - if hasattr(item, "disabled") and exclusions is None or item not in exclusions: + if ( + hasattr(item, "disabled") + and exclusions is None + or item not in exclusions + ): item.disabled = True def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: @@ -239,7 +243,9 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.items` to not enable from the view. """ for item in self.items + [self.accessory]: - if hasattr(item, "disabled") and (exclusions is None or item not in exclusions): + if hasattr(item, "disabled") and ( + exclusions is None or item not in exclusions + ): item.disabled = False def to_component_dict(self) -> SectionComponentPayload: diff --git a/discord/ui/view.py b/discord/ui/view.py index 139822c9e2..7af4aa66b9 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -610,7 +610,11 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.children` to not disable from the view. """ for child in self.children: - if hasattr(item, "disabled") and exclusions is None or item not in exclusions: + if ( + hasattr(item, "disabled") + and exclusions is None + or item not in exclusions + ): child.disabled = True if hasattr(item, "items"): item.disable_all_items(exclusions=exclusions) @@ -625,7 +629,11 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.children` to not enable from the view. """ for child in self.children: - if hasattr(item, "disabled") and exclusions is None or item not in exclusions: + if ( + hasattr(item, "disabled") + and exclusions is None + or item not in exclusions + ): child.disabled = False if hasattr(item, "items"): item.enable_all_items(exclusions=exclusions) From b624930a9e3fb0f78796a23fa3aeb089539b707d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:36:48 +0100 Subject: [PATCH 144/154] VAR --- discord/ui/view.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 7af4aa66b9..91e0ca0f11 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -611,13 +611,13 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ for child in self.children: if ( - hasattr(item, "disabled") + hasattr(child, "disabled") and exclusions is None - or item not in exclusions + or child not in exclusions ): child.disabled = True - if hasattr(item, "items"): - item.disable_all_items(exclusions=exclusions) + if hasattr(child, "items"): + child.disable_all_items(exclusions=exclusions) def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ @@ -630,13 +630,13 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ for child in self.children: if ( - hasattr(item, "disabled") + hasattr(child, "disabled") and exclusions is None - or item not in exclusions + or child not in exclusions ): child.disabled = False - if hasattr(item, "items"): - item.enable_all_items(exclusions=exclusions) + if hasattr(child, "items"): + child.enable_all_items(exclusions=exclusions) def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this View. Equivalent to the `Copy Text` option on Discord clients.""" From 7778210ae3a4cae12faf8d496ef8fc4c6f7cedac Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:55:19 +0100 Subject: [PATCH 145/154] remove_item qol --- discord/ui/container.py | 16 ++++++++++++++++ discord/ui/section.py | 16 ++++++++++++++++ discord/ui/view.py | 8 +++++--- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 74f9269125..80149b7e09 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -138,6 +138,22 @@ def add_item(self, item: Item) -> None: self.items.append(item) self._add_component_from_item(item) + def remove_item(self, item: Item | int) -> None: + """Removes an item from the container. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. + + Parameters + ---------- + item: Union[:class:`Item`, :class:`int`, :class:`str`] + The item, item :attr:`id`, or item ``custom_id`` to remove from the container. + """ + + if isinstance(item, (str, int)): + item = self.get_item(item) + try: + self.items.remove(item) + except ValueError: + pass + def get_item(self, id: str | int) -> Item | None: """Get a top-level item from this container. Roughly equal to `utils.get(container.items, ...)`. If an ``int`` is provided, it will be retrieved by ``id``, otherwise it will check for ``custom_id``. diff --git a/discord/ui/section.py b/discord/ui/section.py index 1126bdc966..56bdcc1eff 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -105,6 +105,22 @@ def add_item(self, item: Item) -> None: self.items.append(item) self._add_component_from_item(item) + def remove_item(self, item: Item | int) -> None: + """Removes an item from the section. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. + + Parameters + ---------- + item: Union[:class:`Item`, :class:`int`, :class:`str`] + The item, item :attr:`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 + def get_item(self, id: int) -> Item | None: """Get an item from this section. Alias for `utils.get(section.items, id=id)`. diff --git a/discord/ui/view.py b/discord/ui/view.py index 91e0ca0f11..9f46e09710 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -346,14 +346,16 @@ def add_item(self, item: Item) -> None: self.children.append(item) def remove_item(self, item: Item) -> None: - """Removes an item from the view. + """Removes an item from the view. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. Parameters ---------- - item: :class:`Item` - The item to remove from the view. + item: Union[:class:`Item`, :class:`int`, :class:`str`] + The item, item :attr:`id`, or item ``custom_id`` to remove from the view. """ + if isinstance(item, (str, int)): + item = self.get_item(item) try: self.children.remove(item) except ValueError: From 0f3e7cd6e6371faae43224acf4b35d136640b26c Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:14:41 +0100 Subject: [PATCH 146/154] brackets --- discord/ui/container.py | 4 ++-- discord/ui/section.py | 4 ++-- discord/ui/view.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 80149b7e09..366c65cdaf 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -342,8 +342,8 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: for item in self.items: if ( hasattr(item, "disabled") - and exclusions is None - or item not in exclusions + and (exclusions is None + or item not in exclusions) ): item.disabled = True diff --git a/discord/ui/section.py b/discord/ui/section.py index 56bdcc1eff..289a5a3fe0 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -243,8 +243,8 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: for item in self.items + [self.accessory]: if ( hasattr(item, "disabled") - and exclusions is None - or item not in exclusions + and (exclusions is None + or item not in exclusions) ): item.disabled = True diff --git a/discord/ui/view.py b/discord/ui/view.py index 9f46e09710..2d22ed4f3b 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -614,8 +614,8 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: for child in self.children: if ( hasattr(child, "disabled") - and exclusions is None - or child not in exclusions + and (exclusions is None + or child not in exclusions) ): child.disabled = True if hasattr(child, "items"): @@ -633,8 +633,8 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: for child in self.children: if ( hasattr(child, "disabled") - and exclusions is None - or child not in exclusions + and (exclusions is None + or child not in exclusions) ): child.disabled = False if hasattr(child, "items"): From 29c1d407cd835681d1c1cf0eb30fe0a3fb8f162d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:15:22 +0000 Subject: [PATCH 147/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 6 ++---- discord/ui/section.py | 6 ++---- discord/ui/view.py | 12 ++++-------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 366c65cdaf..f6ec504d57 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -340,10 +340,8 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.items` to not disable from the view. """ for item in self.items: - if ( - hasattr(item, "disabled") - and (exclusions is None - or item not in exclusions) + if hasattr(item, "disabled") and ( + exclusions is None or item not in exclusions ): item.disabled = True diff --git a/discord/ui/section.py b/discord/ui/section.py index 289a5a3fe0..b79869cf76 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -241,10 +241,8 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.items` to not disable from the view. """ for item in self.items + [self.accessory]: - if ( - hasattr(item, "disabled") - and (exclusions is None - or item not in exclusions) + if hasattr(item, "disabled") and ( + exclusions is None or item not in exclusions ): item.disabled = True diff --git a/discord/ui/view.py b/discord/ui/view.py index 2d22ed4f3b..435e0d0e96 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -612,10 +612,8 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.children` to not disable from the view. """ for child in self.children: - if ( - hasattr(child, "disabled") - and (exclusions is None - or child not in exclusions) + if hasattr(child, "disabled") and ( + exclusions is None or child not in exclusions ): child.disabled = True if hasattr(child, "items"): @@ -631,10 +629,8 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: A list of items in `self.children` to not enable from the view. """ for child in self.children: - if ( - hasattr(child, "disabled") - and (exclusions is None - or child not in exclusions) + if hasattr(child, "disabled") and ( + exclusions is None or child not in exclusions ): child.disabled = False if hasattr(child, "items"): From 98a9bc5058a599ced3cc357f39c6012316aec606 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:40:31 +0100 Subject: [PATCH 148/154] Update discord/ui/view.py Co-authored-by: plun1331 Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 435e0d0e96..93bb524814 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -345,7 +345,7 @@ def add_item(self, item: Item) -> None: item.view = self self.children.append(item) - def remove_item(self, item: Item) -> None: + def remove_item(self, item: Item | int | str) -> None: """Removes an item from the view. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. Parameters From 45fe45eeb54c6f4519e8d76c22c705bd396a0843 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:44:40 +0100 Subject: [PATCH 149/154] Update discord/components.py Co-authored-by: plun1331 Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index dfea23601e..0c5d238515 100644 --- a/discord/components.py +++ b/discord/components.py @@ -807,7 +807,7 @@ def to_dict(self) -> MediaGalleryComponentPayload: class FileComponent(Component): """Represents a File from Components V2. - This is a component that displays some file (elaborate?). + This component displays a downloadable file in a message. This inherits from :class:`Component`. From 7c2d96dff36b6dcd04586e27f14ffcaab3023cfc Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 25 Apr 2025 02:53:00 +0100 Subject: [PATCH 150/154] textdisplay limits --- discord/ui/text_display.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 4ca9b0e18b..dcc7056787 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -19,14 +19,14 @@ class TextDisplay(Item[V]): - """Represents a UI TextDisplay. + """Represents a UI text display. A message can have up to 4000 characters across all :class:`TextDisplay` objects combined. .. versionadded:: 2.7 Parameters ---------- content: :class:`str` - The text display's content. + The text display's content, up to 4000 characters. """ def __init__( From 1c54b08a6194eb51124998e4bb29bb26abef2c8e Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 26 Apr 2025 18:43:28 +0100 Subject: [PATCH 151/154] Update discord/components.py Co-authored-by: Paillat Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/components.py b/discord/components.py index 0c5d238515..2ff6714d1e 100644 --- a/discord/components.py +++ b/discord/components.py @@ -956,6 +956,7 @@ def to_dict(self) -> ContainerComponentPayload: 17: Container, } +STATE_COMPONENTS = (Section, Container, Thumbnail, MediaGallery, FileComponent) def _component_factory(data: ComponentPayload, state=None) -> Component: component_type = data["type"] From 2adca36e78974e69efbb7cc06964c91a554e5740 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 17:43:54 +0000 Subject: [PATCH 152/154] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/components.py b/discord/components.py index 2ff6714d1e..45aab51d0c 100644 --- a/discord/components.py +++ b/discord/components.py @@ -958,6 +958,7 @@ def to_dict(self) -> ContainerComponentPayload: STATE_COMPONENTS = (Section, Container, Thumbnail, MediaGallery, FileComponent) + def _component_factory(data: ComponentPayload, state=None) -> Component: component_type = data["type"] if cls := COMPONENT_MAPPINGS.get(component_type): From 2ee8c019df681910d15ec51179cf2c9eb5c51c8b Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 26 Apr 2025 18:44:37 +0100 Subject: [PATCH 153/154] Update discord/components.py Co-authored-by: Paillat Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 45aab51d0c..fa4354ceea 100644 --- a/discord/components.py +++ b/discord/components.py @@ -962,7 +962,7 @@ def to_dict(self) -> ContainerComponentPayload: def _component_factory(data: ComponentPayload, state=None) -> Component: component_type = data["type"] if cls := COMPONENT_MAPPINGS.get(component_type): - if cls in (Section, Container, Thumbnail, MediaGallery, FileComponent): + if issubclass(cls, STATE_COMPONENTS): return cls(data, state=state) else: return cls(data) From 6296f7223336d5f99312087711f6f85c98eac149 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:16:17 +0100 Subject: [PATCH 154/154] chaining support --- discord/ui/container.py | 13 ++++++++----- discord/ui/media_gallery.py | 3 ++- discord/ui/modal.py | 2 ++ discord/ui/section.py | 9 +++++++-- discord/ui/select.py | 3 ++- discord/ui/view.py | 5 +++++ 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index f6ec504d57..e058602034 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -137,6 +137,7 @@ def add_item(self, item: Item) -> None: self.items.append(item) self._add_component_from_item(item) + return self def remove_item(self, item: Item | int) -> None: """Removes an item from the container. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. @@ -153,6 +154,7 @@ def remove_item(self, item: Item | int) -> None: self.items.remove(item) except ValueError: pass + return self def get_item(self, id: str | int) -> Item | None: """Get a top-level item from this container. Roughly equal to `utils.get(container.items, ...)`. @@ -206,7 +208,7 @@ def add_section( section = Section(*items, accessory=accessory, id=id) - self.add_item(section) + return self.add_item(section) def add_text(self, content: str, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the container. @@ -219,7 +221,7 @@ def add_text(self, content: str, id: int | None = None) -> None: text = TextDisplay(content, id=id) - self.add_item(text) + return self.add_item(text) def add_gallery( self, @@ -243,7 +245,7 @@ def add_gallery( g = MediaGallery(*items, id=id) - self.add_item(g) + return self.add_item(g) def add_file(self, url: str, spoiler: bool = False, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the container. @@ -260,7 +262,7 @@ def add_file(self, url: str, spoiler: bool = False, id: int | None = None) -> No f = File(url, spoiler=spoiler, id=id) - self.add_item(f) + return self.add_item(f) def add_separator( self, @@ -281,7 +283,7 @@ def add_separator( s = Separator(divider=divider, spacing=spacing, id=id) - self.add_item(s) + return self.add_item(s) def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this container. Equivalent to the `Copy Text` option on Discord clients.""" @@ -359,6 +361,7 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: exclusions is None or item not in exclusions ): item.disabled = False + return self def to_component_dict(self) -> ContainerComponentPayload: self._set_components(self.items) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 0d8e2ec2be..daa614e06f 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -63,6 +63,7 @@ def append_item(self, item: MediaGalleryItem) -> None: raise TypeError(f"expected MediaGalleryItem not {item.__class__!r}") self._underlying.items.append(item) + return self def add_item( self, @@ -93,7 +94,7 @@ def add_item( item = MediaGalleryItem(url, description=description, spoiler=spoiler) - self.append_item(item) + return self.append_item(item) @Item.view.setter def view(self, value): diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 966e6abe0f..97b302860f 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -205,6 +205,7 @@ def add_item(self, item: InputText): self._weights.add_item(item) self._children.append(item) + return self def remove_item(self, item: InputText): """Removes an InputText component from the modal dialog. @@ -218,6 +219,7 @@ def remove_item(self, item: InputText): self._children.remove(item) except ValueError: pass + return self def stop(self) -> None: """Stops listening to interaction events from the modal dialog.""" diff --git a/discord/ui/section.py b/discord/ui/section.py index b79869cf76..37c5bb127d 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -104,6 +104,7 @@ def add_item(self, item: Item) -> None: self.items.append(item) self._add_component_from_item(item) + return self def remove_item(self, item: Item | int) -> None: """Removes an item from the section. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. @@ -120,6 +121,7 @@ def remove_item(self, item: Item | int) -> None: self.items.remove(item) except ValueError: pass + return self def get_item(self, id: int) -> Item | None: """Get an item from this section. Alias for `utils.get(section.items, id=id)`. @@ -161,7 +163,7 @@ def add_text(self, content: str, *, id: int | None = None) -> None: text = TextDisplay(content, id=id) - self.add_item(text) + return self.add_item(text) def set_accessory(self, item: Item) -> None: """Set an item as the section's :attr:`accessory`. @@ -185,6 +187,7 @@ def set_accessory(self, item: Item) -> None: self.accessory = item self._underlying.accessory = item._underlying + return self def set_thumbnail( self, @@ -210,7 +213,7 @@ def set_thumbnail( thumbnail = Thumbnail(url, description=description, spoiler=spoiler, id=id) - self.set_accessory(thumbnail) + return self.set_accessory(thumbnail) @Item.view.setter def view(self, value): @@ -245,6 +248,7 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: exclusions is None or item not in exclusions ): item.disabled = True + return self def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ @@ -261,6 +265,7 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: exclusions is None or item not in exclusions ): item.disabled = False + return self def to_component_dict(self) -> SectionComponentPayload: self._set_components(self.items) diff --git a/discord/ui/select.py b/discord/ui/select.py index 982b210693..3bdc4b518a 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -304,7 +304,7 @@ def add_option( default=default, ) - self.append_option(option) + return self.append_option(option) def append_option(self, option: SelectOption): """Appends an option to the select menu. @@ -326,6 +326,7 @@ def append_option(self, option: SelectOption): raise ValueError("maximum number of options already provided") self._underlying.options.append(option) + return self @property def values( diff --git a/discord/ui/view.py b/discord/ui/view.py index 93bb524814..b3fa735e10 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -344,6 +344,7 @@ def add_item(self, item: Item) -> None: if hasattr(item, "items"): item.view = self self.children.append(item) + return self def remove_item(self, item: Item | int | str) -> None: """Removes an item from the view. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively. @@ -362,11 +363,13 @@ def remove_item(self, item: Item | int | str) -> None: pass else: self.__weights.remove_item(item) + return self def clear_items(self) -> None: """Removes all items from the view.""" self.children.clear() self.__weights.clear() + return self def get_item(self, custom_id: str | int) -> Item | None: """Get an item from the view. Roughly equal to `utils.get(view.children, ...)`. @@ -618,6 +621,7 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: child.disabled = True if hasattr(child, "items"): child.disable_all_items(exclusions=exclusions) + return self def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ @@ -635,6 +639,7 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: child.disabled = False if hasattr(child, "items"): child.enable_all_items(exclusions=exclusions) + return self def copy_text(self) -> str: """Returns the text of all :class:`~discord.ui.TextDisplay` items in this View. Equivalent to the `Copy Text` option on Discord clients."""