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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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.""" From 381a3c80f02bfc44805ea8e5ad3a7978d8f5e950 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:41:33 +0100 Subject: [PATCH 155/228] raise limits --- discord/components.py | 2 +- discord/ui/container.py | 6 ++---- discord/ui/view.py | 10 +++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/discord/components.py b/discord/components.py index fa4354ceea..61fc6df506 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` objects. + This is a component that contains 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`. diff --git a/discord/ui/container.py b/discord/ui/container.py index e058602034..fcbfd5cfa7 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -29,7 +29,7 @@ class Container(Item[V]): - """Represents a UI Container. Containers may contain up to 10 items. + """Represents a UI Container. The current items supported are as follows: @@ -46,7 +46,7 @@ class Container(Item[V]): Parameters ---------- *items: :class:`Item` - The initial items in this container, up to 10. + The initial items in this container. colour: Union[:class:`Colour`, :class:`int`] The accent colour of the container. Aliased to ``color`` as well. """ @@ -124,8 +124,6 @@ def add_item(self, item: Item) -> None: ------ TypeError An :class:`Item` was not passed. - ValueError - Maximum number of items has been exceeded (10). """ if not isinstance(item, Item): diff --git a/discord/ui/view.py b/discord/ui/view.py index b3fa735e10..06b221e381 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]*7) if item.row is not None: total = self.weights[item.row] + item.width if total > 5: @@ -200,8 +200,8 @@ def __init_subclass__(cls) -> None: if hasattr(member, "__discord_ui_model_type__"): children.append(member) - if len(children) > 25: - raise TypeError("View cannot have more than 25 children") + if len(children) > 40: + raise TypeError("View cannot have more than 40 children") cls.__view_children_items__ = children @@ -328,11 +328,11 @@ def add_item(self, item: Item) -> None: TypeError An :class:`Item` was not passed. ValueError - Maximum number of children has been exceeded (25) + Maximum number of children has been exceeded (40) or the row the item is trying to be added to is full. """ - if len(self.children) > 25: + if len(self.children) > 40: raise ValueError("maximum number of children exceeded") if not isinstance(item, Item): From 94a313dc0f8e457efd41a721332596b84fafb8b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:42:04 +0000 Subject: [PATCH 156/228] 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 06b221e381..66e2575df5 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]*7) + self.weights.extend([0, 0, 0, 0, 0] * 7) if item.row is not None: total = self.weights[item.row] + item.width if total > 5: From 93f9b5f3612d2e1ea345a4ab6be1182d5ffcf1ae Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 30 Apr 2025 06:38:06 +0100 Subject: [PATCH 157/228] adjust weights --- discord/ui/item.py | 8 ++++---- discord/ui/view.py | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/discord/ui/item.py b/discord/ui/item.py index 59cf81b11d..20757c4ec8 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -104,7 +104,7 @@ def row(self) -> int | None: """Gets or sets the row position of this item within its parent view. The row position determines the vertical placement of the item in the UI. - The value must be an integer between 0 and 4 (inclusive), or ``None`` to indicate + The value must be an integer between 0 and 39 (inclusive), or ``None`` to indicate that no specific row is set. Returns @@ -115,7 +115,7 @@ def row(self) -> int | None: Raises ------ ValueError - If the row value is not ``None`` and is outside the range [0, 4]. + If the row value is not ``None`` and is outside the range [0, 39]. """ return self._row @@ -123,10 +123,10 @@ def row(self) -> int | None: def row(self, value: int | None): if value is None: self._row = None - elif 5 > value >= 0: + elif 39 > value >= 0: self._row = value else: - raise ValueError("row cannot be negative or greater than or equal to 5") + raise ValueError("row cannot be negative or greater than or equal to 39") @property def width(self) -> int: diff --git a/discord/ui/view.py b/discord/ui/view.py index 66e2575df5..80689c6add 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -131,7 +131,7 @@ 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(): + if (item._underlying.is_v2() or not self.fits_legacy(item)) and not self.requires_v2(): self.weights.extend([0, 0, 0, 0, 0] * 7) if item.row is not None: total = self.weights[item.row] + item.width @@ -157,6 +157,10 @@ def clear(self) -> None: def requires_v2(self) -> bool: return sum(w > 0 for w in self.weights) > 5 + def fits_legacy(self, item) -> bool: + if item.row is not None: + return item.row <= 4 + return self.weights[-1] + item.width <= 5 class View: """Represents a UI view. From 6e9248a1eeeb3f3d6b0541b0caee865187490628 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 05:38:52 +0000 Subject: [PATCH 158/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 80689c6add..33d10d0abc 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -131,7 +131,9 @@ 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() or not self.fits_legacy(item)) and not self.requires_v2(): + if ( + item._underlying.is_v2() or not self.fits_legacy(item) + ) and not self.requires_v2(): self.weights.extend([0, 0, 0, 0, 0] * 7) if item.row is not None: total = self.weights[item.row] + item.width @@ -162,6 +164,7 @@ def fits_legacy(self, item) -> bool: return item.row <= 4 return self.weights[-1] + item.width <= 5 + class View: """Represents a UI view. From a2d9140b3fc07a5b86d32d228ce76c13dd857b72 Mon Sep 17 00:00:00 2001 From: plun1331 Date: Wed, 30 Apr 2025 23:43:04 -0700 Subject: [PATCH 159/228] Update discord/ui/section.py Co-authored-by: Paillat Signed-off-by: plun1331 --- 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 37c5bb127d..aa771964da 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -207,7 +207,8 @@ def set_thumbnail( 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`] + id: Optional[:class:`int`] + The thumbnail's ID. """ From ebef722f3cc966b86f893fd097ac802ebf2aadca Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 8 May 2025 06:18:29 +0100 Subject: [PATCH 160/228] additional weight logic --- discord/ui/container.py | 6 +++++- discord/ui/view.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index fcbfd5cfa7..6bc6acf646 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -97,13 +97,17 @@ def _add_component_from_item(self, item: Item): if item._underlying.is_v2(): self._underlying.components.append(item._underlying) else: + found = False 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) + found = True + elif not isinstance(row, ActionRow): + # create new row if last component is v2 break - else: + if not found: row = ActionRow.with_components(item._underlying) self._underlying.components.append(row) diff --git a/discord/ui/view.py b/discord/ui/view.py index 33d10d0abc..7451ff987e 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -125,7 +125,8 @@ def __init__(self, children: list[Item]): def find_open_space(self, item: Item) -> int: for index, weight in enumerate(self.weights): - if weight + item.width <= 5: + # check if open space AND (next row has no items OR this is the last row) + if (weight + item.width <= 5) and ((index < len(self.weights) - 1 and self.weights[index + 1] == 0) or index == len(self.weights) - 1): return index raise ValueError("could not find open space for item") From 08faba35fe5672344d9250657a6bec07631fede9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 05:19:16 +0000 Subject: [PATCH 161/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 7451ff987e..d161b46366 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -126,7 +126,10 @@ def __init__(self, children: list[Item]): def find_open_space(self, item: Item) -> int: for index, weight in enumerate(self.weights): # check if open space AND (next row has no items OR this is the last row) - if (weight + item.width <= 5) and ((index < len(self.weights) - 1 and self.weights[index + 1] == 0) or index == len(self.weights) - 1): + if (weight + item.width <= 5) and ( + (index < len(self.weights) - 1 and self.weights[index + 1] == 0) + or index == len(self.weights) - 1 + ): return index raise ValueError("could not find open space for item") From a7e8f47bcb032c9e1d90f4528b9605090b065254 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 9 May 2025 18:41:20 +0100 Subject: [PATCH 162/228] fixes --- discord/ui/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index d161b46366..6603b71546 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -161,7 +161,7 @@ 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 + return sum(w > 0 for w in self.weights) > 5 or len(self.weights) > 5 def fits_legacy(self, item) -> bool: if item.row is not None: @@ -343,7 +343,7 @@ def add_item(self, item: Item) -> None: or the row the item is trying to be added to is full. """ - if len(self.children) > 40: + if len(self.children) >= 40: raise ValueError("maximum number of children exceeded") if not isinstance(item, Item): From a9d095d0c1eb80bfba5f963a4ef79e639b388afc Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 11 May 2025 20:07:07 +0100 Subject: [PATCH 163/228] view dispatchable --- discord/ui/view.py | 5 +++++ discord/webhook/async_.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 6603b71546..3fd9990284 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -578,6 +578,11 @@ def is_finished(self) -> bool: """Whether the view has finished interacting.""" return self.__stopped.done() + def is_dispatchable(self) -> bool: + return any( + item.is_dispatchable() for item in self.children + ) + def is_dispatching(self) -> bool: """Whether the view has been added for dispatching purposes.""" return self.__cancel_callback is not None diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 5b160ceb5f..b0dd647a25 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1791,9 +1791,9 @@ async def send( with_components = False if view is not MISSING: - if isinstance(self._state, _WebhookState): + if isinstance(self._state, _WebhookState) and view and view.is_dispatchable(): raise InvalidArgument( - "Webhook views require an associated state with the webhook" + "Dispatchable Webhook views require an associated state with the webhook" ) if ephemeral is True and view.timeout is None: view.timeout = 15 * 60.0 From a3d44b6d9cc23290c41f42ff053d1de3584271a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 19:07:35 +0000 Subject: [PATCH 164/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 4 +--- discord/webhook/async_.py | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 3fd9990284..4f30b0835c 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -579,9 +579,7 @@ def is_finished(self) -> bool: return self.__stopped.done() def is_dispatchable(self) -> bool: - return any( - item.is_dispatchable() for item in self.children - ) + return any(item.is_dispatchable() for item in self.children) def is_dispatching(self) -> bool: """Whether the view has been added for dispatching purposes.""" diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index b0dd647a25..780bbb6947 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1791,7 +1791,11 @@ async def send( with_components = False if view is not MISSING: - if isinstance(self._state, _WebhookState) and view and view.is_dispatchable(): + if ( + isinstance(self._state, _WebhookState) + and view + and view.is_dispatchable() + ): raise InvalidArgument( "Dispatchable Webhook views require an associated state with the webhook" ) From cbbb6235052c1c3030e9a4c63bf54784cbb684d8 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 11 May 2025 20:13:28 +0100 Subject: [PATCH 165/228] only store dispatchable views --- discord/abc.py | 3 ++- discord/channel.py | 2 +- discord/interactions.py | 6 ++++-- discord/message.py | 6 ++++-- discord/ui/container.py | 5 +++++ discord/ui/section.py | 3 +++ discord/webhook/async_.py | 6 ++++-- 7 files changed, 23 insertions(+), 8 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 3c3903ece2..bd9b122032 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1671,7 +1671,8 @@ async def send( ret = state.create_message(channel=channel, data=data) if view: - state.store_view(view, ret.id) + if view.dispatchable(): + state.store_view(view, ret.id) view.message = ret if delete_after is not None: diff --git a/discord/channel.py b/discord/channel.py index 687baa5e5a..67e45b044f 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1346,7 +1346,7 @@ async def create_thread( ret = Thread(guild=self.guild, state=self._state, data=data) msg = ret.get_partial_message(int(data["last_message_id"])) - if view: + if view and view.dispatchable():: state.store_view(view, msg.id) if delete_message_after is not None: diff --git a/discord/interactions.py b/discord/interactions.py index 7c0ecaaac6..453516c220 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -571,7 +571,8 @@ async def edit_original_response( message = InteractionMessage(state=state, channel=self.channel, data=data) # type: ignore if view and not view.is_finished(): view.message = message - self._state.store_view(view, message.id) + if view.dispatchable(): + self._state.store_view(view, message.id) if delete_after is not None: await self.delete_original_response(delay=delete_after) @@ -1031,7 +1032,8 @@ async def send_message( view.timeout = 15 * 60.0 view.parent = self._parent - self._parent._state.store_view(view) + if view.dispatchable(): + self._parent._state.store_view(view) self._responded = True if delete_after is not None: diff --git a/discord/message.py b/discord/message.py index dff8ba1ef1..a0f55bb49b 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1643,7 +1643,8 @@ async def edit( if view and not view.is_finished(): view.message = message - self._state.store_view(view, self.id) + if view.dispatchable(): + self._state.store_view(view, self.id) if delete_after is not None: await self.delete(delay=delete_after) @@ -2238,7 +2239,8 @@ async def edit(self, **fields: Any) -> Message | None: msg = self._state.create_message(channel=self.channel, data=data) # type: ignore if view and not view.is_finished(): view.message = msg - self._state.store_view(view, self.id) + if view.dispatchable(): + self._state.store_view(view, self.id) return msg async def end_poll(self) -> Message: diff --git a/discord/ui/container.py b/discord/ui/container.py index 6bc6acf646..ebd4de1b1e 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -334,6 +334,11 @@ def type(self) -> ComponentType: def width(self) -> int: return 5 + def is_dispatchable(self) -> bool: + return any( + item.is_dispatchable() for item in self.items + ) + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ Disables all buttons and select menus in the container. diff --git a/discord/ui/section.py b/discord/ui/section.py index aa771964da..cd49534555 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -234,6 +234,9 @@ def type(self) -> ComponentType: def width(self) -> int: return 5 + def is_dispatchable(self) -> bool: + return self.accessory and self.accessory.is_dispatchable() + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ Disables all buttons and select menus in the section. diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 780bbb6947..73af7348fa 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1850,7 +1850,8 @@ async def send( if view is not MISSING and not view.is_finished(): message_id = None if msg is None else msg.id view.message = None if msg is None else msg - self._state.store_view(view, message_id) + if view.dispatchable(): + self._state.store_view(view, message_id) if delete_after is not None: @@ -2050,7 +2051,8 @@ async def edit_message( message = self._create_message(data) if view and not view.is_finished(): view.message = message - self._state.store_view(view, message_id) + if view.dispatchable(): + self._state.store_view(view, message_id) return message async def delete_message( From 74bf37cba364986f7a266a90ede038b8931c1ade Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 19:13:54 +0000 Subject: [PATCH 166/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index ebd4de1b1e..34e8ec130d 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -335,9 +335,7 @@ def width(self) -> int: return 5 def is_dispatchable(self) -> bool: - return any( - item.is_dispatchable() for item in self.items - ) + return any(item.is_dispatchable() for item in self.items) def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ From f29046ef9319de4192c6ebb22cc69ca832f34bcf Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 11 May 2025 20:15:43 +0100 Subject: [PATCH 167/228] fix --- discord/abc.py | 2 +- discord/channel.py | 2 +- discord/interactions.py | 4 ++-- discord/message.py | 4 ++-- discord/webhook/async_.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index bd9b122032..6397df92c5 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1671,7 +1671,7 @@ async def send( ret = state.create_message(channel=channel, data=data) if view: - if view.dispatchable(): + if view.is_dispatchable(): state.store_view(view, ret.id) view.message = ret diff --git a/discord/channel.py b/discord/channel.py index 67e45b044f..6f15b178ec 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1346,7 +1346,7 @@ async def create_thread( ret = Thread(guild=self.guild, state=self._state, data=data) msg = ret.get_partial_message(int(data["last_message_id"])) - if view and view.dispatchable():: + if view and view.is_dispatchable(): state.store_view(view, msg.id) if delete_message_after is not None: diff --git a/discord/interactions.py b/discord/interactions.py index 453516c220..4754327840 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -571,7 +571,7 @@ async def edit_original_response( message = InteractionMessage(state=state, channel=self.channel, data=data) # type: ignore if view and not view.is_finished(): view.message = message - if view.dispatchable(): + if view.is_dispatchable(): self._state.store_view(view, message.id) if delete_after is not None: @@ -1032,7 +1032,7 @@ async def send_message( view.timeout = 15 * 60.0 view.parent = self._parent - if view.dispatchable(): + if view.is_dispatchable(): self._parent._state.store_view(view) self._responded = True diff --git a/discord/message.py b/discord/message.py index a0f55bb49b..32e86400d9 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1643,7 +1643,7 @@ async def edit( if view and not view.is_finished(): view.message = message - if view.dispatchable(): + if view.is_dispatchable(): self._state.store_view(view, self.id) if delete_after is not None: @@ -2239,7 +2239,7 @@ async def edit(self, **fields: Any) -> Message | None: msg = self._state.create_message(channel=self.channel, data=data) # type: ignore if view and not view.is_finished(): view.message = msg - if view.dispatchable(): + if view.is_dispatchable(): self._state.store_view(view, self.id) return msg diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 73af7348fa..c32336053d 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1850,7 +1850,7 @@ async def send( if view is not MISSING and not view.is_finished(): message_id = None if msg is None else msg.id view.message = None if msg is None else msg - if view.dispatchable(): + if view.is_dispatchable(): self._state.store_view(view, message_id) if delete_after is not None: @@ -2051,7 +2051,7 @@ async def edit_message( message = self._create_message(data) if view and not view.is_finished(): view.message = message - if view.dispatchable(): + if view.is_dispatchable(): self._state.store_view(view, message_id) return message From 3b3384a0d7814de4e1e088b655d2f773a7afea9a Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 16 May 2025 16:39:30 +0100 Subject: [PATCH 168/228] fixes --- discord/ui/button.py | 3 +++ discord/ui/container.py | 2 +- discord/ui/item.py | 3 +++ discord/ui/section.py | 2 +- discord/ui/select.py | 3 +++ discord/ui/view.py | 12 ++++++------ 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/discord/ui/button.py b/discord/ui/button.py index d4ff5e47d4..26c84670ca 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -264,6 +264,9 @@ def to_component_dict(self): def is_dispatchable(self) -> bool: return self.custom_id is not None + def is_storable(self) -> bool: + return self.is_dispatchable() + def is_persistent(self) -> bool: if self.style is ButtonStyle.link: return self.url is not None diff --git a/discord/ui/container.py b/discord/ui/container.py index 34e8ec130d..dc39df86f6 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -289,7 +289,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 if i]) + return "\n".join(t for i in self.items if (t := i.copy_text())) @property def spoiler(self) -> bool: diff --git a/discord/ui/item.py b/discord/ui/item.py index 20757c4ec8..71d69bca21 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -87,6 +87,9 @@ def type(self) -> ComponentType: def is_dispatchable(self) -> bool: return False + def is_storable(self) -> bool: + return False + def is_persistent(self) -> bool: return not self.is_dispatchable() or self._provided_custom_id diff --git a/discord/ui/section.py b/discord/ui/section.py index cd49534555..d85bebf9dd 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -224,7 +224,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 if i) + return "\n".join(t for i in self.items if (t := i.copy_text())) @property def type(self) -> ComponentType: diff --git a/discord/ui/select.py b/discord/ui/select.py index 3bdc4b518a..558b09bba4 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -441,6 +441,9 @@ def type(self) -> ComponentType: def is_dispatchable(self) -> bool: return True + def is_storable(self) -> bool: + return False + _select_types = ( ComponentType.string_select, diff --git a/discord/ui/view.py b/discord/ui/view.py index 4f30b0835c..f9577ca6bd 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -657,7 +657,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 if i) + return "\n".join(t for i in self.children if (t := i.copy_text())) @property def message(self): @@ -699,17 +699,17 @@ def add_view(self, view: View, message_id: int | None = None): view._start_listening_from_store(self) for item in view.children: - if item.is_dispatchable(): + if item.is_storable(): self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore else: if hasattr(item, "items"): for sub_item in item.items: - if sub_item.is_dispatchable(): + if sub_item.is_storable(): 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(): + if sub_item.accessory.is_storable(): self._views[ ( sub_item.accessory.type.value, @@ -718,7 +718,7 @@ def add_view(self, view: View, message_id: int | None = None): ) ] = (view, sub_item.accessory) if hasattr(item, "accessory"): - if item.accessory.is_dispatchable(): + if item.accessory.is_storable(): self._views[ ( item.accessory.type.value, @@ -732,7 +732,7 @@ def add_view(self, view: View, message_id: int | None = None): def remove_view(self, view: View): for item in view.children: - if item.is_dispatchable(): + if item.is_storable(): self._views.pop((item.type.value, item.custom_id), None) # type: ignore for key, value in self._synced_message_views.items(): From f17a9e19335b4765644f4adf64be4a7643e4956c Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 16 May 2025 16:51:56 +0100 Subject: [PATCH 169/228] again --- discord/ui/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/select.py b/discord/ui/select.py index 558b09bba4..bebbbc39da 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -442,7 +442,7 @@ def is_dispatchable(self) -> bool: return True def is_storable(self) -> bool: - return False + return True _select_types = ( From 381edff6c2f5f1f269897b75b1197d2dfb618e7d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 16 May 2025 17:50:36 +0100 Subject: [PATCH 170/228] fix persistance --- discord/ui/container.py | 3 +++ discord/ui/section.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/discord/ui/container.py b/discord/ui/container.py index dc39df86f6..8a1fbb82d6 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -337,6 +337,9 @@ def width(self) -> int: def is_dispatchable(self) -> bool: return any(item.is_dispatchable() for item in self.items) + def is_persistent(self) -> bool: + return all(item.is_persistent() for item in self.items) + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ Disables all buttons and select menus in the container. diff --git a/discord/ui/section.py b/discord/ui/section.py index d85bebf9dd..09511efdd3 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -10,6 +10,7 @@ from .item import Item, ItemCallbackType from .text_display import TextDisplay from .thumbnail import Thumbnail +from .button import Button __all__ = ("Section",) @@ -237,6 +238,11 @@ def width(self) -> int: def is_dispatchable(self) -> bool: return self.accessory and self.accessory.is_dispatchable() + def is_persistent(self) -> bool: + if not isinstance(self.accessory, Button): + return True + return self.accessory.is_persistent() + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ Disables all buttons and select menus in the section. From 4c3d8160cb87b92c244909729484e6f87bea6a34 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 16:53:16 +0000 Subject: [PATCH 171/228] 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 09511efdd3..fea2a137be 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -7,10 +7,10 @@ from ..components import _component_factory from ..enums import ComponentType from ..utils import get +from .button import Button from .item import Item, ItemCallbackType from .text_display import TextDisplay from .thumbnail import Thumbnail -from .button import Button __all__ = ("Section",) From 097a60f9cfb727837bb12ecbfb738875a896c900 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 16 May 2025 22:54:23 +0100 Subject: [PATCH 172/228] Interaction and view error convenience --- discord/bot.py | 4 ++-- discord/client.py | 33 +++++++++++++++++++++++++++++++++ discord/commands/context.py | 24 ++++++++++++++++++------ discord/interactions.py | 18 +++++++++++++++++- discord/ui/modal.py | 6 ++---- discord/ui/view.py | 6 ++---- 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index 0f9b30480c..0731035889 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -879,7 +879,7 @@ async def process_application_commands( ctx = await self.get_application_context(interaction) if command: - ctx.command = command + interaction.command = command await self.invoke_application_command(ctx) async def on_application_command_auto_complete( @@ -887,7 +887,7 @@ async def on_application_command_auto_complete( ) -> None: async def callback() -> None: ctx = await self.get_autocomplete_context(interaction) - ctx.command = command + interaction.command = command return await command.invoke_autocomplete_callback(ctx) autocomplete_task = self._bot.loop.create_task(callback()) diff --git a/discord/client.py b/discord/client.py index 6768d4a660..b085a00c46 100644 --- a/discord/client.py +++ b/discord/client.py @@ -68,9 +68,11 @@ if TYPE_CHECKING: from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime from .channel import DMChannel + from .interaction import Interaction from .member import Member from .message import Message from .poll import Poll + from .ui.item import Item from .voice_client import VoiceProtocol __all__ = ("Client",) @@ -541,6 +543,37 @@ async def on_error(self, event_method: str, *args: Any, **kwargs: Any) -> None: print(f"Ignoring exception in {event_method}", file=sys.stderr) traceback.print_exc() + async def on_view_error( + self, error: Exception, item: Item, interaction: Interaction + ) -> None: + """|coro| + + The default view error handler provided by the client. + + This only fires for a view if you did not define its :func:`~discord.ui.View.on_error`. + """ + + print(f"Ignoring exception in view {self} for item {item}:", file=sys.stderr) + traceback.print_exception( + error.__class__, error, error.__traceback__, file=sys.stderr + ) + + async def on_modal_error( + self, error: Exception, interaction: Interaction + ) -> None: + """|coro| + + The default modal error handler provided by the client. + The default implementation prints the traceback to stderr. + + This only fires for a modal if you did not define its :func:`~discord.ui.Modal.on_error`. + """ + + print(f"Ignoring exception in modal {self}:", file=sys.stderr) + traceback.print_exception( + error.__class__, error, error.__traceback__, file=sys.stderr + ) + # hooks async def _call_before_identify_hook( diff --git a/discord/commands/context.py b/discord/commands/context.py index 532d8abe2a..6c730e3f5c 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -80,8 +80,6 @@ class ApplicationContext(discord.abc.Messageable): The bot that the command belongs to. interaction: :class:`.Interaction` The interaction object that invoked the command. - command: :class:`.ApplicationCommand` - The command that this context belongs to. """ def __init__(self, bot: Bot, interaction: Interaction): @@ -89,7 +87,6 @@ def __init__(self, bot: Bot, interaction: Interaction): self.interaction = interaction # below attributes will be set after initialization - self.command: ApplicationCommand = None # type: ignore self.focused: Option = None # type: ignore self.value: str = None # type: ignore self.options: dict = None # type: ignore @@ -136,6 +133,15 @@ async def invoke( """ return await command(self, *args, **kwargs) + @property + def command(self) -> ApplicationCommand | None: + """The command that this context belongs to.""" + return self.interaction.command + + @command.setter + def command(self, value: ApplicationCommand | None) -> None: + self.interaction.command = value + @cached_property def channel(self) -> InteractionChannel | None: """Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]: @@ -393,8 +399,6 @@ class AutocompleteContext: The bot that the command belongs to. interaction: :class:`.Interaction` The interaction object that invoked the autocomplete. - command: :class:`.ApplicationCommand` - The command that this context belongs to. focused: :class:`.Option` The option the user is currently typing. value: :class:`.str` @@ -409,7 +413,6 @@ def __init__(self, bot: Bot, interaction: Interaction): self.bot = bot self.interaction = interaction - self.command: ApplicationCommand = None # type: ignore self.focused: Option = None # type: ignore self.value: str = None # type: ignore self.options: dict = None # type: ignore @@ -423,3 +426,12 @@ def cog(self) -> Cog | None: return None return self.command.cog + + @property + def command(self) -> ApplicationCommand | None: + """The command that this context belongs to.""" + return self.interaction.command + + @command.setter + def command(self, value: ApplicationCommand | None) -> None: + self.interaction.command = value diff --git a/discord/interactions.py b/discord/interactions.py index 4754327840..36710a96f4 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -75,7 +75,7 @@ VoiceChannel, ) from .client import Client - from .commands import OptionChoice + from .commands import OptionChoice, ApplicationCommand from .embeds import Embed from .mentions import AllowedMentions from .poll import Poll @@ -152,6 +152,18 @@ class Interaction: The context in which this command was executed. .. versionadded:: 2.6 + command: Optional[:class:`ApplicationCommand`] + The command that this interaction belongs to. + + .. versionadded:: 2.7 + view: Optional[:class:`View`] + The view that this interaction belongs to. + + .. versionadded:: 2.7 + modal: Optional[:class:`Modal`] + The modal that this interaction belongs to. + + .. versionadded:: 2.7 """ __slots__: tuple[str, ...] = ( @@ -224,6 +236,10 @@ def _from_data(self, data: InteractionPayload): else None ) + self.command: ApplicationCommand | None = None + self.view: View | None = None + self.modal: Modal | None = None + self.message: Message | None = None self.channel = None diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 97b302860f..4df1eba2d0 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -255,10 +255,7 @@ async def on_error(self, error: Exception, interaction: Interaction) -> None: interaction: :class:`~discord.Interaction` The interaction that led to the failure. """ - print(f"Ignoring exception in modal {self}:", file=sys.stderr) - traceback.print_exception( - error.__class__, error, error.__traceback__, file=sys.stderr - ) + interaction.client.dispatch("modal_error", error, interaction) async def on_timeout(self) -> None: """|coro| @@ -328,6 +325,7 @@ async def dispatch(self, user_id: int, custom_id: str, interaction: Interaction) value = self._modals.get(key) if value is None: return + interaction.modal = value try: components = [ diff --git a/discord/ui/view.py b/discord/ui/view.py index f9577ca6bd..d1301c4f0a 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -486,10 +486,7 @@ async def on_error( interaction: :class:`~discord.Interaction` The interaction that led to the failure. """ - print(f"Ignoring exception in view {self} for item {item}:", file=sys.stderr) - traceback.print_exception( - error.__class__, error, error.__traceback__, file=sys.stderr - ) + interaction.client.dispatch("view_error", error, item, interaction) async def _scheduled_task(self, item: Item, interaction: Interaction): try: @@ -753,6 +750,7 @@ def dispatch(self, component_type: int, custom_id: str, interaction: Interaction return view, item = value + interaction.view = view item.refresh_state(interaction) view._dispatch_item(item, interaction) From 3fead8d273164ef32dc5f2557e6d38d035b39586 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 21:54:49 +0000 Subject: [PATCH 173/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/client.py | 4 +--- discord/interactions.py | 2 +- discord/ui/modal.py | 1 - discord/ui/view.py | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/discord/client.py b/discord/client.py index b085a00c46..92a562da30 100644 --- a/discord/client.py +++ b/discord/client.py @@ -558,9 +558,7 @@ async def on_view_error( error.__class__, error, error.__traceback__, file=sys.stderr ) - async def on_modal_error( - self, error: Exception, interaction: Interaction - ) -> None: + async def on_modal_error(self, error: Exception, interaction: Interaction) -> None: """|coro| The default modal error handler provided by the client. diff --git a/discord/interactions.py b/discord/interactions.py index 36710a96f4..0e1f160acf 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -75,7 +75,7 @@ VoiceChannel, ) from .client import Client - from .commands import OptionChoice, ApplicationCommand + from .commands import ApplicationCommand, OptionChoice from .embeds import Embed from .mentions import AllowedMentions from .poll import Poll diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 4df1eba2d0..6f4bfc9f55 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -4,7 +4,6 @@ import os import sys import time -import traceback from functools import partial from itertools import groupby from typing import TYPE_CHECKING, Any, Callable diff --git a/discord/ui/view.py b/discord/ui/view.py index d1301c4f0a..0a55a96390 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -29,7 +29,6 @@ import os import sys import time -import traceback from functools import partial from itertools import groupby from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterator, Sequence From 08cfdaa4ef2deb47cb81de59ecaa74a62281e2f7 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 16 May 2025 22:58:01 +0100 Subject: [PATCH 174/228] fix --- discord/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index 92a562da30..fa51dbd271 100644 --- a/discord/client.py +++ b/discord/client.py @@ -553,7 +553,7 @@ async def on_view_error( This only fires for a view if you did not define its :func:`~discord.ui.View.on_error`. """ - print(f"Ignoring exception in view {self} for item {item}:", file=sys.stderr) + print(f"Ignoring exception in view {interaction.view} for item {item}:", file=sys.stderr) traceback.print_exception( error.__class__, error, error.__traceback__, file=sys.stderr ) @@ -567,7 +567,7 @@ async def on_modal_error(self, error: Exception, interaction: Interaction) -> No This only fires for a modal if you did not define its :func:`~discord.ui.Modal.on_error`. """ - print(f"Ignoring exception in modal {self}:", file=sys.stderr) + print(f"Ignoring exception in modal {interaction.modal}:", file=sys.stderr) traceback.print_exception( error.__class__, error, error.__traceback__, file=sys.stderr ) From af2c58011c3c350cf9fb40ff4a55169c376e9a26 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 21:58:34 +0000 Subject: [PATCH 175/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index fa51dbd271..2d0f1b8770 100644 --- a/discord/client.py +++ b/discord/client.py @@ -553,7 +553,10 @@ async def on_view_error( This only fires for a view if you did not define its :func:`~discord.ui.View.on_error`. """ - print(f"Ignoring exception in view {interaction.view} for item {item}:", file=sys.stderr) + print( + f"Ignoring exception in view {interaction.view} for item {item}:", + file=sys.stderr, + ) traceback.print_exception( error.__class__, error, error.__traceback__, file=sys.stderr ) From 7244eb289fd1e0da99a35e37848e09cde65dfe19 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 16 May 2025 23:06:16 +0100 Subject: [PATCH 176/228] slots --- discord/commands/context.py | 2 +- discord/interactions.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index 6c730e3f5c..e066bd32bc 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -407,7 +407,7 @@ class AutocompleteContext: A name to value mapping of the options that the user has selected before this option. """ - __slots__ = ("bot", "interaction", "command", "focused", "value", "options") + __slots__ = ("bot", "interaction", "focused", "value", "options") def __init__(self, bot: Bot, interaction: Interaction): self.bot = bot diff --git a/discord/interactions.py b/discord/interactions.py index 0e1f160acf..aa4c7a72b4 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -184,6 +184,9 @@ class Interaction: "entitlements", "context", "authorizing_integration_owners", + "command", + "view", + "modal", "_channel_data", "_message_data", "_guild_data", From 31a9389a03864b603d1db07819956fcf48d82ac3 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 17 May 2025 23:44:33 +0100 Subject: [PATCH 177/228] big refactor for view refreshing --- discord/components.py | 14 +++++++ discord/ext/pages/pagination.py | 9 +++-- discord/ui/container.py | 28 ++++++++----- discord/ui/item.py | 3 +- discord/ui/section.py | 40 +++++++++++++------ discord/ui/view.py | 70 +++++++++++++-------------------- 6 files changed, 97 insertions(+), 67 deletions(-) diff --git a/discord/components.py b/discord/components.py index 61fc6df506..7f2641562f 100644 --- a/discord/components.py +++ b/discord/components.py @@ -606,6 +606,13 @@ def to_dict(self) -> SectionComponentPayload: payload["accessory"] = self.accessory.to_dict() return payload + def walk_components(self) -> Iterator[Component]: + r = self.components + if self.accessory: + r.append(self.accessory) + for c in r: + yield c + class TextDisplay(Component): """Represents a Text Display from Components V2. @@ -937,6 +944,13 @@ def to_dict(self) -> ContainerComponentPayload: payload["spoiler"] = self.spoiler return payload + def walk_components(self) -> Iterator[Component]: + for c in self.components: + if hasattr(c, "walk_components"): + yield from c.walk_components() + else: + yield c + COMPONENT_MAPPINGS = { 1: ActionRow, diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index 4bee02e36e..8dbf1c0f43 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -591,8 +591,9 @@ async def update( async def on_timeout(self) -> None: """Disables all buttons when the view times out.""" if self.disable_on_timeout: - for item in self.children: - item.disabled = True + for item in self.walk_children(): + if hasattr(item, "disabled"): + item.disabled = True page = self.pages[self.current_page] page = self.get_page_content(page) files = page.update_files() @@ -617,12 +618,12 @@ async def disable( The page content to show after disabling the paginator. """ page = self.get_page_content(page) - for item in self.children: + for item in self.walk_children(): if ( include_custom or not self.custom_view or item not in self.custom_view.children - ): + ) and hasattr(item, "disabled"): item.disabled = True if page: await self.message.edit( diff --git a/discord/ui/container.py b/discord/ui/container.py index 8a1fbb82d6..24d56b062b 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -136,6 +136,7 @@ def add_item(self, item: Item) -> None: item._view = self.view if hasattr(item, "items"): item.view = self + item.parent = self self.items.append(item) self._add_component_from_item(item) @@ -165,7 +166,7 @@ def get_item(self, id: str | int) -> Item | None: Parameters ---------- - id: :class:`str` + id: Union[:class:`str`, :class:`int`] The id or custom_id of the item to get. Returns @@ -176,12 +177,7 @@ def get_item(self, id: str | int) -> Item | None: if not id: return None 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 + child = find(lambda i: getattr(i, attr, None) == id, list(self.walk_items())) return child def add_section( @@ -322,6 +318,7 @@ def colour(self, value: int | Colour | None): # type: ignore def view(self, value): self._view = value for item in self.items: + item.parent = self item._view = value if hasattr(item, "items"): item.view = value @@ -340,6 +337,11 @@ def is_dispatchable(self) -> bool: def is_persistent(self) -> bool: return all(item.is_persistent() for item in self.items) + def refresh_component(self, component: ContainerComponent) -> None: + self._underlying = component + for x, y in zip(self.items, component.items): + x.refresh_component(y) + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ Disables all buttons and select menus in the container. @@ -349,11 +351,12 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: exclusions: Optional[List[:class:`Item`]] A list of items in `self.items` to not disable from the view. """ - for item in self.items: + for item in self.walk_items(): if hasattr(item, "disabled") and ( exclusions is None or item not in exclusions ): item.disabled = True + return self def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ @@ -364,13 +367,20 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: exclusions: Optional[List[:class:`Item`]] A list of items in `self.items` to not enable from the view. """ - for item in self.items: + for item in self.walk_items(): if hasattr(item, "disabled") and ( exclusions is None or item not in exclusions ): item.disabled = False return self + def walk_items(self) -> Iterator[Item]: + for item in self.items: + if hasattr(item, "walk_items"): + yield from item.walk_items() + else: + yield item + def to_component_dict(self) -> ContainerComponentPayload: self._set_components(self.items) return self._underlying.to_dict() diff --git a/discord/ui/item.py b/discord/ui/item.py index 71d69bca21..2daf2dad52 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -66,12 +66,13 @@ def __init__(self): # actually affect the intended purpose of this check because from_component is # only called upon edit and we're mainly interested during initial creation time. self._provided_custom_id: bool = False + self.parent: Item | View | None = self.view def to_component_dict(self) -> dict[str, Any]: raise NotImplementedError def refresh_component(self, component: Component) -> None: - return None + self._underlying = component def refresh_state(self, interaction: Interaction) -> None: return None diff --git a/discord/ui/section.py b/discord/ui/section.py index fea2a137be..be9ee965ea 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -103,6 +103,7 @@ def add_item(self, item: Item) -> None: if not isinstance(item, Item): raise TypeError(f"expected Item not {item.__class__!r}") + item.parent = self self.items.append(item) self._add_component_from_item(item) return self @@ -124,13 +125,14 @@ def remove_item(self, item: Item | int) -> None: 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)`. + def get_item(self, id: int | str) -> Item | None: + """Get an item from this section. Alias for `utils.get(section.walk_items(), ...)`. + If an ``int`` is provided, it will be retrieved by ``id``, otherwise it will check the accessory's ``custom_id``. Parameters ---------- - id: :class:`int` - The id of the item to get + id: Union[:class:`str`, :class:`int`] + The id or custom_id of the item to get. Returns ------- @@ -139,9 +141,9 @@ def get_item(self, id: int) -> Item | None: """ if not id: return None - if self.accessory and self.accessory.id == id: - return self.accessory - return get(self.items, id=id) + attr = "id" if isinstance(id, int) else "custom_id" + child = find(lambda i: getattr(i, attr, None) == id, list(self.walk_items())) + return child def add_text(self, content: str, *, id: int | None = None) -> None: """Adds a :class:`TextDisplay` to the section. @@ -185,6 +187,7 @@ def set_accessory(self, item: Item) -> None: raise TypeError(f"expected Item not {item.__class__!r}") if self.view: item._view = self.view + item.parent = self self.accessory = item self._underlying.accessory = item._underlying @@ -220,8 +223,9 @@ def set_thumbnail( @Item.view.setter def view(self, value): self._view = value - if self.accessory: - self.accessory._view = value + for item in self.walk_items(): + item._view = value + item.parent = self 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.""" @@ -243,6 +247,13 @@ def is_persistent(self) -> bool: return True return self.accessory.is_persistent() + def refresh_component(self, component: SectionComponent) -> None: + self._underlying = component + for x, y in zip(self.items, component.items): + x.refresh_component(y) + if self.accessory and component.accessory: + self.accessory.refresh_component(component.accessory) + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ Disables all buttons and select menus in the section. @@ -253,7 +264,7 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: 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]: + for item in self.walk_items(): if hasattr(item, "disabled") and ( exclusions is None or item not in exclusions ): @@ -270,13 +281,20 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: 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]: + for item in self.walk_items(): if hasattr(item, "disabled") and ( exclusions is None or item not in exclusions ): item.disabled = False return self + def walk_items(self) -> Iterator[Item]: + r = self.items + if self.accessory: + r.append(self.accessory) + for item in r: + yield item + 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 0a55a96390..69858ab8bf 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -65,6 +65,15 @@ def _walk_all_components(components: list[Component]) -> Iterator[Component]: else: yield item +def _walk_all_components_v2(components: list[Component]) -> Iterator[Component]: + for item in components: + if isinstance(item, ActionRowComponent): + yield from item.children + elif isinstance(item, (SectionComponent, ContainerComponent)): + yield from item.walk_components() + else: + yield item + def _component_to_item(component: Component) -> Item: if isinstance(component, ButtonComponent): @@ -230,6 +239,7 @@ def __init__( ) item.callback = partial(func, self, item) item._view = self + item.parent = self setattr(self, func.__name__, item) self.children.append(item) @@ -350,6 +360,7 @@ def add_item(self, item: Item) -> None: self.__weights.add_item(item) + item.parent = self item._view = self if hasattr(item, "items"): item.view = self @@ -399,12 +410,7 @@ def get_item(self, custom_id: str | int) -> Item | None: if not custom_id: return None 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 + child = find(lambda i: getattr(i, attr, None) == custom_id, list(self.walk_children())) return child async def interaction_check(self, interaction: Interaction) -> bool: @@ -532,20 +538,17 @@ def _dispatch_item(self, item: Item, interaction: Interaction): ) def refresh(self, components: list[Component]): - # This is pretty hacky at the moment + # Refreshes view using discord's values old_state: dict[tuple[int, str], Item] = { - (item.type.value, item.custom_id): item for item in self.children if item.is_dispatchable() # type: ignore + (item.type.value, item.custom_id): item for item in self.walk_children() if item.is_storable() # type: ignore } - children: list[Item] = [ - item for item in self.children if not item.is_dispatchable() - ] - for component in _walk_all_components(components): + children: list[Item] = [] + + for component in _walk_all_components_v2(components): try: older = old_state[(component.type.value, component.custom_id)] # type: ignore except (KeyError, AttributeError): item = _component_to_item(component) - if not item.is_dispatchable(): - continue children.append(item) else: older.refresh_component(component) @@ -651,6 +654,13 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: child.enable_all_items(exclusions=exclusions) return self + def walk_children(self) -> Iterator[Item]: + for item in self.children: + if hasattr(item, "walk_items"): + yield from item.walk_items() + else: + yield item + 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(t for i in self.children if (t := i.copy_text())) @@ -694,40 +704,15 @@ def add_view(self, view: View, message_id: int | None = None): self.__verify_integrity() view._start_listening_from_store(self) - for item in view.children: + for item in view.walk_children(): if item.is_storable(): self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore - else: - if hasattr(item, "items"): - for sub_item in item.items: - if sub_item.is_storable(): - 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_storable(): - 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_storable(): - 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 def remove_view(self, view: View): - for item in view.children: + for item in view.walk_children(): if item.is_storable(): self._views.pop((item.type.value, item.custom_id), None) # type: ignore @@ -762,4 +747,5 @@ 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, state=self._state) for d in components]) + components = [_component_factory(d, state=self._state) for d in components] + view.refresh(components) From 93b4b6d6b1adad3f556cb110fcd48f3a5a694f2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 22:45:52 +0000 Subject: [PATCH 178/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 3 +-- discord/ui/section.py | 3 +-- discord/ui/view.py | 7 +++++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/discord/components.py b/discord/components.py index 7f2641562f..694f02fd08 100644 --- a/discord/components.py +++ b/discord/components.py @@ -610,8 +610,7 @@ def walk_components(self) -> Iterator[Component]: r = self.components if self.accessory: r.append(self.accessory) - for c in r: - yield c + yield from r class TextDisplay(Component): diff --git a/discord/ui/section.py b/discord/ui/section.py index be9ee965ea..eff2edd5f4 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -292,8 +292,7 @@ def walk_items(self) -> Iterator[Item]: r = self.items if self.accessory: r.append(self.accessory) - for item in r: - yield item + yield from r def to_component_dict(self) -> SectionComponentPayload: self._set_components(self.items) diff --git a/discord/ui/view.py b/discord/ui/view.py index 69858ab8bf..e4ce1df190 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -65,6 +65,7 @@ def _walk_all_components(components: list[Component]) -> Iterator[Component]: else: yield item + def _walk_all_components_v2(components: list[Component]) -> Iterator[Component]: for item in components: if isinstance(item, ActionRowComponent): @@ -410,7 +411,9 @@ def get_item(self, custom_id: str | int) -> Item | None: if not custom_id: return None attr = "id" if isinstance(custom_id, int) else "custom_id" - child = find(lambda i: getattr(i, attr, None) == custom_id, list(self.walk_children())) + child = find( + lambda i: getattr(i, attr, None) == custom_id, list(self.walk_children()) + ) return child async def interaction_check(self, interaction: Interaction) -> bool: @@ -540,7 +543,7 @@ def _dispatch_item(self, item: Item, interaction: Interaction): def refresh(self, components: list[Component]): # Refreshes view using discord's values old_state: dict[tuple[int, str], Item] = { - (item.type.value, item.custom_id): item for item in self.walk_children() if item.is_storable() # type: ignore + (item.type.value, item.custom_id): item for item in self.walk_children() if item.is_storable() # type: ignore } children: list[Item] = [] From 97975e58a6bf516ee54677aad72f04d1d2203ae7 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sat, 17 May 2025 23:48:32 +0100 Subject: [PATCH 179/228] , Iterator --- discord/components.py | 2 +- discord/ui/container.py | 2 +- discord/ui/section.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/discord/components.py b/discord/components.py index 694f02fd08..36b679128b 100644 --- a/discord/components.py +++ b/discord/components.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Iterator from .asset import AssetMixin from .colour import Colour diff --git a/discord/ui/container.py b/discord/ui/container.py index 24d56b062b..c082392ec3 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, ClassVar, TypeVar +from typing import TYPE_CHECKING, ClassVar, TypeVar, Iterator from ..colour import Colour from ..components import ActionRow diff --git a/discord/ui/section.py b/discord/ui/section.py index eff2edd5f4..fc1574359f 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,12 +1,12 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, ClassVar, TypeVar +from typing import TYPE_CHECKING, ClassVar, TypeVar, Iterator from ..components import Section as SectionComponent from ..components import _component_factory from ..enums import ComponentType -from ..utils import get +from ..utils import get, find from .button import Button from .item import Item, ItemCallbackType from .text_display import TextDisplay @@ -292,7 +292,7 @@ def walk_items(self) -> Iterator[Item]: r = self.items if self.accessory: r.append(self.accessory) - yield from r + yield from r def to_component_dict(self) -> SectionComponentPayload: self._set_components(self.items) From 68502d184905aaa7bb96c3663a4b00f7ca18ca52 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 22:48:57 +0000 Subject: [PATCH 180/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 2 +- discord/ui/container.py | 2 +- discord/ui/section.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/components.py b/discord/components.py index 36b679128b..eb700218bb 100644 --- a/discord/components.py +++ b/discord/components.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Iterator +from typing import TYPE_CHECKING, Any, ClassVar, Iterator, TypeVar from .asset import AssetMixin from .colour import Colour diff --git a/discord/ui/container.py b/discord/ui/container.py index c082392ec3..86449f1b80 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, ClassVar, TypeVar, Iterator +from typing import TYPE_CHECKING, ClassVar, Iterator, TypeVar from ..colour import Colour from ..components import ActionRow diff --git a/discord/ui/section.py b/discord/ui/section.py index fc1574359f..d6b087f031 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -1,12 +1,12 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, ClassVar, TypeVar, Iterator +from typing import TYPE_CHECKING, ClassVar, Iterator, TypeVar from ..components import Section as SectionComponent from ..components import _component_factory from ..enums import ComponentType -from ..utils import get, find +from ..utils import find, get from .button import Button from .item import Item, ItemCallbackType from .text_display import TextDisplay From 23758f733632febd30db0945fdf8dff9530c3780 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 00:41:07 +0100 Subject: [PATCH 181/228] again... --- discord/components.py | 3 +++ discord/ui/container.py | 11 ++++++++++- discord/ui/view.py | 34 +++++++++++++++++----------------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/discord/components.py b/discord/components.py index eb700218bb..669c7c9bdd 100644 --- a/discord/components.py +++ b/discord/components.py @@ -184,6 +184,9 @@ def to_dict(self) -> ActionRowPayload: "components": [child.to_dict() for child in self.children], } # type: ignore + def walk_components(self) -> Iterator[Component]: + yield from self.components + @classmethod def with_components(cls, *components, id=None): return cls._raw_construct( diff --git a/discord/ui/container.py b/discord/ui/container.py index 86449f1b80..6e1efa7409 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -339,8 +339,17 @@ def is_persistent(self) -> bool: def refresh_component(self, component: ContainerComponent) -> None: self._underlying = component - for x, y in zip(self.items, component.items): + i = 0 + flattened = [] + for c in component.items: + if isinstance(c, ActionRow): + flattened += c.children + else: + flatten.append(c) + for y in flattened: + x = self.items[i] x.refresh_component(y) + i += 1 def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: """ diff --git a/discord/ui/view.py b/discord/ui/view.py index e4ce1df190..a2c962fa28 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -363,7 +363,7 @@ def add_item(self, item: Item) -> None: item.parent = self item._view = self - if hasattr(item, "items"): + The item, item ``id``, or item ``custom_id`` to remove from the view. item.view = self self.children.append(item) return self @@ -541,23 +541,23 @@ def _dispatch_item(self, item: Item, interaction: Interaction): ) def refresh(self, components: list[Component]): - # Refreshes view using discord's values - old_state: dict[tuple[int, str], Item] = { - (item.type.value, item.custom_id): item for item in self.walk_children() if item.is_storable() # type: ignore - } - children: list[Item] = [] - - for component in _walk_all_components_v2(components): - try: - older = old_state[(component.type.value, component.custom_id)] # type: ignore - except (KeyError, AttributeError): - item = _component_to_item(component) - children.append(item) + # Refreshes view data using discord's values + # Assumes the components and items are identical + + i = 0 + flattened = [] + for c in components: + if isinstance(c, ActionRow): + flattened += c.children else: - older.refresh_component(component) - children.append(older) - - self.children = children + flatten.append(c) + for c in flattened: + try: + item = self.children[i] + except: + break + item.refresh_component(c) + i += 1 def stop(self) -> None: """Stops listening to interaction events from this view. From 36d625b68091f6e6869291f2a6a4b22ee7d76e36 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 00:43:46 +0100 Subject: [PATCH 182/228] ?????? --- discord/ui/container.py | 2 +- discord/ui/view.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 6e1efa7409..7e94dd0df3 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -345,7 +345,7 @@ def refresh_component(self, component: ContainerComponent) -> None: if isinstance(c, ActionRow): flattened += c.children else: - flatten.append(c) + flattened.append(c) for y in flattened: x = self.items[i] x.refresh_component(y) diff --git a/discord/ui/view.py b/discord/ui/view.py index a2c962fa28..dc94639bd0 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -363,7 +363,7 @@ def add_item(self, item: Item) -> None: item.parent = self item._view = self - The item, item ``id``, or item ``custom_id`` to remove from the view. + if hasattr(item, "items"): item.view = self self.children.append(item) return self @@ -550,7 +550,7 @@ def refresh(self, components: list[Component]): if isinstance(c, ActionRow): flattened += c.children else: - flatten.append(c) + flattened.append(c) for c in flattened: try: item = self.children[i] From 3daa1d5a8bbf4125eb2a699711daaa12b19b92ce Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 00:44:56 +0100 Subject: [PATCH 183/228] ActionRowComponent --- 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 dc94639bd0..e18321c007 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -547,7 +547,7 @@ def refresh(self, components: list[Component]): i = 0 flattened = [] for c in components: - if isinstance(c, ActionRow): + if isinstance(c, ActionRowComponent): flattened += c.children else: flattened.append(c) From d202a84930f4b88c356ccf2c0be987e9261e6b57 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 00:48:01 +0100 Subject: [PATCH 184/228] yeild --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 669c7c9bdd..6fae64408a 100644 --- a/discord/components.py +++ b/discord/components.py @@ -185,7 +185,7 @@ def to_dict(self) -> ActionRowPayload: } # type: ignore def walk_components(self) -> Iterator[Component]: - yield from self.components + yield from self.children @classmethod def with_components(cls, *components, id=None): From 399896b0b0b7cfeff3df2fdb29bf336e98d7ef92 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 00:49:37 +0100 Subject: [PATCH 185/228] component.components --- 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 7e94dd0df3..f3d30bc9f5 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -341,7 +341,7 @@ def refresh_component(self, component: ContainerComponent) -> None: self._underlying = component i = 0 flattened = [] - for c in component.items: + for c in component.components: if isinstance(c, ActionRow): flattened += c.children else: From 440fabe3617c32348c155f166d2fa7fa4c533a51 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 01:24:02 +0100 Subject: [PATCH 186/228] else --- 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 e18321c007..5d55eb8a20 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -556,8 +556,9 @@ def refresh(self, components: list[Component]): item = self.children[i] except: break - item.refresh_component(c) - i += 1 + else: + item.refresh_component(c) + i += 1 def stop(self) -> None: """Stops listening to interaction events from this view. From 84c6df86018e51e3901c542fca108ccdeab7762d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 01:33:00 +0100 Subject: [PATCH 187/228] .refresh --- discord/abc.py | 1 + discord/interactions.py | 1 + discord/message.py | 2 ++ discord/ui/view.py | 2 ++ discord/webhook/async_.py | 3 +++ 5 files changed, 9 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index 590b547f9d..e772ebe207 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1684,6 +1684,7 @@ async def send( if view.is_dispatchable(): state.store_view(view, ret.id) view.message = ret + view.refresh(ret.components) if delete_after is not None: await ret.delete(delay=delete_after) diff --git a/discord/interactions.py b/discord/interactions.py index aa4c7a72b4..b39ad7deb8 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -590,6 +590,7 @@ async def edit_original_response( message = InteractionMessage(state=state, channel=self.channel, data=data) # type: ignore if view and not view.is_finished(): view.message = message + view.refresh(message.components) if view.is_dispatchable(): self._state.store_view(view, message.id) diff --git a/discord/message.py b/discord/message.py index f869b43521..4458844a26 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1807,6 +1807,7 @@ async def edit( if view and not view.is_finished(): view.message = message + view.refresh(message.components) if view.is_dispatchable(): self._state.store_view(view, self.id) @@ -2449,6 +2450,7 @@ async def edit(self, **fields: Any) -> Message | None: msg = self._state.create_message(channel=self.channel, data=data) # type: ignore if view and not view.is_finished(): view.message = msg + view.refresh(msg.components) if view.is_dispatchable(): self._state.store_view(view, self.id) return msg diff --git a/discord/ui/view.py b/discord/ui/view.py index 5d55eb8a20..4d99789fe9 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -543,6 +543,8 @@ def _dispatch_item(self, item: Item, interaction: Interaction): def refresh(self, components: list[Component]): # Refreshes view data using discord's values # Assumes the components and items are identical + if not components: + return i = 0 flattened = [] diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index c32336053d..8c24a78007 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1850,6 +1850,8 @@ async def send( if view is not MISSING and not view.is_finished(): message_id = None if msg is None else msg.id view.message = None if msg is None else msg + if msg: + view.refresh(msg.components) if view.is_dispatchable(): self._state.store_view(view, message_id) @@ -2051,6 +2053,7 @@ async def edit_message( message = self._create_message(data) if view and not view.is_finished(): view.message = message + view.refresh(message.components) if view.is_dispatchable(): self._state.store_view(view, message_id) return message From 518a3f264bef61741c35f088fb6c5b976e653bf8 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 07:09:50 +0100 Subject: [PATCH 188/228] 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 d6b087f031..f3dd3539de 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -249,7 +249,7 @@ def is_persistent(self) -> bool: def refresh_component(self, component: SectionComponent) -> None: self._underlying = component - for x, y in zip(self.items, component.items): + for x, y in zip(self.items, component.components): x.refresh_component(y) if self.accessory and component.accessory: self.accessory.refresh_component(component.accessory) From 732722b3018798ba4c29176a8605b0e98ee77bbe Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 11:14:09 +0100 Subject: [PATCH 189/228] from_dict --- discord/ui/view.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/discord/ui/view.py b/discord/ui/view.py index 4d99789fe9..bc21a342c1 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -330,6 +330,31 @@ def from_message( view.add_item(_component_to_item(component)) return view + @classmethod + def from_dict( + cls, data: list[Component], /, *, timeout: float | None = 180.0 + ) -> View: + """Converts a list of component dicts into a :class:`View`. + + Parameters + ---------- + data: List[:class:`.Component`] + The list of components to convert into a view. + timeout: Optional[:class:`float`] + The timeout of the converted view. + + Returns + ------- + :class:`View` + The converted view. This always returns a :class:`View` and not + one of its subclasses. + """ + view = View(timeout=timeout) + components = [_component_factory(d, state=self._state) for d in data] + for component in _walk_all_components(components): + view.add_item(_component_to_item(component)) + return view + @property def _expires_at(self) -> float | None: if self.timeout: From 7d11ddad013f593afa8c8d5641ad38e33b4c32ec Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 11:20:58 +0100 Subject: [PATCH 190/228] state --- discord/ui/view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index bc21a342c1..cfc66a15a1 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -332,7 +332,7 @@ def from_message( @classmethod def from_dict( - cls, data: list[Component], /, *, timeout: float | None = 180.0 + cls, data: list[Component], /, *, timeout: float | None = 180.0, state: ConnectionState | None = None ) -> View: """Converts a list of component dicts into a :class:`View`. @@ -342,6 +342,8 @@ def from_dict( The list of components to convert into a view. timeout: Optional[:class:`float`] The timeout of the converted view. + state: Optional[:class:`ConnectionState`] + The state the view should use. This may be required by certain components, and is typically found under ``Client._connection``. Returns ------- @@ -350,7 +352,7 @@ def from_dict( one of its subclasses. """ view = View(timeout=timeout) - components = [_component_factory(d, state=self._state) for d in data] + components = [_component_factory(d, state=state) for d in data] for component in _walk_all_components(components): view.add_item(_component_to_item(component)) return view From 6e4b17ea9389128cc2334fcc549cc34893fb67a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 10:21:24 +0000 Subject: [PATCH 191/228] style(pre-commit): auto fixes from pre-commit.com hooks --- 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 cfc66a15a1..44ca124fe8 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -332,7 +332,12 @@ def from_message( @classmethod def from_dict( - cls, data: list[Component], /, *, timeout: float | None = 180.0, state: ConnectionState | None = None + cls, + data: list[Component], + /, + *, + timeout: float | None = 180.0, + state: ConnectionState | None = None, ) -> View: """Converts a list of component dicts into a :class:`View`. From 0cf1ebb1d0c4658e8992e5ae6925b076974595b0 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 11:24:30 +0100 Subject: [PATCH 192/228] emoji --- discord/components.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/discord/components.py b/discord/components.py index 6fae64408a..1750aa6fcd 100644 --- a/discord/components.py +++ b/discord/components.py @@ -327,9 +327,9 @@ def __init__(self, data: ButtonComponentPayload): self.disabled: bool = data.get("disabled", False) self.label: str | None = data.get("label") self.emoji: PartialEmoji | None - try: - self.emoji = PartialEmoji.from_dict(data["emoji"]) - except KeyError: + if e := data.get("emoji"): + self.emoji = PartialEmoji.from_dict(e) + else: self.emoji = None self.sku_id: str | None = data.get("sku_id") @@ -538,9 +538,9 @@ def emoji(self, value) -> None: @classmethod def from_dict(cls, data: SelectOptionPayload) -> SelectOption: - try: - emoji = PartialEmoji.from_dict(data["emoji"]) - except KeyError: + if e := data.get("emoji"): + emoji = PartialEmoji.from_dict(e) + else: emoji = None return cls( From 874f93ec7cdad88935d303d773314c65894055d1 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 12:30:13 +0100 Subject: [PATCH 193/228] no state --- discord/ui/view.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 44ca124fe8..018f39eb17 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -337,7 +337,6 @@ def from_dict( /, *, timeout: float | None = 180.0, - state: ConnectionState | None = None, ) -> View: """Converts a list of component dicts into a :class:`View`. @@ -347,8 +346,6 @@ def from_dict( The list of components to convert into a view. timeout: Optional[:class:`float`] The timeout of the converted view. - state: Optional[:class:`ConnectionState`] - The state the view should use. This may be required by certain components, and is typically found under ``Client._connection``. Returns ------- @@ -357,7 +354,7 @@ def from_dict( one of its subclasses. """ view = View(timeout=timeout) - components = [_component_factory(d, state=state) for d in data] + components = [_component_factory(d) for d in data] for component in _walk_all_components(components): view.add_item(_component_to_item(component)) return view From 2abab06b26d4cafe1171acf8befcad279757eb91 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 12:42:15 +0100 Subject: [PATCH 194/228] restore old get_item --- discord/ui/container.py | 7 ++++++- discord/ui/section.py | 4 +++- discord/ui/view.py | 9 ++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index f3d30bc9f5..6bded47d30 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -177,7 +177,12 @@ def get_item(self, id: str | int) -> Item | None: if not id: return None attr = "id" if isinstance(id, int) else "custom_id" - child = find(lambda i: getattr(i, attr, None) == id, list(self.walk_items())) + 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( diff --git a/discord/ui/section.py b/discord/ui/section.py index f3dd3539de..8a1e63373f 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -142,7 +142,9 @@ def get_item(self, id: int | str) -> Item | None: if not id: return None attr = "id" if isinstance(id, int) else "custom_id" - child = find(lambda i: getattr(i, attr, None) == id, list(self.walk_items())) + if self.accessory and id == getattr(self.accessory, attr, None): + return self.accessory + child = find(lambda i: getattr(i, attr, None) == id, self.items) return child def add_text(self, content: str, *, id: int | None = None) -> None: diff --git a/discord/ui/view.py b/discord/ui/view.py index 018f39eb17..2ba93a9d86 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -440,9 +440,12 @@ def get_item(self, custom_id: str | int) -> Item | None: if not custom_id: return None attr = "id" if isinstance(custom_id, int) else "custom_id" - child = find( - lambda i: getattr(i, attr, None) == custom_id, list(self.walk_children()) - ) + 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: From b3a3180cb57afa9842d5e114ac70003c1c212382 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 12:48:23 +0100 Subject: [PATCH 195/228] separator id --- discord/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 1750aa6fcd..a7d680dab7 100644 --- a/discord/components.py +++ b/discord/components.py @@ -883,7 +883,7 @@ class Separator(Component): def __init__(self, data: SeparatorComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) - self.id: int = None + self.id: int = data.get("id") self.divider: bool = data.get("divider") self.spacing: SeparatorSpacingSize = try_enum( SeparatorSpacingSize, data.get("spacing", 1) From 6b34a29c5b67a5e420e3a2e03d101ee1ff467616 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 22:09:35 +0100 Subject: [PATCH 196/228] eh --- 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 8a1e63373f..b8b9cedcc4 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -298,7 +298,8 @@ def walk_items(self) -> Iterator[Item]: def to_component_dict(self) -> SectionComponentPayload: self._set_components(self.items) - self.set_accessory(self.accessory) + if self.accessory: + self.set_accessory(self.accessory) return self._underlying.to_dict() @classmethod From 498859e3d252f8c13d3a6be87442ab0e64515561 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 18 May 2025 22:17:44 +0100 Subject: [PATCH 197/228] fix yield --- discord/components.py | 2 +- discord/ui/section.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index a7d680dab7..d9ded85855 100644 --- a/discord/components.py +++ b/discord/components.py @@ -612,7 +612,7 @@ def to_dict(self) -> SectionComponentPayload: def walk_components(self) -> Iterator[Component]: r = self.components if self.accessory: - r.append(self.accessory) + yield from r + [self.accessory] yield from r diff --git a/discord/ui/section.py b/discord/ui/section.py index b8b9cedcc4..87fdfd71a5 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -293,7 +293,7 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: def walk_items(self) -> Iterator[Item]: r = self.items if self.accessory: - r.append(self.accessory) + yield from r + [self.accessory] yield from r def to_component_dict(self) -> SectionComponentPayload: From a653917966f3fc2faa3e6abd6238f09c46014e61 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Mon, 19 May 2025 21:54:20 +0100 Subject: [PATCH 198/228] forumchannel support --- discord/channel.py | 37 +++++++++++++++++++++++++------------ discord/http.py | 4 ++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index e492d36c85..10c22b86b1 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -45,7 +45,7 @@ ) from .errors import ClientException, InvalidArgument from .file import File -from .flags import ChannelFlags +from .flags import ChannelFlags, MessageFlags from .invite import Invite from .iterators import ArchivedThreadIterator from .mixins import Hashable @@ -71,12 +71,15 @@ if TYPE_CHECKING: from .abc import Snowflake, SnowflakeTime + from .embeds import Embed from .guild import Guild from .guild import GuildChannel as GuildChannelType from .member import Member, VoiceState + from .mentions import AllowedMentions from .message import EmojiInputType, Message, PartialMessage from .role import Role from .state import ConnectionState + from .sticker import GuildSticker, StickerItem from .types.channel import CategoryChannel as CategoryChannelPayload from .types.channel import DMChannel as DMChannelPayload from .types.channel import ForumChannel as ForumChannelPayload @@ -1181,18 +1184,20 @@ async def edit(self, *, reason=None, **options): async def create_thread( self, name: str, - content=None, + content: str | None = None, *, - embed=None, - embeds=None, - file=None, - files=None, - stickers=None, - delete_message_after=None, - nonce=None, - allowed_mentions=None, - view=None, - applied_tags=None, + embed: Embed | None = None, + embeds: list[Embed] | None = None, + file: File | None = None, + files: list[File] | None = None, + stickers: Sequence[GuildSticker | StickerItem] | None = None, + delete_message_after: float | None = None, + nonce: int | str | None = None, + allowed_mentions: AllowedMentions | None = None, + view: View | None = None, + applied_tags: list[ForumTag] | None = None, + suppress: bool = False, + silent: bool = False, auto_archive_duration: ThreadArchiveDuration = MISSING, slowmode_delay: int = MISSING, reason: str | None = None, @@ -1292,6 +1297,11 @@ async def create_thread( else: allowed_mentions = allowed_mentions.to_dict() + flags = MessageFlags( + suppress_embeds=bool(suppress), + suppress_notifications=bool(silent), + ) + if view: if not hasattr(view, "__discord_ui_view__"): raise InvalidArgument( @@ -1299,6 +1309,8 @@ async def create_thread( ) components = view.to_components() + if view.is_components_v2(): + flags.is_components_v2 = True else: components = None @@ -1337,6 +1349,7 @@ async def create_thread( or self.default_auto_archive_duration, rate_limit_per_user=slowmode_delay or self.slowmode_delay, applied_tags=applied_tags, + flags=flags.value, reason=reason, ) finally: diff --git a/discord/http.py b/discord/http.py index 2db704b268..a6eec1a82a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1228,6 +1228,7 @@ def start_forum_thread( allowed_mentions: message.AllowedMentions | None = None, stickers: list[sticker.StickerItem] | None = None, components: list[components.Component] | None = None, + flags: int | None = None, ) -> Response[threads.Thread]: payload: dict[str, Any] = { "name": name, @@ -1264,6 +1265,9 @@ def start_forum_thread( if stickers: message["sticker_ids"] = stickers + if flags: + message["flags"] = flags + if message != {}: payload["message"] = message From 101aec796096a386ab705ad51600491c715513c8 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Mon, 19 May 2025 21:57:50 +0100 Subject: [PATCH 199/228] typing --- discord/channel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/channel.py b/discord/channel.py index 10c22b86b1..1b0be2015a 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -26,7 +26,7 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, TypeVar, overload +from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, TypeVar, overload, Sequence import discord.abc @@ -90,6 +90,7 @@ from .types.channel import VoiceChannel as VoiceChannelPayload from .types.snowflake import SnowflakeList from .types.threads import ThreadArchiveDuration + from .ui.view import View from .user import BaseUser, ClientUser, User from .webhook import Webhook From ecda5c4d532492eddb5108515968f8c19098c1be Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 20:58:15 +0000 Subject: [PATCH 200/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/channel.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/discord/channel.py b/discord/channel.py index 1b0be2015a..aa5045c25c 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -26,7 +26,16 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, TypeVar, overload, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterable, + Mapping, + Sequence, + TypeVar, + overload, +) import discord.abc From ade5c6304e5122f91b50eadae386d62f132c622d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 20 May 2025 13:18:09 +0100 Subject: [PATCH 201/228] disable --- discord/ui/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 2ba93a9d86..9fe9c26345 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -670,7 +670,7 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: exclusions is None or child not in exclusions ): child.disabled = True - if hasattr(child, "items"): + if hasattr(child, "disable_all_items"): child.disable_all_items(exclusions=exclusions) return self @@ -688,7 +688,7 @@ def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: exclusions is None or child not in exclusions ): child.disabled = False - if hasattr(child, "items"): + if hasattr(child, "enable_all_items"): child.enable_all_items(exclusions=exclusions) return self From 2eb62e998b9e1a85c9fce72223a292d8f7384e73 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 20 May 2025 13:18:37 +0100 Subject: [PATCH 202/228] Update discord/ui/section.py Co-authored-by: Paillat Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- 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 87fdfd71a5..fc4189a7af 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -294,7 +294,8 @@ def walk_items(self) -> Iterator[Item]: r = self.items if self.accessory: yield from r + [self.accessory] - yield from r + else: + yield from r def to_component_dict(self) -> SectionComponentPayload: self._set_components(self.items) From 61fad2a8af291cb3fecf9f34d81d00ac30899249 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 01:57:59 +0000 Subject: [PATCH 203/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 1d4aaeeb6c..94ddb0bb51 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -68,7 +68,6 @@ def _walk_all_components(components: list[Component]) -> Iterator[Component]: yield item - def _walk_all_components_v2(components: list[Component]) -> Iterator[Component]: for item in components: if isinstance(item, ActionRowComponent): @@ -77,8 +76,8 @@ def _walk_all_components_v2(components: list[Component]) -> Iterator[Component]: yield from item.walk_components() else: yield item - - + + def _component_to_item(component: Component) -> Item[V]: if isinstance(component, ButtonComponent): From fffc8484a414c76ee71e260dc22c22f001f630fc Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 11 Jun 2025 03:10:22 +0100 Subject: [PATCH 204/228] add new file and unfurledmedia attributes --- discord/components.py | 19 ++++++++++++------- discord/types/components.py | 3 +++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/discord/components.py b/discord/components.py index d9ded85855..2ad30aa40f 100644 --- a/discord/components.py +++ b/discord/components.py @@ -659,6 +659,7 @@ def __init__(self, url: str): self.placeholder_version: int | None = None self.loading_state: MediaItemLoadingState | None = None self.src_is_animated: bool | None = None + self.attachment_id: int | None = None @property def url(self) -> str: @@ -678,6 +679,7 @@ def from_dict(cls, data: UnfurledMediaItemPayload, state=None) -> UnfurledMediaI 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.attachment_id = data.get("attachment_id") r._state = state return r @@ -825,7 +827,11 @@ class FileComponent(Component): Attributes ---------- file: :class:`UnfurledMediaItem` - The file's media URL. + The file's media item. + name: :class:`str` + The file's name. + size: :class:`int` + The file's size in bytes. spoiler: Optional[:class:`bool`] Whether the file is a spoiler. """ @@ -833,6 +839,8 @@ class FileComponent(Component): __slots__: tuple[str, ...] = ( "file", "spoiler", + "name", + "size", ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ @@ -841,9 +849,9 @@ class FileComponent(Component): def __init__(self, data: FileComponentPayload, state=None): self.type: ComponentType = try_enum(ComponentType, data["type"]) self.id: int = data.get("id") - self.file: UnfurledMediaItem = ( - umi := data.get("file") - ) and UnfurledMediaItem.from_dict(umi, state=state) + self.name: str = data.get("name") + self.size: int = data.get("size") + self.file: UnfurledMediaItem = UnfurledMediaItem.from_dict(data.get("file", {}), state=state) self.spoiler: bool | None = data.get("spoiler") def to_dict(self) -> FileComponentPayload: @@ -853,9 +861,6 @@ def to_dict(self) -> FileComponentPayload: return payload -# Alternate idea - subclass above components as UnfurledMedia? - - class Separator(Component): """Represents a Separator from Components V2. diff --git a/discord/types/components.py b/discord/types/components.py index 380fb59451..cf487f8382 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -114,6 +114,7 @@ class UnfurledMediaItem(TypedDict): placeholder_version: int loading_state: LoadingState flags: NotRequired[int] + attachment_id: NotRequired[Snowflake] class ThumbnailComponent(BaseComponent): @@ -138,6 +139,8 @@ class FileComponent(BaseComponent): type: Literal[13] file: UnfurledMediaItem spoiler: NotRequired[bool] + name: str + size: int class SeparatorComponent(BaseComponent): From 8ef3c142d8f113ee97dabddd5925ada564d3b842 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 02:10:51 +0000 Subject: [PATCH 205/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/components.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 2ad30aa40f..bf5ae41c52 100644 --- a/discord/components.py +++ b/discord/components.py @@ -851,7 +851,9 @@ def __init__(self, data: FileComponentPayload, state=None): self.id: int = data.get("id") self.name: str = data.get("name") self.size: int = data.get("size") - self.file: UnfurledMediaItem = UnfurledMediaItem.from_dict(data.get("file", {}), state=state) + self.file: UnfurledMediaItem = UnfurledMediaItem.from_dict( + data.get("file", {}), state=state + ) self.spoiler: bool | None = data.get("spoiler") def to_dict(self) -> FileComponentPayload: From d92fd927cf9e72ffbd7516d7ffc606e768b18a50 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:34:17 +0100 Subject: [PATCH 206/228] raise on content/embeds --- discord/abc.py | 2 ++ discord/channel.py | 2 ++ discord/interactions.py | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index e772ebe207..d80e135451 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1616,6 +1616,8 @@ async def send( components = view.to_components() if view.is_components_v2(): + if embeds or content: + raise TypeError("cannot send embeds or content with a view using v2 component logic") flags.is_components_v2 = True else: components = None diff --git a/discord/channel.py b/discord/channel.py index aa5045c25c..6c9a58117f 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1320,6 +1320,8 @@ async def create_thread( components = view.to_components() if view.is_components_v2(): + if embeds or content: + raise TypeError("cannot send embeds or content with a view using v2 component logic") flags.is_components_v2 = True else: components = None diff --git a/discord/interactions.py b/discord/interactions.py index e87e7f9192..4a1f610fdb 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -960,7 +960,7 @@ async def send_message( HTTPException Sending the message failed. TypeError - You specified both ``embed`` and ``embeds``. + You specified both ``embed`` and ``embeds``, or sent content or embeds with V2 components. ValueError The length of ``embeds`` was invalid. InteractionResponded @@ -992,6 +992,8 @@ async def send_message( if view is not None: payload["components"] = view.to_components() if view.is_components_v2(): + if embeds or content: + raise TypeError("cannot send embeds or content with a view using v2 component logic") flags.is_components_v2 = True if poll is not None: From d26dba54bc5f94c22e488a207824bc9dade415e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:35:26 +0000 Subject: [PATCH 207/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/abc.py | 4 +++- discord/channel.py | 4 +++- discord/interactions.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index d80e135451..4f419a3942 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1617,7 +1617,9 @@ async def send( components = view.to_components() if view.is_components_v2(): if embeds or content: - raise TypeError("cannot send embeds or content with a view using v2 component logic") + raise TypeError( + "cannot send embeds or content with a view using v2 component logic" + ) flags.is_components_v2 = True else: components = None diff --git a/discord/channel.py b/discord/channel.py index 6c9a58117f..de589a5502 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1321,7 +1321,9 @@ async def create_thread( components = view.to_components() if view.is_components_v2(): if embeds or content: - raise TypeError("cannot send embeds or content with a view using v2 component logic") + raise TypeError( + "cannot send embeds or content with a view using v2 component logic" + ) flags.is_components_v2 = True else: components = None diff --git a/discord/interactions.py b/discord/interactions.py index 4a1f610fdb..0e9084468d 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -993,7 +993,9 @@ async def send_message( payload["components"] = view.to_components() if view.is_components_v2(): if embeds or content: - raise TypeError("cannot send embeds or content with a view using v2 component logic") + raise TypeError( + "cannot send embeds or content with a view using v2 component logic" + ) flags.is_components_v2 = True if poll is not None: From 72234fffff95e6390b196c8813f7ea2171e472ff Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:14:20 +0100 Subject: [PATCH 208/228] updated docs, streamlined unfurledmedia --- discord/components.py | 43 ++++++++++++++++++++++++++----------- discord/enums.py | 13 ----------- discord/types/components.py | 5 ----- discord/ui/button.py | 2 ++ discord/ui/container.py | 6 ++++++ discord/ui/file.py | 16 ++++++++++++++ discord/ui/input_text.py | 2 ++ discord/ui/media_gallery.py | 2 ++ discord/ui/section.py | 5 ++++- discord/ui/select.py | 4 ++++ discord/ui/separator.py | 6 ++++++ discord/ui/text_display.py | 2 ++ discord/ui/thumbnail.py | 8 +++++++ docs/api/enums.rst | 15 +++++++++++++ 14 files changed, 97 insertions(+), 32 deletions(-) diff --git a/discord/components.py b/discord/components.py index bf5ae41c52..c62e943739 100644 --- a/discord/components.py +++ b/discord/components.py @@ -34,7 +34,6 @@ ChannelType, ComponentType, InputTextStyle, - MediaItemLoadingState, SeparatorSpacingSize, try_enum, ) @@ -646,6 +645,17 @@ def to_dict(self) -> TextDisplayComponentPayload: class UnfurledMediaItem(AssetMixin): + """Represents an Unfurled Media Item used in Components V2. + + This is used as an underlying component for other media-based components such as :class:`Thumbnail`, :class:`FileComponent`, and :class:`MediaGalleryItem`. + + .. versionadded:: 2.7 + + Attributes + ---------- + url: :class:`str` + The URL of this media item. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files. + """ def __init__(self, url: str): self._state = None @@ -655,15 +665,11 @@ def __init__(self, url: str): 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 self.attachment_id: int | None = None @property def url(self) -> str: - """Returns the underlying URL of this media.""" + """Returns this media item's url.""" return self._url @classmethod @@ -675,10 +681,6 @@ def from_dict(cls, data: UnfurledMediaItemPayload, state=None) -> UnfurledMediaI 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.attachment_id = data.get("attachment_id") r._state = state return r @@ -699,7 +701,7 @@ class Thumbnail(Component): Attributes ---------- media: :class:`UnfurledMediaItem` - The component's media object. + The component's underlying media object. description: Optional[:class:`str`] The thumbnail's description, up to 1024 characters. spoiler: Optional[:class:`bool`] @@ -726,7 +728,7 @@ def __init__(self, data: ThumbnailComponentPayload, state=None): @property def url(self) -> str: - """Returns the underlying URL of this thumbnail.""" + """Returns the URL of this thumbnail's underlying media item.""" return self.media.url def to_dict(self) -> ThumbnailComponentPayload: @@ -739,6 +741,21 @@ def to_dict(self) -> ThumbnailComponentPayload: class MediaGalleryItem: + """Represents an item used in the :class:`MediaGallery` component. + + This is used as an underlying component for other media-based components such as :class:`Thumbnail`, :class:`FileComponent`, and :class:`MediaGalleryItem`. + + .. versionadded:: 2.7 + + Attributes + ---------- + url: :class:`str` + The URL of this gallery item. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files. + description: Optional[:class:`str`] + The gallery item's description, up to 1024 characters. + spoiler: Optional[:class:`bool`] + Whether the gallery item is a spoiler. + """ def __init__(self, url, *, description=None, spoiler=False): self._state = None @@ -748,7 +765,7 @@ def __init__(self, url, *, description=None, spoiler=False): @property def url(self) -> str: - """Returns the underlying URL of this gallery item.""" + """Returns the URL of this gallery's underlying media item.""" return self.media.url def is_dispatchable(self) -> bool: diff --git a/discord/enums.py b/discord/enums.py index e9fa745c77..a8affc8a8b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -80,7 +80,6 @@ "MessageReferenceType", "SubscriptionStatus", "SeparatorSpacingSize", - "MediaItemLoadingState", ) @@ -1099,18 +1098,6 @@ def __int__(self): return self.value -class MediaItemLoadingState(Enum): - """An :class:`~discord.UnfurledMediaItem`'s ``loading_state``.""" - - unknown = 0 - loading = 1 - loaded_success = 2 - loaded_not_found = 3 - - def __int__(self): - return self.value - - T = TypeVar("T") diff --git a/discord/types/components.py b/discord/types/components.py index cf487f8382..ec68bcde3e 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -37,7 +37,6 @@ 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): @@ -109,10 +108,6 @@ class UnfurledMediaItem(TypedDict): height: NotRequired[int | None] width: NotRequired[int | None] content_type: NotRequired[str] - src_is_animated: NotRequired[bool] - placeholder: str - placeholder_version: int - loading_state: LoadingState flags: NotRequired[int] attachment_id: NotRequired[Snowflake] diff --git a/discord/ui/button.py b/discord/ui/button.py index 26c84670ca..6ffc70e320 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -75,6 +75,8 @@ class Button(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The button's ID. """ __item_repr_attributes__: tuple[str, ...] = ( diff --git a/discord/ui/container.py b/discord/ui/container.py index 6bded47d30..7ccf213f57 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -49,6 +49,10 @@ class Container(Item[V]): The initial items in this container. colour: Union[:class:`Colour`, :class:`int`] The accent colour of the container. Aliased to ``color`` as well. + spoiler: Optional[:class:`bool`] + Whether this container is a spoiler. + id: Optional[:class:`int`] + The container's ID. """ __container_children_items__: ClassVar[list[ItemCallbackType]] = [] @@ -282,6 +286,8 @@ def add_separator( 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`. + id: Optional[:class:`int`] + The separator's ID. """ s = Separator(divider=divider, spacing=spacing, id=id) diff --git a/discord/ui/file.py b/discord/ui/file.py index 9a941df4f4..b9b00b8150 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -25,6 +25,12 @@ class File(Item[V]): Parameters ---------- + url: :class:`str` + The URL of this file. This must be an ``attachment://`` URL referring to a local file used with :class:`~discord.File`. + spoiler: Optional[:class:`bool`] + Whether this file is a spoiler. + id: Optional[:class:`int`] + The file component's ID. """ def __init__(self, url: str, *, spoiler: bool = False, id: int | None = None): @@ -65,6 +71,16 @@ def spoiler(self) -> bool: def spoiler(self, spoiler: bool) -> None: self._underlying.spoiler = spoiler + @property + def name(self) -> str: + """The name of this file, if provided by Discord.""" + return self._underlying.name + + @property + def size(self) -> int: + """The size of this file in bytes, if provided by Discord.""" + return self._underlying.size + def to_component_dict(self) -> FileComponentPayload: return self._underlying.to_dict() diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 8f0d4b5368..784ff3b806 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -46,6 +46,8 @@ class InputText: like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The input text's ID. """ def __init__( diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index daa614e06f..b3030860de 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -27,6 +27,8 @@ class MediaGallery(Item[V]): ---------- *items: :class:`MediaGalleryItem` The initial items contained in this gallery, up to 10. + id: Optional[:class:`int`] + The gallery's ID. """ def __init__(self, *items: MediaGalleryItem, id: int | None = None): diff --git a/discord/ui/section.py b/discord/ui/section.py index fc4189a7af..c34de510be 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -33,9 +33,13 @@ class Section(Item[V]): *items: :class:`Item` The initial items contained in this section, up to 3. Currently only supports :class:`~discord.ui.TextDisplay`. + Sections must have at least 1 item before being sent. accessory: Optional[:class:`Item`] The section's accessory. This is displayed in the top right of the section. Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`. + Sections must have an accessory attached before being sent. + id: Optional[:class:`int`] + The section's ID. """ __section_accessory_item__: ClassVar[ItemCallbackType] = None @@ -214,7 +218,6 @@ def set_thumbnail( spoiler: Optional[:class:`bool`] Whether the thumbnail is a spoiler. Defaults to ``False``. id: Optional[:class:`int`] - The thumbnail's ID. """ diff --git a/discord/ui/select.py b/discord/ui/select.py index bebbbc39da..2bb0b79b61 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -110,6 +110,8 @@ class Select(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The select menu's ID. """ __item_repr_attributes__: tuple[str, ...] = ( @@ -513,6 +515,8 @@ def select( Defaults to all channel types. disabled: :class:`bool` Whether the select is disabled or not. Defaults to ``False``. + id: Optional[:class:`int`] + The select menu's ID. """ if select_type not in _select_types: raise ValueError( diff --git a/discord/ui/separator.py b/discord/ui/separator.py index c0a9a771e3..a014e324f4 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -25,6 +25,12 @@ 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`. + id: Optional[:class:`int`] + The separator's ID. """ def __init__( diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index dcc7056787..59e178b08d 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -27,6 +27,8 @@ class TextDisplay(Item[V]): ---------- content: :class:`str` The text display's content, up to 4000 characters. + id: Optional[:class:`int`] + The text display's ID. """ def __init__( diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index d8214fc658..10b9bf4b53 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -25,6 +25,14 @@ class Thumbnail(Item[V]): Parameters ---------- + url: :class:`str` + The url of the thumbnail. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files. + 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: Optional[:class:`int`] + The thumbnail's ID. """ def __init__( diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 4d278e3758..1210990717 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2519,3 +2519,18 @@ of :class:`enum.Enum`. .. attribute:: inactive The subscription is inactive and the subscription owner is not being charged. + + +.. class:: SeparatorSpacingSize + + Represents the padding size around a separator component. + + .. versionadded:: 2.7 + + .. attribute:: small + + The separator uses small padding. + + .. attribute:: large + + The separator uses large padding. From b7d5040b24bfc2da9dd5ef10aab7d7fa4bbf2878 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:25:44 +0100 Subject: [PATCH 209/228] fix with_components --- discord/webhook/async_.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 22f9691e55..d82ed4e731 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -408,12 +408,16 @@ def edit_webhook_message( payload: dict[str, Any] | None = None, multipart: list[dict[str, Any]] | None = None, files: list[File] | None = None, + with_components: bool | None = None, ) -> Response[WebhookMessage]: params = {} if thread_id: params["thread_id"] = thread_id + if with_components is not None: + params["with_components"] = int(with_components) + route = Route( "PATCH", "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", @@ -2011,14 +2015,22 @@ async def edit_message( raise InvalidArgument( "This webhook does not have a token associated with it" ) + + with_components = False if view is not MISSING: - if isinstance(self._state, _WebhookState): + if ( + isinstance(self._state, _WebhookState) + and view + and view.is_dispatchable() + ): raise InvalidArgument( - "This webhook does not have state associated with it" + "Dispatchable Webhook views require an associated state with the webhook" ) self._state.prevent_view_updates_for(message_id) + if self.type is not WebhookType.application: + with_components = True previous_mentions: AllowedMentions | None = getattr( self._state, "allowed_mentions", None @@ -2052,6 +2064,7 @@ async def edit_message( payload=params.payload, multipart=params.multipart, files=params.files, + with_components=with_components, ) message = self._create_message(data) From 969600d87721506ffd3f83231c4acb27e9db6e82 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:26:10 +0000 Subject: [PATCH 210/228] style(pre-commit): auto fixes from pre-commit.com hooks --- 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 d82ed4e731..73e8402a73 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -2015,7 +2015,7 @@ async def edit_message( raise InvalidArgument( "This webhook does not have a token associated with it" ) - + with_components = False if view is not MISSING: From 0c8c719a1c993a37beaee75126e0b9114afdc9ab Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:53:22 +0100 Subject: [PATCH 211/228] typing.Self --- discord/ui/container.py | 34 ++++++++++++++++------------------ discord/ui/media_gallery.py | 3 ++- discord/ui/modal.py | 5 +++-- discord/ui/section.py | 25 ++++++++++++++----------- discord/ui/select.py | 5 +++-- 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 7ccf213f57..8195e6692c 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from ..types.components import ContainerComponent as ContainerComponentPayload from .view import View + from typing_extensions import Self C = TypeVar("C", bound="Container") @@ -120,7 +121,7 @@ def _set_components(self, items: list[Item]): for item in items: self._add_component_from_item(item) - def add_item(self, item: Item) -> None: + def add_item(self, item: Item) -> Self: """Adds an item to the container. Parameters @@ -146,13 +147,13 @@ def add_item(self, item: Item) -> None: self._add_component_from_item(item) return self - def remove_item(self, item: Item | int) -> None: + def remove_item(self, item: Item | int) -> Self: """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. + The item, ``id``, or item ``custom_id`` to remove from the container. """ if isinstance(item, (str, int)): @@ -164,8 +165,8 @@ def remove_item(self, item: Item | int) -> None: 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, ...)`. - If an ``int`` is provided, it will be retrieved by ``id``, otherwise it will check for ``custom_id``. + """Get a top-level item from this container. Roughly equivalent to `utils.get(container.items, ...)`. + If an ``int`` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``. This method will also search for nested items. Parameters @@ -194,7 +195,7 @@ def add_section( *items: Item, accessory: Item, id: int | None = None, - ): + ) -> Self: """Adds a :class:`Section` to the container. To append a pre-existing :class:`Section`, use the @@ -211,13 +212,12 @@ def add_section( id: Optional[:class:`int`] The section's ID. """ - # accept raw strings? section = Section(*items, accessory=accessory, id=id) return self.add_item(section) - def add_text(self, content: str, id: int | None = None) -> None: + def add_text(self, content: str, id: int | None = None) -> Self: """Adds a :class:`TextDisplay` to the container. Parameters @@ -234,12 +234,10 @@ def add_gallery( self, *items: Item, id: int | None = None, - ): + ) -> Self: """Adds a :class:`MediaGallery` to the container. - To append a pre-existing :class:`MediaGallery`, use the - - :meth:`add_item` method instead. + To append a pre-existing :class:`MediaGallery`, use :meth:`add_item` instead. Parameters ---------- @@ -248,13 +246,12 @@ def add_gallery( id: Optiona[:class:`int`] The gallery's ID. """ - # accept raw urls? g = MediaGallery(*items, id=id) return self.add_item(g) - def add_file(self, url: str, spoiler: bool = False, id: int | None = None) -> None: + def add_file(self, url: str, spoiler: bool = False, id: int | None = None) -> Self: """Adds a :class:`TextDisplay` to the container. Parameters @@ -277,7 +274,7 @@ def add_separator( divider: bool = True, spacing: SeparatorSpacingSize = SeparatorSpacingSize.small, id: int | None = None, - ) -> None: + ) -> Self: """Adds a :class:`Separator` to the container. Parameters @@ -295,7 +292,8 @@ def add_separator( 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.""" + """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(t for i in self.items if (t := i.copy_text())) @property @@ -362,7 +360,7 @@ def refresh_component(self, component: ContainerComponent) -> None: x.refresh_component(y) i += 1 - def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Disables all buttons and select menus in the container. @@ -378,7 +376,7 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: item.disabled = True return self - def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + def enable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Enables all buttons and select menus in the container. diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index b3030860de..571ed33678 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from ..types.components import MediaGalleryComponent as MediaGalleryComponentPayload from .view import View + from typing_extensions import Self M = TypeVar("M", bound="MediaGallery") @@ -42,7 +43,7 @@ def __init__(self, *items: MediaGalleryItem, id: int | None = None): def items(self): return self._underlying.items - def append_item(self, item: MediaGalleryItem) -> None: + def append_item(self, item: MediaGalleryItem) -> Self: """Adds a :attr:`MediaGalleryItem` to the gallery. Parameters diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 6f4bfc9f55..0403ffc1e3 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -19,6 +19,7 @@ if TYPE_CHECKING: from ..interactions import Interaction from ..state import ConnectionState + from typing_extensions import Self class Modal: @@ -187,7 +188,7 @@ def key(item: InputText) -> int: return components - def add_item(self, item: InputText): + def add_item(self, item: InputText) -> Self: """Adds an InputText component to the modal dialog. Parameters @@ -206,7 +207,7 @@ def add_item(self, item: InputText): self._children.append(item) return self - def remove_item(self, item: InputText): + def remove_item(self, item: InputText) -> Self: """Removes an InputText component from the modal dialog. Parameters diff --git a/discord/ui/section.py b/discord/ui/section.py index c34de510be..ab5cf3f8d9 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from ..types.components import SectionComponent as SectionComponentPayload from .view import View + from typing_extensions import Self S = TypeVar("S", bound="Section") @@ -85,7 +86,7 @@ def _set_components(self, items: list[Item]): for item in items: self._add_component_from_item(item) - def add_item(self, item: Item) -> None: + def add_item(self, item: Item) -> Self: """Adds an item to the section. Parameters @@ -98,7 +99,7 @@ def add_item(self, item: Item) -> None: TypeError An :class:`Item` was not passed. ValueError - Maximum number of items has been exceeded (10). + Maximum number of items has been exceeded (3). """ if len(self.items) >= 3: @@ -112,13 +113,14 @@ def add_item(self, item: Item) -> None: 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. + def remove_item(self, item: Item | str | int) -> Self: + """Removes an item from the section. If an :class:`int` or :class:`str` is passed, + the item will be removed by Item ``id`` or ``custom_id`` respectively. Parameters ---------- item: Union[:class:`Item`, :class:`int`, :class:`str`] - The item, item :attr:`id`, or item ``custom_id`` to remove from the section. + The item, item ``id``, or item ``custom_id`` to remove from the section. """ if isinstance(item, (str, int)): @@ -151,7 +153,7 @@ def get_item(self, id: int | str) -> Item | None: child = find(lambda i: getattr(i, attr, None) == id, self.items) return child - def add_text(self, content: str, *, id: int | None = None) -> None: + def add_text(self, content: str, *, id: int | None = None) -> Self: """Adds a :class:`TextDisplay` to the section. Parameters @@ -174,7 +176,7 @@ def add_text(self, content: str, *, id: int | None = None) -> None: return self.add_item(text) - def set_accessory(self, item: Item) -> None: + def set_accessory(self, item: Item) -> Self: """Set an item as the section's :attr:`accessory`. Parameters @@ -206,7 +208,7 @@ def set_thumbnail( description: str | None = None, spoiler: bool = False, id: int | None = None, - ) -> None: + ) -> Self: """Sets a :class:`Thumbnail` with the provided URL as the section's :attr:`accessory`. Parameters @@ -233,7 +235,8 @@ def view(self, value): item.parent = self 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.""" + """Returns the text of all :class:`~discord.ui.TextDisplay` items in this section. + Equivalent to the `Copy Text` option on Discord clients.""" return "\n".join(t for i in self.items if (t := i.copy_text())) @property @@ -259,7 +262,7 @@ def refresh_component(self, component: SectionComponent) -> None: if self.accessory and component.accessory: self.accessory.refresh_component(component.accessory) - def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + def disable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Disables all buttons and select menus in the section. At the moment, this only disables :attr:`accessory` if it is a button. @@ -276,7 +279,7 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: item.disabled = True return self - def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + def enable_all_items(self, *, exclusions: list[Item] | None = None) -> Self: """ Enables all buttons and select menus in the container. At the moment, this only enables :attr:`accessory` if it is a button. diff --git a/discord/ui/select.py b/discord/ui/select.py index 2bb0b79b61..33b31260f4 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -58,6 +58,7 @@ from ..types.components import SelectMenu as SelectMenuPayload from ..types.interactions import ComponentInteractionData from .view import View + from typing_extensions import Self S = TypeVar("S", bound="Select") V = TypeVar("V", bound="View", covariant=True) @@ -267,7 +268,7 @@ def add_option( description: str | None = None, emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, default: bool = False, - ): + ) -> Self: """Adds an option to the select menu. To append a pre-existing :class:`discord.SelectOption` use the @@ -308,7 +309,7 @@ def add_option( return self.append_option(option) - def append_option(self, option: SelectOption): + def append_option(self, option: SelectOption) -> Self: """Appends an option to the select menu. Parameters From c29b967448d459749dd773df2df36ba2f0e6fe34 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:53:47 +0000 Subject: [PATCH 212/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/container.py | 6 ++++-- discord/ui/media_gallery.py | 3 ++- discord/ui/modal.py | 3 ++- discord/ui/section.py | 8 +++++--- discord/ui/select.py | 3 ++- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 8195e6692c..0b24dc5ebb 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -20,9 +20,10 @@ __all__ = ("Container",) if TYPE_CHECKING: + from typing_extensions import Self + from ..types.components import ContainerComponent as ContainerComponentPayload from .view import View - from typing_extensions import Self C = TypeVar("C", bound="Container") @@ -293,7 +294,8 @@ 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.""" + Equivalent to the `Copy Text` option on Discord clients. + """ return "\n".join(t for i in self.items if (t := i.copy_text())) @property diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 571ed33678..66344431c5 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -10,9 +10,10 @@ __all__ = ("MediaGallery",) if TYPE_CHECKING: + from typing_extensions import Self + from ..types.components import MediaGalleryComponent as MediaGalleryComponentPayload from .view import View - from typing_extensions import Self M = TypeVar("M", bound="MediaGallery") diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 0403ffc1e3..7c79aa3e00 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -17,9 +17,10 @@ if TYPE_CHECKING: + from typing_extensions import Self + from ..interactions import Interaction from ..state import ConnectionState - from typing_extensions import Self class Modal: diff --git a/discord/ui/section.py b/discord/ui/section.py index ab5cf3f8d9..119a5d2d3b 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -15,9 +15,10 @@ __all__ = ("Section",) if TYPE_CHECKING: + from typing_extensions import Self + from ..types.components import SectionComponent as SectionComponentPayload from .view import View - from typing_extensions import Self S = TypeVar("S", bound="Section") @@ -114,7 +115,7 @@ def add_item(self, item: Item) -> Self: return self def remove_item(self, item: Item | str | int) -> Self: - """Removes an item from the section. If an :class:`int` or :class:`str` is passed, + """Removes an item from the section. If an :class:`int` or :class:`str` is passed, the item will be removed by Item ``id`` or ``custom_id`` respectively. Parameters @@ -236,7 +237,8 @@ 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.""" + Equivalent to the `Copy Text` option on Discord clients. + """ return "\n".join(t for i in self.items if (t := i.copy_text())) @property diff --git a/discord/ui/select.py b/discord/ui/select.py index 33b31260f4..f607669899 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -54,11 +54,12 @@ ) if TYPE_CHECKING: + from typing_extensions import Self + from ..abc import GuildChannel from ..types.components import SelectMenu as SelectMenuPayload from ..types.interactions import ComponentInteractionData from .view import View - from typing_extensions import Self S = TypeVar("S", bound="Select") V = TypeVar("V", bound="View", covariant=True) From ca3f1ce178c9be2b74a6e550e52e94bea002763f Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:56:34 +0100 Subject: [PATCH 213/228] raise again --- discord/webhook/async_.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 73e8402a73..e1bdd00acf 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -670,6 +670,8 @@ 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_components_v2(): + if payload["content"] or payload["embeds"]: + raise TypeError("cannot send embeds or content with a view using v2 component logic") flags.is_components_v2 = True if poll is not MISSING: payload["poll"] = poll.to_dict() From 5490f908152157b413d042d4b6026b9f732dbbb1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:57:01 +0000 Subject: [PATCH 214/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/webhook/async_.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index e1bdd00acf..3f4959f52b 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -671,7 +671,9 @@ def handle_message_parameters( payload["components"] = view.to_components() if view is not None else [] if view and view.is_components_v2(): if payload["content"] or payload["embeds"]: - raise TypeError("cannot send embeds or content with a view using v2 component logic") + raise TypeError( + "cannot send embeds or content with a view using v2 component logic" + ) flags.is_components_v2 = True if poll is not MISSING: payload["poll"] = poll.to_dict() From 2fdf147c124c1bb26be4a2a21fd8e7c094541a49 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:59:33 +0100 Subject: [PATCH 215/228] Apply suggestions from code review Co-authored-by: plun1331 Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/file.py | 3 +++ discord/ui/text_display.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/ui/file.py b/discord/ui/file.py index b9b00b8150..54a6845579 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -21,6 +21,9 @@ class File(Item[V]): """Represents a UI File. + .. note:: + This component does not show media previews. Use :class:`MediaGallery` for previews instead. + .. versionadded:: 2.7 Parameters diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 59e178b08d..98963994e6 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -65,7 +65,7 @@ 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.""" + """Returns the content of this text display. Equivalent to the `Copy Text` option on Discord clients.""" return self.content @classmethod From 310ecf9d339c7076ce7db35f6daf8e528311f8f3 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 17:59:25 +0100 Subject: [PATCH 216/228] Apply suggestions from code review Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- discord/ui/view.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 94ddb0bb51..6d43d522f9 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -402,12 +402,13 @@ def add_item(self, item: Item[V]) -> None: return self def remove_item(self, item: Item[V] | 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. + """Removes an item from the view. If an :class:`int` or :class:`str` is passed, + the item will be removed by Item ``id`` or ``custom_id`` respectively. Parameters ---------- item: Union[:class:`Item`, :class:`int`, :class:`str`] - The item, item :attr:`id`, or item ``custom_id`` to remove from the view. + The item, item ``id``, or item ``custom_id`` to remove from the view. """ if isinstance(item, (str, int)): @@ -427,8 +428,8 @@ def clear_items(self) -> None: return self def get_item(self, custom_id: str | int) -> Item[V] | 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``. + """Gets an item from the view. Roughly equal to `utils.get(view.children, ...)`. + If an :class:`int` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``. This method will also search nested items. Parameters @@ -704,7 +705,8 @@ def walk_children(self) -> Iterator[Item]: yield item 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.""" + """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(t for i in self.children if (t := i.copy_text())) @property From 80ff8396aa28283a1c93f209bc7a53af40a52569 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:59:50 +0000 Subject: [PATCH 217/228] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 6d43d522f9..64b0520172 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -706,7 +706,8 @@ def walk_children(self) -> Iterator[Item]: 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.""" + Equivalent to the `Copy Text` option on Discord clients. + """ return "\n".join(t for i in self.children if (t := i.copy_text())) @property From a54223d00467400bfd63c109ef2bc45a65b1da76 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 18:01:38 +0100 Subject: [PATCH 218/228] Update examples/views/new_components.py Co-authored-by: plun1331 Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> --- examples/views/new_components.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/views/new_components.py b/examples/views/new_components.py index a8ac571a6c..26605c0ca0 100644 --- a/examples/views/new_components.py +++ b/examples/views/new_components.py @@ -59,7 +59,8 @@ def __init__(self, user: User): @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() + await interaction.message.delete() + async def on_timeout(self): self.get_item(200).disabled = True From 3e5fec880e9c72d172cb5160536f3cc55472f283 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 17:02:02 +0000 Subject: [PATCH 219/228] style(pre-commit): auto fixes from pre-commit.com hooks --- examples/views/new_components.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/views/new_components.py b/examples/views/new_components.py index 26605c0ca0..5f323c7bca 100644 --- a/examples/views/new_components.py +++ b/examples/views/new_components.py @@ -61,7 +61,6 @@ async def delete_button(self, button: Button, interaction: Interaction): await interaction.response.defer(invisible=True) await interaction.message.delete() - async def on_timeout(self): self.get_item(200).disabled = True await self.message.edit(view=self) From 2930396021941764b83060dc5929781fadfa9299 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 18:06:55 +0100 Subject: [PATCH 220/228] typ --- 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 0b24dc5ebb..1551757e3a 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -148,7 +148,7 @@ def add_item(self, item: Item) -> Self: self._add_component_from_item(item) return self - def remove_item(self, item: Item | int) -> Self: + def remove_item(self, item: Item | str | int) -> Self: """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 From bc88140d237114895621b19ec17c749b52ffd133 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 18:25:38 +0100 Subject: [PATCH 221/228] item docstring --- discord/ui/item.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/discord/ui/item.py b/discord/ui/item.py index 2daf2dad52..6e607a7541 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -44,12 +44,26 @@ class Item(Generic[V]): """Represents the base UI item that all UI components inherit from. - The current UI items supported are: + The following are the original items: - :class:`discord.ui.Button` - :class:`discord.ui.Select` + - :class:`discord.ui.InputText` (Modals only) + + And the following are new items under the "Components V2" specification: + + - :class:`discord.ui.Section` + - :class:`discord.ui.TextDisplay` + - :class:`discord.ui.Thumbnail` + - :class:`discord.ui.MediaGallery` + - :class:`discord.ui.File` + - :class:`discord.ui.Separator` + - :class:`discord.ui.Container` .. versionadded:: 2.0 + + .. versionchanged:: 2.7 + Added V2 Components. """ __item_repr_attributes__: tuple[str, ...] = ("row",) From 37da37a3598b7c634d89280dd71ef0e6dfe13a98 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:21:10 +0100 Subject: [PATCH 222/228] i hate this --- discord/ui/section.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 119a5d2d3b..9ecda930c3 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -47,11 +47,11 @@ class Section(Item[V]): __section_accessory_item__: ClassVar[ItemCallbackType] = None def __init_subclass__(cls) -> None: - accessory: ItemCallbackType = None + accessory: list[ItemCallbackType] = [] for base in reversed(cls.__mro__): for member in base.__dict__.values(): if hasattr(member, "__discord_ui_model_type__"): - accessory = member + accessory.append(member) cls.__section_accessory_item__ = accessory @@ -67,14 +67,14 @@ def __init__(self, *items: Item, accessory: Item = None, id: int | None = None): components=[], accessory=None, ) - if func := self.__section_accessory_item__: + for func in self.__section_accessory_item__: item: Item = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ ) item.callback = partial(func, self, item) self.set_accessory(item) setattr(self, func.__name__, item) - elif accessory: + if accessory: self.set_accessory(accessory) for i in items: self.add_item(i) From c66b66802af47ea18ad06aa11004e6ef47bd0116 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:48:44 +0100 Subject: [PATCH 223/228] add repr --- discord/ui/button.py | 2 ++ discord/ui/container.py | 7 +++++++ discord/ui/file.py | 6 ++++++ discord/ui/input_text.py | 17 +++++++++++++++++ discord/ui/item.py | 1 - discord/ui/media_gallery.py | 5 +++++ discord/ui/modal.py | 12 ++++++++++++ discord/ui/section.py | 6 ++++++ discord/ui/select.py | 2 ++ discord/ui/separator.py | 6 ++++++ discord/ui/text_display.py | 5 +++++ discord/ui/thumbnail.py | 7 +++++++ 12 files changed, 75 insertions(+), 1 deletion(-) diff --git a/discord/ui/button.py b/discord/ui/button.py index 6ffc70e320..6184c52718 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -87,6 +87,8 @@ class Button(Item[V]): "emoji", "sku_id", "row", + "custom_id", + "id", ) def __init__( diff --git a/discord/ui/container.py b/discord/ui/container.py index 1551757e3a..12c674b63c 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -57,6 +57,13 @@ class Container(Item[V]): The container's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "items", + "colour", + "spoiler", + "id", + ) + __container_children_items__: ClassVar[list[ItemCallbackType]] = [] def __init_subclass__(cls) -> None: diff --git a/discord/ui/file.py b/discord/ui/file.py index 54a6845579..e662dfcec3 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -36,6 +36,12 @@ class File(Item[V]): The file component's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "file", + "spoiler", + "id", + ) + def __init__(self, url: str, *, spoiler: bool = False, id: int | None = None): super().__init__() diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 784ff3b806..d9ac87259b 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -50,6 +50,17 @@ class InputText: The input text's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "label", + "placeholder", + "value", + "required", + "style", + "min_length", + "max_length", + "id", + ) + def __init__( self, *, @@ -97,6 +108,12 @@ def __init__( self.row = row self._rendered_row: int | None = None + def __repr__(self) -> str: + attrs = " ".join( + f"{key}={getattr(self, key)!r}" for key in self.__item_repr_attributes__ + ) + return f"<{self.__class__.__name__} {attrs}>" + @property def type(self) -> ComponentType: return self._underlying.type diff --git a/discord/ui/item.py b/discord/ui/item.py index 6e607a7541..7c324b0b2b 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -48,7 +48,6 @@ class Item(Generic[V]): - :class:`discord.ui.Button` - :class:`discord.ui.Select` - - :class:`discord.ui.InputText` (Modals only) And the following are new items under the "Components V2" specification: diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 66344431c5..acb8a4e28f 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -33,6 +33,11 @@ class MediaGallery(Item[V]): The gallery's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "items", + "id", + ) + def __init__(self, *items: MediaGalleryItem, id: int | None = None): super().__init__() diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 7c79aa3e00..58cf7db1e6 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -45,6 +45,12 @@ class Modal: If ``None`` then there is no timeout. """ + __item_repr_attributes__: tuple[str, ...] = ( + "title", + "children", + "timeout", + ) + def __init__( self, *children: InputText, @@ -70,6 +76,12 @@ def __init__( self.__timeout_task: asyncio.Task[None] | None = None self.loop = asyncio.get_event_loop() + def __repr__(self) -> str: + attrs = " ".join( + f"{key}={getattr(self, key)!r}" for key in self.__item_repr_attributes__ + ) + return f"<{self.__class__.__name__} {attrs}>" + def _start_listening_from_store(self, store: ModalStore) -> None: self.__cancel_callback = partial(store.remove_modal) if self.timeout: diff --git a/discord/ui/section.py b/discord/ui/section.py index 9ecda930c3..d4511ba146 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -44,6 +44,12 @@ class Section(Item[V]): The section's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "items", + "accessory", + "id", + ) + __section_accessory_item__: ClassVar[ItemCallbackType] = None def __init_subclass__(cls) -> None: diff --git a/discord/ui/select.py b/discord/ui/select.py index f607669899..7c2f8b1f4a 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -124,6 +124,8 @@ class Select(Item[V]): "options", "channel_types", "disabled", + "custom_id", + "id", ) def __init__( diff --git a/discord/ui/separator.py b/discord/ui/separator.py index a014e324f4..6b81674401 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -33,6 +33,12 @@ class Separator(Item[V]): The separator's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "divider", + "spacing", + "id", + ) + def __init__( self, *, diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 98963994e6..6624500a3f 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -31,6 +31,11 @@ class TextDisplay(Item[V]): The text display's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "content", + "id", + ) + def __init__( self, content: str, diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 10b9bf4b53..351f57c2cc 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -35,6 +35,13 @@ class Thumbnail(Item[V]): The thumbnail's ID. """ + __item_repr_attributes__: tuple[str, ...] = ( + "url", + "description", + "spoiler", + "id", + ) + def __init__( self, url: str, From 32f149cb8615cf0b479944e8cc0107169c20c55d Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:49:48 +0100 Subject: [PATCH 224/228] custom_id --- discord/ui/input_text.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index d9ac87259b..fd2fabeb6c 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -58,6 +58,7 @@ class InputText: "style", "min_length", "max_length", + "custom_id", "id", ) From 66bbf2a16712fff98e64bc0d5e6f988a937c93ae Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Tue, 1 Jul 2025 20:12:26 +0100 Subject: [PATCH 225/228] Update section.py --- 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 d4511ba146..5f1b850b8c 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -50,7 +50,7 @@ class Section(Item[V]): "id", ) - __section_accessory_item__: ClassVar[ItemCallbackType] = None + __section_accessory_item__: ClassVar[ItemCallbackType] = [] def __init_subclass__(cls) -> None: accessory: list[ItemCallbackType] = [] From f6de681e4732c78497b85d49f5c65800e8e40d03 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:06:57 +0100 Subject: [PATCH 226/228] keyerror --- 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 3f4959f52b..4475890d85 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -670,7 +670,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_components_v2(): - if payload["content"] or payload["embeds"]: + if payload.get("content") or payload.get("embeds"): raise TypeError( "cannot send embeds or content with a view using v2 component logic" ) From b470a36df86c9475176c8b0507eb8fd82a8a14d3 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Wed, 2 Jul 2025 23:06:21 +0100 Subject: [PATCH 227/228] add warning for Button.row --- discord/ui/button.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/ui/button.py b/discord/ui/button.py index 6184c52718..29e74cd8af 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -75,6 +75,11 @@ class Button(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + + .. warning:: + + This parameter does not work with V2 components or with more than 25 items in your view. + id: Optional[:class:`int`] The button's ID. """ From bcf7aa14a3196d1fe42f384e60d071dfe9982339 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Fri, 11 Jul 2025 05:07:25 +0100 Subject: [PATCH 228/228] InputText.id --- discord/components.py | 7 +++++-- discord/ui/input_text.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/discord/components.py b/discord/components.py index c62e943739..5473b7babc 100644 --- a/discord/components.py +++ b/discord/components.py @@ -202,7 +202,7 @@ class InputText(Component): style: :class:`.InputTextStyle` The style of the input text field. custom_id: Optional[:class:`str`] - The ID of the input text field that gets received during an interaction. + The custom ID of the input text field that gets received during an interaction. label: :class:`str` The label for the input text field. placeholder: Optional[:class:`str`] @@ -216,6 +216,8 @@ class InputText(Component): Whether the input text field is required or not. Defaults to `True`. value: Optional[:class:`str`] The value that has been entered in the input text field. + id: Optional[:class:`int`] + The input text's ID. """ __slots__: tuple[str, ...] = ( @@ -228,6 +230,7 @@ class InputText(Component): "max_length", "required", "value", + "id", ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ @@ -235,7 +238,7 @@ class InputText(Component): def __init__(self, data: InputTextComponentPayload): self.type = ComponentType.input_text - self.id: int = data.get("id") + self.id: int | None = data.get("id") self.style: InputTextStyle = try_enum(InputTextStyle, data["style"]) self.custom_id = data["custom_id"] self.label: str = data.get("label", None) diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index fd2fabeb6c..fa9d7615ab 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -46,8 +46,6 @@ class InputText: like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). - id: Optional[:class:`int`] - The input text's ID. """ __item_repr_attributes__: tuple[str, ...] = ( @@ -124,6 +122,11 @@ def style(self) -> InputTextStyle: """The style of the input text field.""" return self._underlying.style + @property + def id(self) -> int | None: + """The input text's ID. If not provided by the user, it is set sequentially by Discord.""" + return self._underlying.id + @style.setter def style(self, value: InputTextStyle): if not isinstance(value, InputTextStyle):