From 86897182ba406936470467230c7db5cc93e9635d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Mar 2025 13:48:40 +0100 Subject: [PATCH] chore: more things to components v2 --- discord/http.py | 2 +- discord/ui/container.py | 46 ++++++++++++++++++-------------- discord/ui/section.py | 16 ++++++------ discord/ui/view.py | 55 +++++++++++++++++++++++++++++++-------- discord/webhook/async_.py | 7 +++++ 5 files changed, 86 insertions(+), 40 deletions(-) diff --git a/discord/http.py b/discord/http.py index 58b501722..d8eedeb2e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -57,6 +57,7 @@ from .file import File from .mentions import AllowedMentions from . import __version__, utils from .utils import MISSING +from .flags import MessageFlags _log = logging.getLogger(__name__) @@ -66,7 +67,6 @@ if TYPE_CHECKING: from .ui.view import View from .embeds import Embed from .message import Attachment - from .flags import MessageFlags from .poll import Poll from .types import ( diff --git a/discord/ui/container.py b/discord/ui/container.py index 4bd68b724..a2ca83a25 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -23,16 +23,16 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar +import sys +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union from .item import Item +from .view import View, _component_to_item from ..enums import ComponentType if TYPE_CHECKING: from typing_extensions import Self - from .view import View - from ..colour import Colour, Color from ..components import Container as ContainerComponent @@ -41,15 +41,16 @@ V = TypeVar('V', bound='View', covariant=True) __all__ = ('Container',) -class Container(Item[V]): +class Container(View, Item[V]): """Represents a Components V2 Container. .. versionadded:: 2.6 Parameters ---------- - children: List[:class:`Item`] - The initial children of this container. + children: List[Union[:class:`Item`, :class:`View`]] + The initial children or :class:`View`s of this container. Can have up to 10 + items. accent_colour: Optional[:class:`~discord.Colour`] The colour of the container. Defaults to ``None``. accent_color: Optional[:class:`~discord.Color`] @@ -57,31 +58,34 @@ class Container(Item[V]): spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults to ``False``. - timeout: Optional[:class:`float`] - The timeout to set to this container items. Defaults to ``180``. """ __discord_ui_container__ = True def __init__( self, - children: List[Item[Any]], + children: List[Union[Item[Any], View]], *, accent_colour: Optional[Colour] = None, accent_color: Optional[Color] = None, spoiler: bool = False, + timeout: Optional[float] = 180, ) -> None: - self._children: List[Item[Any]] = children + if len(children) > 10: + raise ValueError('maximum number of components exceeded') + self._children: List[Union[Item[Any], View]] = children self.spoiler: bool = spoiler self._colour = accent_colour or accent_color + super().__init__(timeout=timeout) + @property - def children(self) -> List[Item[Any]]: + def children(self) -> List[Union[Item[Any], View]]: """List[:class:`Item`]: The children of this container.""" return self._children.copy() @children.setter - def children(self, value: List[Item[Any]]) -> None: + def children(self, value: List[Union[Item[Any], View]]) -> None: self._children = value @property @@ -100,22 +104,24 @@ class Container(Item[V]): def type(self) -> Literal[ComponentType.container]: return ComponentType.container + @property + def _views(self) -> List[View]: + return [c for c in self._children if isinstance(c, View)] + def _is_v2(self) -> bool: return True - def to_component_dict(self) -> Dict[str, Any]: - base = { + def to_components(self) -> List[Dict[str, Any]]: + components = super().to_components() + return [{ 'type': self.type.value, + 'accent_color': self._colour.value if self._colour else None, 'spoiler': self.spoiler, - 'components': [c.to_component_dict() for c in self._children] - } - if self._colour is not None: - base['accent_color'] = self._colour.value - return base + 'components': components, + }] @classmethod def from_component(cls, component: ContainerComponent) -> Self: - from .view import _component_to_item return cls( children=[_component_to_item(c) for c in component.children], accent_colour=component.accent_colour, diff --git a/discord/ui/section.py b/discord/ui/section.py index 81a0e4ba4..5176d761b 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -58,14 +58,14 @@ class Section(Item[V]): def __init__( self, - children: List[Union[TextDisplay[Any], str]], + children: List[Union[Item[Any], str]], *, accessory: Optional[Item[Any]] = None, ) -> None: if len(children) > 3: raise ValueError('maximum number of children exceeded') - self._children: List[TextDisplay[Any]] = [ - c if isinstance(c, TextDisplay) else TextDisplay(c) for c in children + self._children: List[Item[Any]] = [ + c if isinstance(c, Item) else TextDisplay(c) for c in children ] self.accessory: Optional[Item[Any]] = accessory @@ -76,7 +76,7 @@ class Section(Item[V]): def _is_v2(self) -> bool: return True - def add_item(self, item: Union[str, TextDisplay[Any]]) -> Self: + def add_item(self, item: Union[str, Item[Any]]) -> Self: """Adds an item to this section. This function returns the class instance to allow for fluent-style @@ -98,15 +98,15 @@ class Section(Item[V]): if len(self._children) >= 3: raise ValueError('maximum number of children exceeded') - if not isinstance(item, (TextDisplay, str)): - raise TypeError(f'expected TextDisplay or str not {item.__class__.__name__}') + if not isinstance(item, (Item, str)): + raise TypeError(f'expected Item or str not {item.__class__.__name__}') self._children.append( - item if isinstance(item, TextDisplay) else TextDisplay(item), + item if isinstance(item, Item) else TextDisplay(item), ) return self - def remove_item(self, item: TextDisplay[Any]) -> Self: + def remove_item(self, item: Item[Any]) -> Self: """Removes an item from this section. This function returns the class instance to allow for fluent-style diff --git a/discord/ui/view.py b/discord/ui/view.py index 4abac5116..19bc3f33b 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type +from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type, Union from functools import partial from itertools import groupby @@ -95,13 +95,11 @@ class _ViewWeights: # fmt: off __slots__ = ( 'weights', - 'max_weight', ) # fmt: on - def __init__(self, children: List[Item], container: bool): + def __init__(self, children: List[Item]): self.weights: List[int] = [0, 0, 0, 0, 0] - self.max_weight: int = 5 if container is False else 10 key = lambda i: sys.maxsize if i.row is None else i.row children = sorted(children, key=key) @@ -109,18 +107,26 @@ class _ViewWeights: for item in group: self.add_item(item) - def find_open_space(self, item: Item) -> int: + def find_open_space(self, item: Union[Item, View]) -> int: for index, weight in enumerate(self.weights): - if weight + item.width <= self.max_weight: + if weight + item.width <= 5: return index raise ValueError('could not find open space for item') - def add_item(self, item: Item) -> None: + def add_item(self, item: Union[Item, View]) -> None: + if hasattr(item, '__discord_ui_container__') and item.__discord_ui_container__ is True: + raise TypeError( + 'containers cannot be added to views' + ) + + if item._is_v2() and not self.v2_weights(): + # v2 components allow up to 10 rows + self.weights.extend([0, 0, 0, 0, 0]) if item.row is not None: total = self.weights[item.row] + item.width if total > 10: - raise ValueError(f'item would not fit at row {item.row} ({total} > {self.max_weight} width)') + raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)') self.weights[item.row] = total item._rendered_row = item.row else: @@ -128,7 +134,7 @@ class _ViewWeights: self.weights[index] += item.width item._rendered_row = index - def remove_item(self, item: Item) -> None: + def remove_item(self, item: Union[Item, View]) -> None: if item._rendered_row is not None: self.weights[item._rendered_row] -= item.width item._rendered_row = None @@ -136,6 +142,9 @@ class _ViewWeights: def clear(self) -> None: self.weights = [0, 0, 0, 0, 0] + def v2_weights(self) -> bool: + return sum(1 if w > 0 else 0 for w in self.weights) > 5 + class _ViewCallback: __slots__ = ('view', 'callback', 'item') @@ -176,6 +185,8 @@ class View: for name, member in base.__dict__.items(): if hasattr(member, '__discord_ui_model_type__'): children[name] = member + if cls.__discord_ui_container__ and isinstance(member, View): + children[name] = member if len(children) > 25: raise TypeError('View cannot have more than 25 children') @@ -192,16 +203,25 @@ class View: children.append(item) return children - def __init__(self, *, timeout: Optional[float] = 180.0): + def __init__(self, *, timeout: Optional[float] = 180.0, row: Optional[int] = None): self.__timeout = timeout self._children: List[Item[Self]] = self._init_children() - self.__weights = _ViewWeights(self._children, self.__discord_ui_container__) + self.__weights = _ViewWeights(self._children) self.id: str = os.urandom(16).hex() self._cache_key: Optional[int] = None self.__cancel_callback: Optional[Callable[[View], None]] = None self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + self.row: Optional[int] = row + self._rendered_row: Optional[int] = None + + def _is_v2(self) -> bool: + return False + + @property + def width(self): + return 5 def __repr__(self) -> str: return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>' @@ -602,6 +622,19 @@ class ViewStore: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False + # components V2 containers allow for views to exist inside them + # with dispatchable items, so we iterate over it and add it + # to the store + if hasattr(view, '_views'): + for v in view._views: + for item in v._children: + if isinstance(item, DynamicItem): + pattern = item.__discord_ui_compiled_template__ + self._dynamic_items[pattern] = item.__class__ + elif item.is_dispatchable(): + dispatch_info[(item.type.value, item.custom_id)] = item + is_fully_dynamic = False + view._cache_key = message_id if message_id is not None and not is_fully_dynamic: self._synced_message_views[message_id] = view diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 3b62b10fa..f1cfb573b 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -592,6 +592,13 @@ def interaction_message_response_params( if view is not MISSING: if view is not None: data['components'] = view.to_components() + + if view.has_components_v2(): + if flags is not MISSING: + flags.components_v2 = True + else: + flags = MessageFlags(components_v2=True) + else: data['components'] = []