Browse Source

chore: more components v2 things and finished danny's suggested impl

pull/10166/head
DA-344 3 months ago
parent
commit
8f59216e68
  1. 2
      discord/components.py
  2. 16
      discord/ui/action_row.py
  3. 5
      discord/ui/button.py
  4. 8
      discord/ui/container.py
  5. 8
      discord/ui/dynamic.py
  6. 8
      discord/ui/file.py
  7. 16
      discord/ui/item.py
  8. 15
      discord/ui/media_gallery.py
  9. 8
      discord/ui/section.py
  10. 5
      discord/ui/select.py
  11. 8
      discord/ui/separator.py
  12. 9
      discord/ui/text_display.py
  13. 8
      discord/ui/thumbnail.py
  14. 397
      discord/ui/view.py

2
discord/components.py

@ -967,7 +967,7 @@ class MediaGalleryItem:
def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem: def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem:
media = data['media'] media = data['media']
self = cls( self = cls(
media=media['url'], media=UnfurledMediaItem._from_data(media, state),
description=data.get('description'), description=data.get('description'),
spoiler=data.get('spoiler', False), spoiler=data.get('spoiler', False),
) )

16
discord/ui/action_row.py

@ -45,7 +45,8 @@ from typing import (
from .item import Item, ItemCallbackType from .item import Item, ItemCallbackType
from .button import Button from .button import Button
from .dynamic import DynamicItem from .dynamic import DynamicItem
from .select import select as _select, Select, SelectCallbackDecorator, UserSelect, RoleSelect, ChannelSelect, MentionableSelect from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect
from ..components import ActionRow as ActionRowComponent
from ..enums import ButtonStyle, ComponentType, ChannelType from ..enums import ButtonStyle, ComponentType, ChannelType
from ..partial_emoji import PartialEmoji from ..partial_emoji import PartialEmoji
from ..utils import MISSING from ..utils import MISSING
@ -61,7 +62,8 @@ if TYPE_CHECKING:
ChannelSelectT, ChannelSelectT,
RoleSelectT, RoleSelectT,
UserSelectT, UserSelectT,
SelectT SelectT,
SelectCallbackDecorator,
) )
from ..emoji import Emoji from ..emoji import Emoji
from ..components import SelectOption from ..components import SelectOption
@ -125,7 +127,7 @@ class ActionRow(Item[V]):
for func in self.__action_row_children_items__: for func in self.__action_row_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ActionRowCallback(func, self, item) item.callback = _ActionRowCallback(func, self, item) # type: ignore
item._parent = getattr(func, '__discord_ui_parent__', self) # type: ignore item._parent = getattr(func, '__discord_ui_parent__', self) # type: ignore
setattr(self, func.__name__, item) setattr(self, func.__name__, item)
children.append(item) children.append(item)
@ -478,3 +480,11 @@ class ActionRow(Item[V]):
return r return r
return decorator # type: ignore return decorator # type: ignore
@classmethod
def from_component(cls, component: ActionRowComponent) -> ActionRow:
from .view import _component_to_item
self = cls()
for cmp in component.children:
self.add_item(_component_to_item(cmp))
return self

5
discord/ui/button.py

@ -42,12 +42,12 @@ __all__ = (
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import View from .view import BaseView
from .action_row import ActionRow from .action_row import ActionRow
from ..emoji import Emoji from ..emoji import Emoji
from ..types.components import ButtonComponent as ButtonComponentPayload from ..types.components import ButtonComponent as ButtonComponentPayload
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='BaseView', covariant=True)
class Button(Item[V]): class Button(Item[V]):
@ -147,6 +147,7 @@ class Button(Item[V]):
) )
self._parent: Optional[ActionRow] = None self._parent: Optional[ActionRow] = None
self.row = row self.row = row
self.id = custom_id
@property @property
def style(self) -> ButtonStyle: def style(self) -> ButtonStyle:

8
discord/ui/container.py

