Browse Source

chore: Update components

pull/10166/head
DA-344 3 months ago
parent
commit
335b3976d8
  1. 25
      discord/attachment.py
  2. 286
      discord/components.py
  3. 4
      discord/enums.py
  4. 2
      discord/message.py
  5. 6
      discord/types/attachment.py
  6. 29
      discord/types/components.py
  7. 1
      discord/ui/__init__.py
  8. 86
      discord/ui/container.py
  9. 55
      discord/ui/section.py
  10. 11
      discord/ui/view.py

25
discord/attachment.py

@ -27,6 +27,7 @@ import io
from os import PathLike
from typing import TYPE_CHECKING, Any, Optional, Union
from .errors import ClientException
from .mixins import Hashable
from .file import File
from .flags import AttachmentFlags
@ -67,9 +68,9 @@ class AttachmentBase:
'_state',
)
def __init__(self, data: AttachmentBasePayload, state: ConnectionState) -> None:
self._state: ConnectionState = state
self._http: HTTPClient = state.http
def __init__(self, data: AttachmentBasePayload, state: Optional[ConnectionState]) -> None:
self._state: Optional[ConnectionState] = state
self._http: Optional[HTTPClient] = state.http if state else None
self.url: str = data['url']
self.proxy_url: str = data['proxy_url']
self.description: Optional[str] = data.get('description')
@ -162,12 +163,19 @@ class AttachmentBase:
You do not have permissions to access this attachment
NotFound
The attachment was deleted.
ClientException
Cannot read a stateless attachment.
Returns
-------
:class:`bytes`
The contents of the attachment.
"""
if not self._http:
raise ClientException(
'Cannot read a stateless attachment'
)
url = self.proxy_url if use_cached else self.url
data = await self._http.get_from_cdn(url)
return data
@ -240,8 +248,8 @@ class AttachmentBase:
spoiler=spoiler,
)
def to_dict(self):
base = {
def to_dict(self) -> AttachmentBasePayload:
base: AttachmentBasePayload = {
'url': self.url,
'proxy_url': self.proxy_url,
'spoiler': self.spoiler,
@ -415,9 +423,12 @@ class UnfurledAttachment(AttachmentBase):
'loading_state',
)
def __init__(self, data: UnfurledAttachmentPayload, state: ConnectionState) -> None:
self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data['loading_state'])
def __init__(self, data: UnfurledAttachmentPayload, state: Optional[ConnectionState]) -> None:
self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data.get('loading_state', 0))
super().__init__(data, state)
def __repr__(self) -> str:
return f'<UnfurledAttachment url={self.url!r}>'
def to_object_dict(self):
return {'url': self.url}

286
discord/components.py

@ -33,6 +33,8 @@ from typing import (
Tuple,
Union,
)
from .attachment import UnfurledAttachment
from .enums import (
try_enum,
ComponentType,
@ -40,8 +42,9 @@ from .enums import (
TextStyle,
ChannelType,
SelectDefaultValueType,
DividerSize,
SeparatorSize,
)
from .colour import Colour
from .utils import get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag
@ -59,16 +62,20 @@ if TYPE_CHECKING:
SelectDefaultValues as SelectDefaultValuesPayload,
SectionComponent as SectionComponentPayload,
TextComponent as TextComponentPayload,
ThumbnailComponent as ThumbnailComponentPayload,
MediaGalleryComponent as MediaGalleryComponentPayload,
FileComponent as FileComponentPayload,
DividerComponent as DividerComponentPayload,
ComponentContainer as ComponentContainerPayload,
SeparatorComponent as SeparatorComponentPayload,
MediaGalleryItem as MediaGalleryItemPayload,
ThumbnailComponent as ThumbnailComponentPayload,
ContainerComponent as ContainerComponentPayload,
)
from .emoji import Emoji
from .abc import Snowflake
from .state import ConnectionState
ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput']
SectionComponentType = Union['TextDisplay', 'Button']
__all__ = (
@ -80,12 +87,10 @@ __all__ = (
'TextInput',
'SelectDefaultValue',
'SectionComponent',
'TextComponent',
'ThumbnailComponent',
'MediaGalleryComponent',
'FileComponent',
'DividerComponent',
'ComponentContainer',
'SectionComponent',
)
@ -159,7 +164,7 @@ class ActionRow(Component):
component = _component_factory(component_data)
if component is not None:
self.children.append(component)
self.children.append(component) # type: ignore # should be the correct type here
@property
def type(self) -> Literal[ComponentType.action_row]:
@ -701,12 +706,12 @@ class SectionComponent(Component):
"""
def __init__(self, data: SectionComponentPayload) -> None:
self.components: List[Union[TextDisplay, Button]] = []
self.components: List[SectionComponentType] = []
for component_data in data['components']:
component = _component_factory(component_data)
if component is not None:
self.components.append(component)
self.components.append(component) # type: ignore # should be the correct type here
try:
self.accessory: Optional[Component] = _component_factory(data['accessory'])
@ -727,6 +732,43 @@ class SectionComponent(Component):
return payload
class ThumbnailComponent(Component):
"""Represents a Thumbnail from the Discord Bot UI Kit.
This inherits from :class:`Component`.
Attributes
----------
media: :class:`UnfurledAttachment`
The media for this thumbnail.
description: Optional[:class:`str`]
The description shown within this thumbnail.
spoiler: :class:`bool`
Whether this thumbnail is flagged as a spoiler.
"""
def __init__(
self,
data: ThumbnailComponentPayload,
state: ConnectionState,
) -> None:
self.media: UnfurledAttachment = UnfurledAttachment(data['media'], state)
self.description: Optional[str] = data.get('description')
self.spoiler: bool = data.get('spoiler', False)
@property
def type(self) -> Literal[ComponentType.thumbnail]:
return ComponentType.thumbnail
def to_dict(self) -> ThumbnailComponentPayload:
return {
'media': self.media.to_dict(), # type: ignroe
'description': self.description,
'spoiler': self.spoiler,
'type': self.type.value,
}
class TextDisplay(Component):
"""Represents a text display from the Discord Bot UI Kit.
@ -734,51 +776,231 @@ class TextDisplay(Component):
.. versionadded:: 2.6
Parameters
Attributes
----------
content: :class:`str`
The content that this display shows.
"""
def __init__(self, content: str) -> None:
self.content: str = content
def __init__(self, data: TextComponentPayload) -> None:
self.content: str = data['content']
@property
def type(self) -> Literal[ComponentType.text_display]:
return ComponentType.text_display
def to_dict(self) -> TextComponentPayload:
return {
'type': self.type.value,
'content': self.content,
}
class MediaGalleryItem:
"""Represents a :class:`MediaGalleryComponent` media item.
Parameters
----------
url: :class:`str`
The url of the media item. This can be a local file uploaded
as an attachment in the message, that can be accessed using
the ``attachment://file-name.extension`` format.
description: Optional[:class:`str`]
The description to show within this item.
spoiler: :class:`bool`
Whether this item should be flagged as a spoiler.
"""
__slots__ = (
'url',
'description',
'spoiler',
'_state',
)
def __init__(
self,
url: str,
*,
description: Optional[str] = None,
spoiler: bool = False,
) -> None:
self.url: str = url
self.description: Optional[str] = description
self.spoiler: bool = spoiler
self._state: Optional[ConnectionState] = None
@classmethod
def _from_data(cls, data: TextComponentPayload) -> TextDisplay:
return cls(
content=data['content'],
def _from_data(
cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]
) -> MediaGalleryItem:
media = data['media']
self = cls(
url=media['url'],
description=data.get('description'),
spoiler=data.get('spoiler', False),
)
self._state = state
return self
def to_dict(self) -> TextComponentPayload:
@classmethod
def _from_gallery(
cls,
items: List[MediaGalleryItemPayload],
state: Optional[ConnectionState],
) -> List[MediaGalleryItem]:
return [cls._from_data(item, state) for item in items]
def to_dict(self) -> MediaGalleryItemPayload:
return { # type: ignore
'media': {'url': self.url},
'description': self.description,
'spoiler': self.spoiler,
}
class MediaGalleryComponent(Component):
"""Represents a Media Gallery component from the Discord Bot UI Kit.
This inherits from :class:`Component`.
Attributes
----------
items: List[:class:`MediaGalleryItem`]
The items this gallery has.
"""
__slots__ = ('items', 'id')
def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None:
self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state)
@property
def type(self) -> Literal[ComponentType.media_gallery]:
return ComponentType.media_gallery
def to_dict(self) -> MediaGalleryComponentPayload:
return {
'id': self.id,
'type': self.type.value,
'content': self.content,
'items': [item.to_dict() for item in self.items],
}
class ThumbnailComponent(Component):
"""Represents a thumbnail display from the Discord Bot UI Kit.
class FileComponent(Component):
"""Represents a File component from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
Attributes
----------
media: :class:`UnfurledAttachment`
The unfurled attachment contents of the file.
spoiler: :class:`bool`
Whether this file is flagged as a spoiler.
"""
__slots__ = (
'media',
'spoiler',
)
The user constructuble and usable type to create a thumbnail
component is :class:`discord.ui.Thumbnail` not this one.
def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None:
self.media: UnfurledAttachment = UnfurledAttachment(
data['file'], state,
)
self.spoiler: bool = data.get('spoiler', False)
.. versionadded:: 2.6
@property
def type(self) -> Literal[ComponentType.file]:
return ComponentType.file
def to_dict(self) -> FileComponentPayload:
return { # type: ignore
'file': {'url': self.url},
'spoiler': self.spoiler,
'type': self.type.value,
}
class SeparatorComponent(Component):
"""Represents a Separator from the Discord Bot UI Kit.
This inherits from :class:`Component`.
Attributes
----------
media: :class:`ComponentMedia`
spacing: :class:`SeparatorSize`
The spacing size of the separator.
divider: :class:`bool`
Whether this separator is a divider.
"""
__slots__ = (
'spacing',
'divider',
)
def __init__(
self,
data: SeparatorComponentPayload,
) -> None:
self.spacing: SeparatorSize = try_enum(SeparatorSize, data.get('spacing', 1))
self.divider: bool = data.get('divider', True)
@property
def type(self) -> Literal[ComponentType.separator]:
return ComponentType.separator
def to_dict(self) -> SeparatorComponentPayload:
return {
'type': self.type.value,
'divider': self.divider,
'spacing': self.spacing.value,
}
class Container(Component):
"""Represents a Container from the Discord Bot UI Kit.
This inherits from :class:`Component`.
Attributes
----------
children: :class:`Component`
This container's children.
spoiler: :class:`bool`
Whether this container is flagged as a spoiler.
"""
def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None:
self.children: List[Component] = []
for child in data['components']:
comp = _component_factory(child, state)
if comp:
self.children.append(comp)
self.spoiler: bool = data.get('spoiler', False)
self._colour: Optional[Colour]
try:
self._colour = Colour(data['accent_color'])
except KeyError:
self._colour = None
@property
def accent_colour(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: The container's accent colour."""
return self._colour
accent_color = accent_colour
"""Optional[:class:`Color`]: The container's accent color."""
def _component_factory(data: ComponentPayload) -> Optional[Component]:
def _component_factory(
data: ComponentPayload, state: Optional[ConnectionState] = None
) -> Optional[Component]:
if data['type'] == 1:
return ActionRow(data)
elif data['type'] == 2:
@ -790,14 +1012,12 @@ def _component_factory(data: ComponentPayload) -> Optional[Component]:
elif data['type'] == 9:
return SectionComponent(data)
elif data['type'] == 10:
return TextDisplay._from_data(data)
elif data['type'] == 11:
return ThumbnailComponent(data)
return TextDisplay(data)
elif data['type'] == 12:
return MediaGalleryComponent(data)
return MediaGalleryComponent(data, state)
elif data['type'] == 13:
return FileComponent(data)
return FileComponent(data, state)
elif data['type'] == 14:
return DividerComponent(data)
return SeparatorComponent(data)
elif data['type'] == 17:
return ComponentContainer(data)
return Container(data, state)