@ -26,7 +26,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar
from .item import Item from .item import Item
from .view import View, _component_to_item from .view import View, _component_to_item, LayoutView
from .dynamic import DynamicItem from .dynamic import DynamicItem
from ..enums import ComponentType from ..enums import ComponentType
from ..utils import MISSING from ..utils import MISSING
@ -37,7 +37,7 @@ if TYPE_CHECKING:
from ..colour import Colour, Color from ..colour import Colour, Color
from ..components import Container as ContainerComponent from ..components import Container as ContainerComponent
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Container',) __all__ = ('Container',)
@ -69,6 +69,8 @@ class Container(View, Item[V]):
passing an index is advised. For example, row=1 will show passing an index is advised. For example, row=1 will show
up before row=2. Defaults to ``None``, which is automatic up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 9 (i.e. zero indexed) ordering. The row number must be between 0 and 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
""" """
__discord_ui_container__ = True __discord_ui_container__ = True
@ -82,6 +84,7 @@ class Container(View, Item[V]):
spoiler: bool = False, spoiler: bool = False,
timeout: Optional[float] = 180, timeout: Optional[float] = 180,
row: Optional[int] = None, row: Optional[int] = None,
id: Optional[str] = None,
) -> None: ) -> None:
super().__init__(timeout=timeout) super().__init__(timeout=timeout)
if children is not MISSING: if children is not MISSING:
@ -95,6 +98,7 @@ class Container(View, Item[V]):
self._row: Optional[int] = None self._row: Optional[int] = None
self._rendered_row: Optional[int] = None self._rendered_row: Optional[int] = None
self.row: Optional[int] = row self.row: Optional[int] = row
self.id: Optional[str] = id
@property @property
def children(self) -> List[Item[Self]]: def children(self) -> List[Item[Self]]:

8
discord/ui/dynamic.py

@ -38,14 +38,14 @@ if TYPE_CHECKING:
from ..interactions import Interaction from ..interactions import Interaction
from ..components import Component from ..components import Component
from ..enums import ComponentType from ..enums import ComponentType
from .view import View from .view import BaseView
V = TypeVar('V', bound='View', covariant=True, default=View) V = TypeVar('V', bound='BaseView', covariant=True, default=BaseView)
else: else:
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='BaseView', covariant=True)
class DynamicItem(Generic[BaseT], Item['View']): class DynamicItem(Generic[BaseT], Item['BaseView']):
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within """Represents an item with a dynamic ``custom_id`` that can be used to store state within
that ``custom_id``. that ``custom_id``.

8
discord/ui/file.py

@ -32,9 +32,9 @@ from ..enums import ComponentType
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import View from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('File',) __all__ = ('File',)
@ -59,6 +59,8 @@ class File(Item[V]):
passing an index is advised. For example, row=1 will show passing an index is advised. For example, row=1 will show
up before row=2. Defaults to ``None``, which is automatic up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 9 (i.e. zero indexed) ordering. The row number must be between 0 and 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
""" """
def __init__( def __init__(
@ -67,6 +69,7 @@ class File(Item[V]):
*, *,
spoiler: bool = False, spoiler: bool = False,
row: Optional[int] = None, row: Optional[int] = None,
id: Optional[str] = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self._underlying = FileComponent._raw_construct( self._underlying = FileComponent._raw_construct(
@ -75,6 +78,7 @@ class File(Item[V]):
) )
self.row = row self.row = row
self.id = id
def _is_v2(self): def _is_v2(self):
return True return True

16
discord/ui/item.py

@ -37,11 +37,11 @@ __all__ = (
if TYPE_CHECKING: if TYPE_CHECKING:
from ..enums import ComponentType from ..enums import ComponentType
from .view import View from .view import BaseView
from ..components import Component from ..components import Component
I = TypeVar('I', bound='Item[Any]') I = TypeVar('I', bound='Item[Any]')
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='BaseView', covariant=True)
ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]] ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]]
@ -70,6 +70,7 @@ class Item(Generic[V]):
# actually affect the intended purpose of this check because from_component is # 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. # only called upon edit and we're mainly interested during initial creation time.
self._provided_custom_id: bool = False self._provided_custom_id: bool = False
self._id: Optional[str] = None
self._max_row: int = 5 if not self._is_v2() else 10 self._max_row: int = 5 if not self._is_v2() else 10
def to_component_dict(self) -> Dict[str, Any]: def to_component_dict(self) -> Dict[str, Any]:
@ -124,6 +125,17 @@ class Item(Generic[V]):
"""Optional[:class:`View`]: The underlying view for this item.""" """Optional[:class:`View`]: The underlying view for this item."""
return self._view return self._view
@property
def id(self) -> Optional[str]:
"""Optional[:class:`str`]: The ID of this component. For non v2 components this is the
equivalent to ``custom_id``.
"""
return self._id
@id.setter
def id(self, value: Optional[str]) -> None:
self._id = value
async def callback(self, interaction: Interaction[ClientT]) -> Any: async def callback(self, interaction: Interaction[ClientT]) -> Any:
"""|coro| """|coro|

15
discord/ui/media_gallery.py

@ -35,9 +35,9 @@ from ..components import (
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import View from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('MediaGallery',) __all__ = ('MediaGallery',)
@ -60,9 +60,17 @@ class MediaGallery(Item[V]):
passing an index is advised. For example, row=1 will show passing an index is advised. For example, row=1 will show
up before row=2. Defaults to ``None``, which is automatic up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 9 (i.e. zero indexed) ordering. The row number must be between 0 and 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
""" """
def __init__(self, items: List[MediaGalleryItem], *, row: Optional[int] = None) -> None: def __init__(
self,
items: List[MediaGalleryItem],
*,
row: Optional[int] = None,
id: Optional[str] = None,
) -> None:
super().__init__() super().__init__()
self._underlying = MediaGalleryComponent._raw_construct( self._underlying = MediaGalleryComponent._raw_construct(
@ -70,6 +78,7 @@ class MediaGallery(Item[V]):
) )
self.row = row self.row = row
self.id = id
@property @property
def items(self) -> List[MediaGalleryItem]: def items(self) -> List[MediaGalleryItem]:

8
discord/ui/section.py

@ -33,10 +33,10 @@ from ..utils import MISSING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import View from .view import LayoutView
from ..components import SectionComponent from ..components import SectionComponent
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Section',) __all__ = ('Section',)
@ -59,6 +59,8 @@ class Section(Item[V]):
passing an index is advised. For example, row=1 will show passing an index is advised. For example, row=1 will show
up before row=2. Defaults to ``None``, which is automatic up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 9 (i.e. zero indexed) ordering. The row number must be between 0 and 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
""" """
__slots__ = ( __slots__ = (
@ -72,6 +74,7 @@ class Section(Item[V]):
*, *,
accessory: Item[Any], accessory: Item[Any],
row: Optional[int] = None, row: Optional[int] = None,
id: Optional[str] = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self._children: List[Item[Any]] = [] self._children: List[Item[Any]] = []
@ -84,6 +87,7 @@ class Section(Item[V]):
self.accessory: Item[Any] = accessory self.accessory: Item[Any] = accessory
self.row = row self.row = row
self.id = id
@property @property
def type(self) -> Literal[ComponentType.section]: def type(self) -> Literal[ComponentType.section]:

5
discord/ui/select.py

@ -72,7 +72,7 @@ __all__ = (
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import TypeAlias, TypeGuard from typing_extensions import TypeAlias, TypeGuard
from .view import View from .view import BaseView
from .action_row import ActionRow from .action_row import ActionRow
from ..types.components import SelectMenu as SelectMenuPayload from ..types.components import SelectMenu as SelectMenuPayload
from ..types.interactions import SelectMessageComponentInteractionData from ..types.interactions import SelectMessageComponentInteractionData
@ -102,7 +102,7 @@ if TYPE_CHECKING:
Thread, Thread,
] ]
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='BaseView', covariant=True)
BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]') BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]')
SelectT = TypeVar('SelectT', bound='Select[Any]') SelectT = TypeVar('SelectT', bound='Select[Any]')
UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]') UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]')
@ -259,6 +259,7 @@ class BaseSelect(Item[V]):
) )
self.row = row self.row = row
self.id = custom_id if custom_id is not MISSING else None
self._parent: Optional[ActionRow] = None self._parent: Optional[ActionRow] = None
self._values: List[PossibleValue] = [] self._values: List[PossibleValue] = []

8
discord/ui/separator.py

@ -32,9 +32,9 @@ from ..enums import SeparatorSize, ComponentType
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import View from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Separator',) __all__ = ('Separator',)
@ -58,6 +58,8 @@ class Separator(Item[V]):
passing an index is advised. For example, row=1 will show passing an index is advised. For example, row=1 will show
up before row=2. Defaults to ``None``, which is automatic up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 9 (i.e. zero indexed) ordering. The row number must be between 0 and 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
""" """
def __init__( def __init__(
@ -66,6 +68,7 @@ class Separator(Item[V]):
visible: bool = True, visible: bool = True,
spacing: SeparatorSize = SeparatorSize.small, spacing: SeparatorSize = SeparatorSize.small,
row: Optional[int] = None, row: Optional[int] = None,
id: Optional[str] = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self._underlying = SeparatorComponent._raw_construct( self._underlying = SeparatorComponent._raw_construct(
@ -74,6 +77,7 @@ class Separator(Item[V]):
) )
self.row = row self.row = row
self.id = id
def _is_v2(self): def _is_v2(self):
return True return True

9
discord/ui/text_display.py

@ -32,9 +32,9 @@ from ..enums import ComponentType
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import View from .view import LayoutView
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('TextDisplay',) __all__ = ('TextDisplay',)
@ -55,13 +55,16 @@ class TextDisplay(Item[V]):
passing an index is advised. For example, row=1 will show passing an index is advised. For example, row=1 will show
up before row=2. Defaults to ``None``, which is automatic up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 9 (i.e. zero indexed) ordering. The row number must be between 0 and 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
""" """
def __init__(self, content: str, *, row: Optional[int] = None) -> None: def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[str] = None) -> None:
super().__init__() super().__init__()
self.content: str = content self.content: str = content
self.row = row self.row = row
self.id = id
def to_component_dict(self): def to_component_dict(self):
return { return {

8
discord/ui/thumbnail.py

@ -32,10 +32,10 @@ from ..components import UnfurledMediaItem
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import View from .view import LayoutView
from ..components import ThumbnailComponent from ..components import ThumbnailComponent
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Thumbnail',) __all__ = ('Thumbnail',)
@ -62,6 +62,8 @@ class Thumbnail(Item[V]):
passing an index is advised. For example, row=1 will show passing an index is advised. For example, row=1 will show
up before row=2. Defaults to ``None``, which is automatic up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 9 (i.e. zero indexed) ordering. The row number must be between 0 and 9 (i.e. zero indexed)
id: Optional[:class:`str`]
The ID of this component. This must be unique across the view.
""" """
def __init__( def __init__(
@ -71,6 +73,7 @@ class Thumbnail(Item[V]):
description: Optional[str] = None, description: Optional[str] = None,
spoiler: bool = False, spoiler: bool = False,
row: Optional[int] = None, row: Optional[int] = None,
id: Optional[str] = None,
) -> None: ) -> None:
super().__init__() super().__init__()
@ -79,6 +82,7 @@ class Thumbnail(Item[V]):
self.spoiler: bool = spoiler self.spoiler: bool = spoiler
self.row = row self.row = row
self.id = id
@property @property
def width(self): def width(self):

397
discord/ui/view.py

@ -36,6 +36,7 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Type, Type,
Union,
) )
from functools import partial from functools import partial
from itertools import groupby from itertools import groupby
@ -46,7 +47,6 @@ import sys
import time import time
import os import os
from .item import Item, ItemCallbackType from .item import Item, ItemCallbackType
from .action_row import ActionRow
from .dynamic import DynamicItem from .dynamic import DynamicItem
from ..components import ( from ..components import (
Component, Component,
@ -61,10 +61,12 @@ from ..components import (
SeparatorComponent, SeparatorComponent,
ThumbnailComponent, ThumbnailComponent,
) )
from ..utils import get as _utils_get
# fmt: off # fmt: off
__all__ = ( __all__ = (
'View', 'View',
'LayoutView',
) )
# fmt: on # fmt: on
@ -80,6 +82,8 @@ if TYPE_CHECKING:
from ..state import ConnectionState from ..state import ConnectionState
from .modal import Modal from .modal import Modal
ItemLike = Union[ItemCallbackType[Any, Any], Item[Any]]
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
@ -188,57 +192,18 @@ class _ViewCallback:
return self.callback(self.view, interaction, self.item) return self.callback(self.view, interaction, self.item)
class View: # NOTE: maybe add a deprecation warning in favour of LayoutView? class BaseView:
"""Represents a UI view. __discord_ui_view__: ClassVar[bool] = False
This object must be inherited to create a UI within Discord.
.. versionadded:: 2.0
Parameters
-----------
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
"""
__discord_ui_view__: ClassVar[bool] = True
__discord_ui_modal__: ClassVar[bool] = False __discord_ui_modal__: ClassVar[bool] = False
__discord_ui_container__: ClassVar[bool] = False __discord_ui_container__: ClassVar[bool] = False
__view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] __view_children_items__: ClassVar[List[ItemLike]] = []
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemCallbackType[Any, Any]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'):
children[name] = member
if len(children) > 25:
raise TypeError('View cannot have more than 25 children')
cls.__view_children_items__ = list(children.values())
def _init_children(self) -> List[Item[Self]]:
children = []
for func in self.__view_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(func, self, item) # type: ignore
item._view = self
setattr(self, func.__name__, item)
children.append(item)
return children
def __init__(self, *, timeout: Optional[float] = 180.0): def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
self.__timeout = timeout self.__timeout = timeout
self._children: List[Item[Self]] = self._init_children() self._children: List[Item[Self]] = self._init_children()
self.__weights = _ViewWeights(self._children)
self.id: str = os.urandom(16).hex() self.id: str = os.urandom(16).hex()
self._cache_key: Optional[int] = None self._cache_key: Optional[int] = None
self.__cancel_callback: Optional[Callable[[View], None]] = None self.__cancel_callback: Optional[Callable[[BaseView], None]] = None
self.__timeout_expiry: Optional[float] = None self.__timeout_expiry: Optional[float] = None
self.__timeout_task: Optional[asyncio.Task[None]] = None self.__timeout_task: Optional[asyncio.Task[None]] = None
self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
@ -246,12 +211,32 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
def _is_v2(self) -> bool: def _is_v2(self) -> bool:
return False return False
@property
def width(self):
return 5
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>' return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}'
def _init_children(self) -> List[Item[Self]]:
children = []
for raw in self.__view_children_items__:
if isinstance(raw, Item):
raw._view = self
parent = getattr(raw, '__discord_ui_parent__', None)
if parent and parent._view is None:
parent._view = self
item = raw
else:
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(raw, self, item) # type: ignore
item._view = self
setattr(self, raw.__name__, item)
parent = getattr(raw, '__discord_ui_parent__', None)
if parent:
if not self._is_v2():
raise RuntimeError('This view cannot have v2 items')
parent._children.append(item)
children.append(item)
return children
async def __timeout_task_impl(self) -> None: async def __timeout_task_impl(self) -> None:
while True: while True:
@ -279,24 +264,7 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
return any(c._is_v2() for c in self.children) return any(c._is_v2() for c in self.children)
def to_components(self) -> List[Dict[str, Any]]: def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int: return NotImplemented
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,
}
)
return components
def _refresh_timeout(self) -> None: def _refresh_timeout(self) -> None:
if self.__timeout: if self.__timeout:
@ -327,7 +295,7 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
return self._children.copy() return self._children.copy()
@classmethod @classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> Any:
"""Converts a message's components into a :class:`View`. """Converts a message's components into a :class:`View`.
The :attr:`.Message.components` of a message are read-only The :attr:`.Message.components` of a message are read-only
@ -341,28 +309,8 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
The message with components to convert into a view. The message with components to convert into a view.
timeout: Optional[:class:`float`] timeout: Optional[:class:`float`]
The timeout of the converted view. 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) pass
row = 0
for component in message.components:
if isinstance(component, ActionRowComponent):
for child in component.children:
item = _component_to_item(child)
item.row = row
view.add_item(item)
row += 1
else:
item = _component_to_item(component)
item.row = row
view.add_item(item)
return view
def add_item(self, item: Item[Any]) -> Self: def add_item(self, item: Item[Any]) -> Self:
"""Adds an item to the view. """Adds an item to the view.
@ -385,18 +333,10 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
you tried to add is not allowed in this View. you tried to add is not allowed in this View.
""" """
if len(self._children) >= 25:
raise ValueError('maximum number of children exceeded')
if not isinstance(item, Item): if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}') raise TypeError(f'expected Item not {item.__class__.__name__}')
if item._is_v2() and not self._is_v2(): if item._is_v2() and not self._is_v2():
raise ValueError( raise ValueError('v2 items cannot be added to this view')
'The item can only be added on LayoutView'
)
self.__weights.add_item(item)
item._view = self item._view = self
self._children.append(item) self._children.append(item)
@ -418,8 +358,6 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
self._children.remove(item) self._children.remove(item)
except ValueError: except ValueError:
pass pass
else:
self.__weights.remove_item(item)
return self return self
def clear_items(self) -> Self: def clear_items(self) -> Self:
@ -429,9 +367,30 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
chaining. chaining.
""" """
self._children.clear() self._children.clear()
self.__weights.clear()
return self return self
def get_item_by_id(self, id: str, /) -> Optional[Item[Self]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
.. versionadded:: 2.6
Parameters
----------
id: :class:`str`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self._children, id=id)
async def interaction_check(self, interaction: Interaction, /) -> bool: async def interaction_check(self, interaction: Interaction, /) -> bool:
"""|coro| """|coro|
@ -599,61 +558,167 @@ class View: # NOTE: maybe add a deprecation warning in favour of LayoutView?
return await self.__stopped return await self.__stopped
class LayoutView(View): class View(BaseView): # NOTE: maybe add a deprecation warning in favour of LayoutView?
__view_children_items__: ClassVar[List[Item[Any]]] = [] """Represents a UI view.
__view_pending_children__: ClassVar[List[ItemCallbackType[Any, Any]]] = []
This object must be inherited to create a UI within Discord.
.. versionadded:: 2.0
Parameters
-----------
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
"""
__discord_ui_view__: ClassVar[bool] = True
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemCallbackType[Any, Any]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'):
children[name] = member
if len(children) > 25:
raise TypeError('View cannot have more than 25 children')
cls.__view_children_items__ = list(children.values())
def __init__(self, *, timeout: Optional[float] = 180.0):
super().__init__(timeout=timeout)
self.__weights = _ViewWeights(self._children)
@property
def width(self):
return 5
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,
}
)
return components
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
"""Converts a message's components into a :class:`View`.
The :attr:`.Message.components` of a message are read-only
and separate types from those in the ``discord.ui`` namespace.
In order to modify and edit message components they must be
converted into a :class:`View` first.
Parameters
-----------
message: :class:`discord.Message`
The message with 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)
row = 0
for component in message.components:
if isinstance(component, ActionRowComponent):
for child in component.children:
item = _component_to_item(child)
item.row = row
if item._is_v2():
raise RuntimeError('v2 components cannot be added to this View')
view.add_item(item)
row += 1
else:
item = _component_to_item(component)
item.row = row
if item._is_v2():
raise RuntimeError('v2 components cannot be added to this View')
view.add_item(item)
return view
def __init__(self, *, timeout: Optional[float] = 180) -> None: def add_item(self, item: Item[Any]) -> Self:
if len(self._children) >= 25:
raise ValueError('maximum number of children exceeded')
super().add_item(item)
try:
self.__weights.add_item(item)
except ValueError as e:
# if the item has no space left then remove it from _children
self._children.remove(item)
raise e
return self
def remove_item(self, item: Item[Any]) -> Self:
try:
self._children.remove(item)
except ValueError:
pass
else:
self.__weights.remove_item(item)
return self
def clear_items(self) -> Self:
super().clear_items()
self.__weights.clear()
return self
class LayoutView(BaseView):
"""Represents a layout view for components v2.
Unline :class:`View` this allows for components v2 to exist
within it.
.. versionadded:: 2.6
Parameters
----------
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
"""
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
super().__init__(timeout=timeout) super().__init__(timeout=timeout)
self.__weights.weights.extend([0, 0, 0, 0, 0])
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
children: Dict[str, Item[Any]] = {} children: Dict[str, Item[Any]] = {}
pending: Dict[str, ItemCallbackType[Any, Any]] = {}
for base in reversed(cls.__mro__): for base in reversed(cls.__mro__):
for name, member in base.__dict__.items(): for name, member in base.__dict__.items():
if isinstance(member, Item): if isinstance(member, Item):
children[name] = member children[name] = member
elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
pending[name] = member children[name] = member
if len(children) > 10: if len(children) > 10:
raise TypeError('LayoutView cannot have more than 10 top-level children') raise TypeError('LayoutView cannot have more than 10 top-level children')
cls.__view_children_items__ = list(children.values()) cls.__view_children_items__ = list(children.values())
cls.__view_pending_children__ = list(pending.values())
def _init_children(self) -> List[Item[Self]]:
children = []
for func in self.__view_pending_children__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(func, self, item)
item._view = self
setattr(self, func.__name__, item)
parent: ActionRow = func.__discord_ui_parent__
parent.add_item(item)
for i in self.__view_children_items__:
if isinstance(i, Item):
if getattr(i, '_parent', None):
# this is for ActionRows which have decorators such as
# @action_row.button and @action_row.select that will convert
# those callbacks into their types but will have a _parent
# attribute which is checked here so the item is not added twice
continue
i._view = self
if getattr(i, '__discord_ui_action_row__', False):
i._update_children_view(self) # type: ignore
children.append(i)
else:
# guard just in case
raise TypeError(
'LayoutView can only have items'
)
return children
def _is_v2(self) -> bool: def _is_v2(self) -> bool:
return True return True
@ -670,11 +735,49 @@ class LayoutView(View):
return child return child
def add_item(self, item: Item[Any]) -> Self:
if len(self._children) >= 10:
raise ValueError('maximum number of children exceeded')
super().add_item(item)
return self
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> LayoutView:
"""Converts a message's components into a :class:`LayoutView`.
The :attr:`.Message.components` of a message are read-only
and separate types from those in the ``discord.ui`` namespace.
In order to modify and edit message components they must be
converted into a :class:`LayoutView` first.
Unlike :meth:`View.from_message` this works for
Parameters
-----------
message: :class:`discord.Message`
The message with components to convert into a view.
timeout: Optional[:class:`float`]
The timeout of the converted view.
Returns
--------
:class:`LayoutView`
The converted view. This always returns a :class:`LayoutView` and not
one of its subclasses.
"""
view = LayoutView(timeout=timeout)
for component in message.components:
item = _component_to_item(component)
item.row = 0
view.add_item(item)
return view
class ViewStore: class ViewStore:
def __init__(self, state: ConnectionState): def __init__(self, state: ConnectionState):
# entity_id: {(component_type, custom_id): Item} # entity_id: {(component_type, custom_id): Item}
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[View]]] = {} self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {}
# message_id: View # message_id: View
self._synced_message_views: Dict[int, View] = {} self._synced_message_views: Dict[int, View] = {}
# custom_id: Modal # custom_id: Modal
@ -684,7 +787,7 @@ class ViewStore:
self._state: ConnectionState = state self._state: ConnectionState = state
@property @property
def persistent_views(self) -> Sequence[View]: def persistent_views(self) -> Sequence[BaseView]:
# fmt: off # fmt: off
views = { views = {
item.view.id: item.view item.view.id: item.view
@ -722,7 +825,7 @@ class ViewStore:
is_fully_dynamic = item._update_store_data( # type: ignore is_fully_dynamic = item._update_store_data( # type: ignore
dispatch_info, dispatch_info,
self._dynamic_items, self._dynamic_items,
) ) or is_fully_dynamic
elif getattr(item, '__discord_ui_action_row__', False): elif getattr(item, '__discord_ui_action_row__', False):
is_fully_dynamic = item._update_store_data( # type: ignore is_fully_dynamic = item._update_store_data( # type: ignore
dispatch_info, dispatch_info,
@ -784,7 +887,7 @@ class ViewStore:
return return
# Swap the item in the view with our new dynamic item # Swap the item in the view with our new dynamic item
view._children[base_item_index] = item view._children[base_item_index] = item # type: ignore
item._view = view item._view = view
item._rendered_row = base_item._rendered_row item._rendered_row = base_item._rendered_row
item._refresh_state(interaction, interaction.data) # type: ignore item._refresh_state(interaction, interaction.data) # type: ignore
@ -826,7 +929,7 @@ class ViewStore:
key = (component_type, custom_id) key = (component_type, custom_id)
# The entity_id can either be message_id, interaction_id, or None in that priority order. # The entity_id can either be message_id, interaction_id, or None in that priority order.
item: Optional[Item[View]] = None item: Optional[Item[BaseView]] = None
if message_id is not None: if message_id is not None:
item = self._views.get(message_id, {}).get(key) item = self._views.get(message_id, {}).get(key)
@ -878,7 +981,7 @@ class ViewStore:
def is_message_tracked(self, message_id: int) -> bool: def is_message_tracked(self, message_id: int) -> bool:
return message_id in self._synced_message_views return message_id in self._synced_message_views
def remove_message_tracking(self, message_id: int) -> Optional[View]: def remove_message_tracking(self, message_id: int) -> Optional[BaseView]:
return self._synced_message_views.pop(message_id, None) return self._synced_message_views.pop(message_id, None)
def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None: def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None:

Loading…
Cancel
Save