4
discord/enums.py

@ -77,7 +77,7 @@ __all__ = (
'VoiceChannelEffectAnimationType',
'SubscriptionStatus',
'MessageReferenceType',
'DividerSize',
'SeparatorSize',
'MediaLoadingState',
)
@ -872,7 +872,7 @@ class SubscriptionStatus(Enum):
inactive = 2
class DividerSize(Enum):
class SeparatorSize(Enum):
small = 1
large = 2

2
discord/message.py

@ -238,7 +238,7 @@ class MessageSnapshot:
self.components: List[MessageComponentType] = []
for component_data in data.get('components', []):
component = _component_factory(component_data)
component = _component_factory(component_data, state)
if component is not None:
self.components.append(component)

6
discord/types/attachment.py

@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Literal, Optional, TypedDict
from typing_extensions import NotRequired
from typing_extensions import NotRequired, Required
from .snowflake import Snowflake
@ -52,5 +52,5 @@ class Attachment(AttachmentBase):
title: NotRequired[str]
class UnfurledAttachment(AttachmentBase):
loading_state: LoadingState
class UnfurledAttachment(AttachmentBase, total=False):
loading_state: Required[LoadingState]

29
discord/types/components.py

@ -24,14 +24,14 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import List, Literal, TypedDict, Union
from typing import List, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
from .emoji import PartialEmoji
from .channel import ChannelType
from .attachment import UnfurledAttachment
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]
TextStyle = Literal[1, 2]
DefaultValueType = Literal["user", "role", "channel"]
@ -137,13 +137,22 @@ class TextComponent(ComponentBase):
content: str
class ThumbnailComponent(ComponentBase, UnfurledAttachment):
class ThumbnailComponent(ComponentBase):
type: Literal[11]
media: UnfurledAttachment
description: NotRequired[Optional[str]]
spoiler: NotRequired[bool]
class MediaGalleryItem(TypedDict):
media: UnfurledAttachment
description: NotRequired[Optional[str]]
spoiler: NotRequired[bool]
class MediaGalleryComponent(ComponentBase):
type: Literal[12]
items: List[UnfurledAttachment]
items: List[MediaGalleryItem]
class FileComponent(ComponentBase):
@ -152,26 +161,28 @@ class FileComponent(ComponentBase):
spoiler: NotRequired[bool]
class DividerComponent(ComponentBase):
class SeparatorComponent(ComponentBase):
type: Literal[14]
divider: NotRequired[bool]
spacing: NotRequired[DividerSize]
class ComponentContainer(ComponentBase):
class ContainerComponent(ComponentBase):
type: Literal[17]
accent_color: NotRequired[int]
spoiler: NotRequired[bool]
components: List[ContainerComponent]
components: List[ContainerChildComponent]
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput]
ContainerComponent = Union[
ContainerChildComponent = Union[
ActionRow,
TextComponent,
MediaGalleryComponent,
FileComponent,
SectionComponent,
SectionComponent,
ContainerComponent,
SeparatorComponent,
]
Component = Union[ActionRowChildComponent, ContainerComponent]
Component = Union[ActionRowChildComponent, ContainerChildComponent]

1
discord/ui/__init__.py

@ -16,3 +16,4 @@ from .button import *
from .select import *
from .text_input import *
from .dynamic import *
from .container import *

86
discord/ui/container.py

@ -0,0 +1,86 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING:
from ..components import Component
from ..colour import Colour, Color
__all__ = ('Container',)
class Container:
"""Represents a Components V2 Container.
.. versionadded:: 2.6
Parameters
----------
children: List[:class:`Item`]
The initial children of this container.
accent_colour: Optional[:class:`~discord.Colour`]
The colour of the container. Defaults to ``None``.
accent_color: Optional[:class:`~discord.Color`]
The color of the container. Defaults to ``None``.
spoiler: :class:`bool`
Whether to flag this container as a spoiler. Defaults
to ``False``.
"""
__discord_ui_container__ = True
def __init__(
self,
children: List[Component],
*,
accent_colour: Optional[Colour] = None,
accent_color: Optional[Color] = None,
spoiler: bool = False,
) -> None:
self._children: List[Component] = children
self.spoiler: bool = spoiler
self._colour = accent_colour or accent_color
@property
def children(self) -> List[Component]:
"""List[:class:`~discord.Component`]: The children of this container."""
return self._children.copy()
@children.setter
def children(self, value: List[Component]) -> None:
self._children = value
@property
def accent_colour(self) -> Optional[Colour]:
"""Optional[:class:`~discord.Colour`]: The colour of the container, or ``None``."""
return self._colour
@accent_colour.setter
def accent_colour(self, value: Optional[Colour]) -> None:
self._colour = value
accent_color = accent_colour
"""Optional[:class:`~discord.Color`]: The color of the container, or ``None``."""

55
discord/ui/section.py

@ -1,55 +0,0 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Optional
from .item import Item
from ..components import SectionComponent
__all__ = ('Section',)
class Section(Item):
"""Represents a UI section.
.. versionadded:: tbd
Parameters
----------
accessory: Optional[:class:`Item`]
The accessory to show within this section, displayed on the top right of this section.
"""
__slots__ = (
'accessory',
'_children',
)
def __init__(self, *, accessory: Optional[Item]) -> None:
self.accessory: Optional[Item] = accessory
self._children: List[Item] = []
self._underlying = SectionComponent._raw_construct(
accessory=accessory,
)

11
discord/ui/view.py

@ -40,6 +40,11 @@ from ..components import (
_component_factory,
Button as ButtonComponent,
SelectMenu as SelectComponent,
SectionComponent,
TextDisplay,
MediaGalleryComponent,
FileComponent,
SeparatorComponent,
)
# fmt: off
@ -62,6 +67,7 @@ if TYPE_CHECKING:
_log = logging.getLogger(__name__)
V2_COMPONENTS = (SectionComponent, TextDisplay, MediaGalleryComponent, FileComponent, SeparatorComponent)
def _walk_all_components(components: List[Component]) -> Iterator[Component]:
@ -81,6 +87,8 @@ def _component_to_item(component: Component) -> Item:
from .select import BaseSelect
return BaseSelect.from_component(component)
if isinstance(component, V2_COMPONENTS):
return component
return Item.from_component(component)
@ -157,6 +165,7 @@ class View:
__discord_ui_view__: ClassVar[bool] = True
__discord_ui_modal__: ClassVar[bool] = False
__discord_ui_container__: ClassVar[bool] = False
__view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = []
def __init_subclass__(cls) -> None:
@ -737,7 +746,7 @@ class ViewStore:
components: List[Component] = []
for component_data in data:
component = _component_factory(component_data)
component = _component_factory(component_data, self._state)
if component is not None:
components.append(component)

Loading…
Cancel
Save