From e30c16afa9e79f7254d5b71d1bcadb84de574123 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 4 Oct 2023 21:22:21 +0200 Subject: [PATCH 01/24] fix(docs): move `SelectOption` to Discord Models section --- docs/api/components.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/api/components.rst b/docs/api/components.rst index 5702e55238..7a3941de2e 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -98,6 +98,14 @@ UserSelectMenu :members: :inherited-members: +SelectOption +~~~~~~~~~~~~ + +.. attributetable:: SelectOption + +.. autoclass:: SelectOption + :members: + TextInput ~~~~~~~~~ @@ -107,17 +115,6 @@ TextInput :members: :inherited-members: -Data Classes -------------- - -SelectOption -~~~~~~~~~~~~ - -.. attributetable:: SelectOption - -.. autoclass:: SelectOption - :members: - Enumerations ------------ From d71670b856645c77b03b18eb4e5f9d05363cd094 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 4 Oct 2023 21:51:05 +0200 Subject: [PATCH 02/24] feat(components): implement `default_values` --- disnake/components.py | 72 ++++++++++++++++++++++++++++++++++++- disnake/enums.py | 10 ++++++ disnake/types/components.py | 9 +++++ docs/api/components.rst | 8 +++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/disnake/components.py b/disnake/components.py index e6f3d14904..cd9177bac8 100644 --- a/disnake/components.py +++ b/disnake/components.py @@ -17,7 +17,14 @@ cast, ) -from .enums import ButtonStyle, ChannelType, ComponentType, TextInputStyle, try_enum +from .enums import ( + ButtonStyle, + ChannelType, + ComponentType, + SelectDefaultValueType, + TextInputStyle, + try_enum, +) from .partial_emoji import PartialEmoji, _EmojiTag from .utils import MISSING, assert_never, get_slots @@ -33,6 +40,7 @@ Component as ComponentPayload, MentionableSelectMenu as MentionableSelectMenuPayload, RoleSelectMenu as RoleSelectMenuPayload, + SelectDefaultValue as SelectDefaultValuePayload, SelectOption as SelectOptionPayload, StringSelectMenu as StringSelectMenuPayload, TextInput as TextInputPayload, @@ -51,6 +59,7 @@ "MentionableSelectMenu", "ChannelSelectMenu", "SelectOption", + "SelectDefaultValue", "TextInput", ) @@ -261,6 +270,11 @@ class BaseSelectMenu(Component): A list of options that can be selected in this select menu. disabled: :class:`bool` Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + A list of default values (users/roles/channels) that are selected by default. + Only available for auto-populated select menus. + + .. versionadded:: 2.10 """ __slots__: Tuple[str, ...] = ( @@ -269,6 +283,7 @@ class BaseSelectMenu(Component): "min_values", "max_values", "disabled", + "default_values", ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -280,6 +295,9 @@ def __init__(self, data: BaseSelectMenuPayload) -> None: self.min_values: int = data.get("min_values", 1) self.max_values: int = data.get("max_values", 1) self.disabled: bool = data.get("disabled", False) + self.default_values: List[SelectDefaultValue] = [ + SelectDefaultValue._from_dict(d) for d in (data.get("default_values") or []) + ] def to_dict(self) -> BaseSelectMenuPayload: payload: BaseSelectMenuPayload = { @@ -293,6 +311,9 @@ def to_dict(self) -> BaseSelectMenuPayload: if self.placeholder: payload["placeholder"] = self.placeholder + if self.default_values: + payload["default_values"] = [v.to_dict() for v in self.default_values] + return payload @@ -368,6 +389,10 @@ class UserSelectMenu(BaseSelectMenu): Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + A list of default values (users/members) that are selected by default. + + .. versionadded:: 2.10 """ __slots__: Tuple[str, ...] = () @@ -401,6 +426,10 @@ class RoleSelectMenu(BaseSelectMenu): Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + A list of default values (roles) that are selected by default. + + .. versionadded:: 2.10 """ __slots__: Tuple[str, ...] = () @@ -434,6 +463,10 @@ class MentionableSelectMenu(BaseSelectMenu): Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + A list of default values (users/roles) that are selected by default. + + .. versionadded:: 2.10 """ __slots__: Tuple[str, ...] = () @@ -470,6 +503,10 @@ class ChannelSelectMenu(BaseSelectMenu): channel_types: Optional[List[:class:`ChannelType`]] A list of channel types that can be selected in this select menu. If ``None``, channels of all types may be selected. + default_values: List[:class:`SelectDefaultValue`] + A list of default values (channels) that are selected by default. + + .. versionadded:: 2.10 """ __slots__: Tuple[str, ...] = ("channel_types",) @@ -597,6 +634,39 @@ def to_dict(self) -> SelectOptionPayload: return payload +class SelectDefaultValue: + """Represents a default value of an auto-populated select menu (currently all + select menu types except :class:`StringSelectMenu`). + + Depending on the :attr:`type` attribute, this can represent different types of objects. + + .. versionadded:: 2.10 + + Attributes + ---------- + id: :class:`int` + The ID of the target object. + type: :class:`SelectDefaultValueType` + The type of the target object. + """ + + __slots__: Tuple[str, ...] = ("id", "type") + + def __init__(self, id: int, type: SelectDefaultValueType) -> None: + self.id: int = id + self.type: SelectDefaultValueType = type + + @classmethod + def _from_dict(cls, data: SelectDefaultValuePayload) -> Self: + return cls(int(data["id"]), try_enum(SelectDefaultValueType, data["type"])) + + def to_dict(self) -> SelectDefaultValuePayload: + return { + "id": self.id, + "type": self.type.value, + } + + class TextInput(Component): """Represents a text input from the Discord Bot UI Kit. diff --git a/disnake/enums.py b/disnake/enums.py index 29835a8715..5eaccf0f20 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -45,6 +45,7 @@ "ComponentType", "ButtonStyle", "TextInputStyle", + "SelectDefaultValueType", "StagePrivacyLevel", "InteractionType", "InteractionResponseType", @@ -669,6 +670,15 @@ def __int__(self) -> int: return self.value +class SelectDefaultValueType(Enum): + user = "user" + role = "role" + channel = "channel" + + def __str__(self) -> str: + return self.value + + class ApplicationCommandType(Enum): chat_input = 1 user = 2 diff --git a/disnake/types/components.py b/disnake/types/components.py index 0ca01cbd1b..14d7c29c55 100644 --- a/disnake/types/components.py +++ b/disnake/types/components.py @@ -8,11 +8,13 @@ from .channel import ChannelType from .emoji import PartialEmoji +from .snowflake import Snowflake ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8] ButtonStyle = Literal[1, 2, 3, 4, 5] TextInputStyle = Literal[1, 2] +SelectDefaultValueType = Literal["user", "role", "channel"] Component = Union["ActionRow", "ButtonComponent", "AnySelectMenu", "TextInput"] @@ -40,12 +42,19 @@ class SelectOption(TypedDict): default: NotRequired[bool] +class SelectDefaultValue(TypedDict): + id: Snowflake + type: SelectDefaultValueType + + class _SelectMenu(TypedDict): custom_id: str placeholder: NotRequired[str] min_values: NotRequired[int] max_values: NotRequired[int] disabled: NotRequired[bool] + # This is technically not applicable to string selects, but for simplicity we'll just have it here + default_values: NotRequired[List[SelectDefaultValue]] class BaseSelectMenu(_SelectMenu): diff --git a/docs/api/components.rst b/docs/api/components.rst index 7a3941de2e..2d50e41957 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -106,6 +106,14 @@ SelectOption .. autoclass:: SelectOption :members: +SelectDefaultValue +~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SelectDefaultValue + +.. autoclass:: SelectDefaultValue + :members: + TextInput ~~~~~~~~~ From 922ff4567fdd6c4a55ad1b1366a814fe32e16187 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Thu, 5 Oct 2023 23:00:39 +0200 Subject: [PATCH 03/24] fix: move `ui` import in `abc` module into method to avoid cyclic imports later on --- disnake/abc.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/disnake/abc.py b/disnake/abc.py index 605bb725aa..9a9c91dbc3 100644 --- a/disnake/abc.py +++ b/disnake/abc.py @@ -43,7 +43,6 @@ from .permissions import PermissionOverwrite, Permissions from .role import Role from .sticker import GuildSticker, StickerItem -from .ui.action_row import components_to_dict from .utils import _overload_with_permissions from .voice_client import VoiceClient, VoiceProtocol @@ -1701,16 +1700,14 @@ async def send( if view is not None and components is not None: raise TypeError("cannot pass both view and components parameter to send()") - elif view: if not hasattr(view, "__discord_ui_view__"): raise TypeError(f"view parameter must be View not {view.__class__!r}") - components_payload = view.to_components() - elif components: - components_payload = components_to_dict(components) + from .ui.action_row import components_to_dict + components_payload = components_to_dict(components) else: components_payload = None From 8f49e508303f0a013cae3ef7153100f95e756b22 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Thu, 5 Oct 2023 23:05:17 +0200 Subject: [PATCH 04/24] refactor: rename `select.base.Object` to `ItemShape` to avoid mixups --- disnake/ui/button.py | 6 +++--- disnake/ui/item.py | 2 +- disnake/ui/select/base.py | 4 ++-- disnake/ui/select/channel.py | 6 +++--- disnake/ui/select/mentionable.py | 6 +++--- disnake/ui/select/role.py | 6 +++--- disnake/ui/select/string.py | 6 +++--- disnake/ui/select/user.py | 6 +++--- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/disnake/ui/button.py b/disnake/ui/button.py index 8bb1e60ff2..88ca18faab 100644 --- a/disnake/ui/button.py +++ b/disnake/ui/button.py @@ -21,7 +21,7 @@ from ..enums import ButtonStyle, ComponentType from ..partial_emoji import PartialEmoji, _EmojiTag from ..utils import MISSING -from .item import DecoratedItem, Item, Object +from .item import DecoratedItem, Item, ItemShape __all__ = ( "Button", @@ -269,13 +269,13 @@ def button( @overload def button( - cls: Type[Object[B_co, P]], *_: P.args, **kwargs: P.kwargs + cls: Type[ItemShape[B_co, P]], *_: P.args, **kwargs: P.kwargs ) -> Callable[[ItemCallbackType[B_co]], DecoratedItem[B_co]]: ... def button( - cls: Type[Object[B_co, P]] = Button[Any], **kwargs: Any + cls: Type[ItemShape[B_co, P]] = Button[Any], **kwargs: Any ) -> Callable[[ItemCallbackType[B_co]], DecoratedItem[B_co]]: """A decorator that attaches a button to a component. diff --git a/disnake/ui/item.py b/disnake/ui/item.py index 971ca8dcb3..2b9b73ac23 100644 --- a/disnake/ui/item.py +++ b/disnake/ui/item.py @@ -180,7 +180,7 @@ def __get__(self, obj: Any, objtype: Any) -> I_co: P = ParamSpec("P") -class Object(Protocol[T_co, P]): +class ItemShape(Protocol[T_co, P]): def __new__(cls) -> T_co: ... diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index cea174000d..59caa101f4 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -21,7 +21,7 @@ from ...components import AnySelectMenu from ...enums import ComponentType from ...utils import MISSING -from ..item import DecoratedItem, Item, Object +from ..item import DecoratedItem, Item, ItemShape __all__ = ("BaseSelect",) @@ -173,7 +173,7 @@ def is_dispatchable(self) -> bool: def _create_decorator( - cls: Type[Object[S_co, P]], + cls: Type[ItemShape[S_co, P]], # only for input validation base_cls: Type[BaseSelect[Any, Any, Any]], /, diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index a455172799..20e487b94b 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -13,7 +13,7 @@ from typing_extensions import Self from ...interactions.base import InteractionChannel - from ..item import DecoratedItem, ItemCallbackType, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -162,13 +162,13 @@ def channel_select( @overload def channel_select( - cls: Type[Object[S_co, P]], *_: P.args, **kwargs: P.kwargs + cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: ... def channel_select( - cls: Type[Object[S_co, P]] = ChannelSelect[Any], + cls: Type[ItemShape[S_co, P]] = ChannelSelect[Any], /, **kwargs: Any, ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index c9e5802f78..12fb367628 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -15,7 +15,7 @@ from ...member import Member from ...role import Role from ...user import User - from ..item import DecoratedItem, ItemCallbackType, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -138,13 +138,13 @@ def mentionable_select( @overload def mentionable_select( - cls: Type[Object[S_co, P]], *_: P.args, **kwargs: P.kwargs + cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: ... def mentionable_select( - cls: Type[Object[S_co, P]] = MentionableSelect[Any], + cls: Type[ItemShape[S_co, P]] = MentionableSelect[Any], /, **kwargs: Any, ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 4644b9a660..3af80565ba 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -13,7 +13,7 @@ from typing_extensions import Self from ...role import Role - from ..item import DecoratedItem, ItemCallbackType, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -136,13 +136,13 @@ def role_select( @overload def role_select( - cls: Type[Object[S_co, P]], *_: P.args, **kwargs: P.kwargs + cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: ... def role_select( - cls: Type[Object[S_co, P]] = RoleSelect[Any], + cls: Type[ItemShape[S_co, P]] = RoleSelect[Any], /, **kwargs: Any, ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: diff --git a/disnake/ui/select/string.py b/disnake/ui/select/string.py index 0a975c2aa8..c5cebe2864 100644 --- a/disnake/ui/select/string.py +++ b/disnake/ui/select/string.py @@ -26,7 +26,7 @@ from ...emoji import Emoji from ...partial_emoji import PartialEmoji - from ..item import DecoratedItem, ItemCallbackType, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -262,13 +262,13 @@ def string_select( @overload def string_select( - cls: Type[Object[S_co, P]], *_: P.args, **kwargs: P.kwargs + cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: ... def string_select( - cls: Type[Object[S_co, P]] = StringSelect[Any], + cls: Type[ItemShape[S_co, P]] = StringSelect[Any], /, **kwargs: Any, ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index 9a995739fc..8398b4e63c 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -14,7 +14,7 @@ from ...member import Member from ...user import User - from ..item import DecoratedItem, ItemCallbackType, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -137,13 +137,13 @@ def user_select( @overload def user_select( - cls: Type[Object[S_co, P]], *_: P.args, **kwargs: P.kwargs + cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: ... def user_select( - cls: Type[Object[S_co, P]] = UserSelect[Any], + cls: Type[ItemShape[S_co, P]] = UserSelect[Any], /, **kwargs: Any, ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: From e823f5d8bcbbfa735bcfd3226045ad37ab19562c Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 15:30:52 +0200 Subject: [PATCH 05/24] feat(ui): implement `default_values` handling --- disnake/ui/select/base.py | 61 ++++++++++++++++++++++++++++++-- disnake/ui/select/channel.py | 30 ++++++++++++++-- disnake/ui/select/mentionable.py | 28 +++++++++++---- disnake/ui/select/role.py | 18 +++++++--- disnake/ui/select/string.py | 4 +++ disnake/ui/select/user.py | 20 ++++++++--- 6 files changed, 140 insertions(+), 21 deletions(-) diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index 59caa101f4..3cbaacebc2 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -9,18 +9,23 @@ TYPE_CHECKING, Any, Callable, + ClassVar, Generic, List, + Mapping, Optional, + Sequence, Tuple, Type, TypeVar, + Union, get_origin, ) -from ...components import AnySelectMenu -from ...enums import ComponentType -from ...utils import MISSING +from ...components import AnySelectMenu, SelectDefaultValue +from ...enums import ComponentType, SelectDefaultValueType +from ...object import Object +from ...utils import MISSING, humanize_list from ..item import DecoratedItem, Item, ItemShape __all__ = ("BaseSelect",) @@ -28,6 +33,7 @@ if TYPE_CHECKING: from typing_extensions import ParamSpec, Self + from ...abc import Snowflake from ...interactions import MessageInteraction from ..item import ItemCallbackType from ..view import View @@ -42,6 +48,10 @@ SelectValueT = TypeVar("SelectValueT") P = ParamSpec("P") +SelectDefaultValueMultiInputType = Union[SelectValueT, SelectDefaultValue] +# almost the same as above, but with `Object`; used for selects where the type isn't ambiguous (i.e. all except mentionable select) +SelectDefaultValueInputType = Union[SelectDefaultValueMultiInputType[SelectValueT], Object] + class BaseSelect(Generic[SelectMenuT, SelectValueT, V_co], Item[V_co], ABC): """Represents an abstract UI select menu. @@ -64,10 +74,14 @@ class BaseSelect(Generic[SelectMenuT, SelectValueT, V_co], Item[V_co], ABC): "min_values", "max_values", "disabled", + "default_values", ) # We have to set this to MISSING in order to overwrite the abstract property from WrappedComponent _underlying: SelectMenuT = MISSING + # Subclasses are expected to set this + _default_value_type_map: ClassVar[Mapping[Tuple[Type[Snowflake], ...], SelectDefaultValueType]] + def __init__( self, underlying_type: Type[SelectMenuT], @@ -78,6 +92,7 @@ def __init__( min_values: int, max_values: int, disabled: bool, + default_values: Optional[Sequence[SelectDefaultValueInputType[SelectValueT]]], row: Optional[int], ) -> None: super().__init__() @@ -91,6 +106,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=self._transform_default_values(default_values) if default_values else [], ) self.row = row @@ -171,6 +187,45 @@ def is_dispatchable(self) -> bool: """ return True + @classmethod + def _transform_default_values( + cls, values: Sequence[SelectDefaultValueInputType[SelectValueT]] + ) -> List[SelectDefaultValue]: + result: List[SelectDefaultValue] = [] + default_value_types = set(cls._default_value_type_map.values()) + + for value in values: + # If we have a SelectDefaultValue, just use it as-is + if isinstance(value, SelectDefaultValue): + if value.type not in default_value_types: + allowed_types = [str(t) for t in default_value_types] + raise ValueError( + f"SelectDefaultValue.type should be {humanize_list(allowed_types, 'or')}, not {value.type}" + ) + result.append(value) + continue + + # Otherwise, look through the list of allowed input types and + # get the associated SelectDefaultValueType + for ( + types, + value_type, # noqa: B007 # we use value_type outside of the loop + ) in cls._default_value_type_map.items(): + if isinstance(value, types): + break + else: + allowed_types = [ + t.__name__ for ts in cls._default_value_type_map.keys() for t in ts + ] + allowed_types.append(SelectDefaultValue.__name__) + raise TypeError( + f"Expected type of default value to be {humanize_list(allowed_types, 'or')}, not {type(value)!r}" + ) + + result.append(SelectDefaultValue(value.id, value_type)) + + return result + def _create_decorator( cls: Type[ItemShape[S_co, P]], diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index 20e487b94b..b7d4418df9 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -2,12 +2,27 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Type, TypeVar, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + List, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + overload, +) +from ...abc import GuildChannel +from ...channel import PartialMessageable from ...components import ChannelSelectMenu -from ...enums import ChannelType, ComponentType +from ...enums import ChannelType, ComponentType, SelectDefaultValueType +from ...object import Object +from ...threads import Thread from ...utils import MISSING -from .base import BaseSelect, P, V_co, _create_decorator +from .base import BaseSelect, P, SelectDefaultValueInputType, V_co, _create_decorator if TYPE_CHECKING: from typing_extensions import Self @@ -64,6 +79,10 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "InteractionChannel", V_co]): __repr_attributes__: Tuple[str, ...] = BaseSelect.__repr_attributes__ + ("channel_types",) + _default_value_type_map = { + (GuildChannel, Thread, PartialMessageable, Object): SelectDefaultValueType.channel, + } + @overload def __init__( self: ChannelSelect[None], @@ -73,6 +92,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, channel_types: Optional[List[ChannelType]] = None, row: Optional[int] = None, ) -> None: @@ -87,6 +107,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, channel_types: Optional[List[ChannelType]] = None, row: Optional[int] = None, ) -> None: @@ -100,6 +121,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, channel_types: Optional[List[ChannelType]] = None, row: Optional[int] = None, ) -> None: @@ -111,6 +133,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, row=row, ) self._underlying.channel_types = channel_types or None @@ -123,6 +146,7 @@ def from_component(cls, component: ChannelSelectMenu) -> Self: min_values=component.min_values, max_values=component.max_values, disabled=component.disabled, + default_values=component.default_values, channel_types=component.channel_types, row=None, ) diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index 12fb367628..4bb5b1ca94 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -2,19 +2,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, Type, TypeVar, Union, overload +from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, TypeVar, Union, overload from ...components import MentionableSelectMenu -from ...enums import ComponentType +from ...enums import ComponentType, SelectDefaultValueType +from ...member import Member +from ...role import Role +from ...user import ClientUser, User from ...utils import MISSING -from .base import BaseSelect, P, V_co, _create_decorator +from .base import BaseSelect, P, SelectDefaultValueMultiInputType, V_co, _create_decorator if TYPE_CHECKING: from typing_extensions import Self - from ...member import Member - from ...role import Role - from ...user import User from ..item import DecoratedItem, ItemCallbackType, ItemShape @@ -61,6 +61,11 @@ class MentionableSelect(BaseSelect[MentionableSelectMenu, "Union[User, Member, R A list of users, members and/or roles that have been selected by the user. """ + _default_value_type_map = { + (Member, User, ClientUser): SelectDefaultValueType.user, + (Role,): SelectDefaultValueType.role, + } + @overload def __init__( self: MentionableSelect[None], @@ -70,6 +75,9 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[ + Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]] + ] = None, row: Optional[int] = None, ) -> None: ... @@ -83,6 +91,9 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[ + Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]] + ] = None, row: Optional[int] = None, ) -> None: ... @@ -95,6 +106,9 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[ + Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]] + ] = None, row: Optional[int] = None, ) -> None: super().__init__( @@ -105,6 +119,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, row=row, ) @@ -116,6 +131,7 @@ def from_component(cls, component: MentionableSelectMenu) -> Self: min_values=component.min_values, max_values=component.max_values, disabled=component.disabled, + default_values=component.default_values, row=None, ) diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 3af80565ba..1f9b8465b6 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -2,17 +2,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, Type, TypeVar, overload +from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, TypeVar, overload from ...components import RoleSelectMenu -from ...enums import ComponentType +from ...enums import ComponentType, SelectDefaultValueType +from ...object import Object +from ...role import Role from ...utils import MISSING -from .base import BaseSelect, P, V_co, _create_decorator +from .base import BaseSelect, P, SelectDefaultValueInputType, V_co, _create_decorator if TYPE_CHECKING: from typing_extensions import Self - from ...role import Role from ..item import DecoratedItem, ItemCallbackType, ItemShape @@ -59,6 +60,10 @@ class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]): A list of roles that have been selected by the user. """ + _default_value_type_map = { + (Role, Object): SelectDefaultValueType.role, + } + @overload def __init__( self: RoleSelect[None], @@ -68,6 +73,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None, row: Optional[int] = None, ) -> None: ... @@ -81,6 +87,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None, row: Optional[int] = None, ) -> None: ... @@ -93,6 +100,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None, row: Optional[int] = None, ) -> None: super().__init__( @@ -103,6 +111,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, row=row, ) @@ -114,6 +123,7 @@ def from_component(cls, component: RoleSelectMenu) -> Self: min_values=component.min_values, max_values=component.max_values, disabled=component.disabled, + default_values=component.default_values, row=None, ) diff --git a/disnake/ui/select/string.py b/disnake/ui/select/string.py index c5cebe2864..ec9b2d51b9 100644 --- a/disnake/ui/select/string.py +++ b/disnake/ui/select/string.py @@ -98,6 +98,9 @@ class StringSelect(BaseSelect[StringSelectMenu, str, V_co]): __repr_attributes__: Tuple[str, ...] = BaseSelect.__repr_attributes__ + ("options",) + # In practice this should never be used by anything, might as well have it anyway though. + _default_value_type_map = {} + @overload def __init__( self: StringSelect[None], @@ -145,6 +148,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=None, row=row, ) self._underlying.options = [] if options is MISSING else _parse_select_options(options) diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index 8398b4e63c..ce03658196 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -2,18 +2,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, Type, TypeVar, Union, overload +from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, TypeVar, Union, overload from ...components import UserSelectMenu -from ...enums import ComponentType +from ...enums import ComponentType, SelectDefaultValueType +from ...member import Member +from ...object import Object +from ...user import ClientUser, User from ...utils import MISSING -from .base import BaseSelect, P, V_co, _create_decorator +from .base import BaseSelect, P, SelectDefaultValueInputType, V_co, _create_decorator if TYPE_CHECKING: from typing_extensions import Self - from ...member import Member - from ...user import User from ..item import DecoratedItem, ItemCallbackType, ItemShape @@ -60,6 +61,10 @@ class UserSelect(BaseSelect[UserSelectMenu, "Union[User, Member]", V_co]): A list of users/members that have been selected by the user. """ + _default_value_type_map = { + (Member, User, ClientUser, Object): SelectDefaultValueType.user, + } + @overload def __init__( self: UserSelect[None], @@ -69,6 +74,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None, row: Optional[int] = None, ) -> None: ... @@ -82,6 +88,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None, row: Optional[int] = None, ) -> None: ... @@ -94,6 +101,7 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None, row: Optional[int] = None, ) -> None: super().__init__( @@ -104,6 +112,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, row=row, ) @@ -115,6 +124,7 @@ def from_component(cls, component: UserSelectMenu) -> Self: min_values=component.min_values, max_values=component.max_values, disabled=component.disabled, + default_values=component.default_values, row=None, ) From d9f0f79f9e2f055ad7122a7eb39685fd96a584d8 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 15:35:19 +0200 Subject: [PATCH 06/24] refactor: swap keys/values of default type map this way we avoid having to create a set in _transform_default_values --- disnake/ui/select/base.py | 11 +++++------ disnake/ui/select/channel.py | 2 +- disnake/ui/select/mentionable.py | 4 ++-- disnake/ui/select/role.py | 2 +- disnake/ui/select/user.py | 2 +- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index 3cbaacebc2..62bc955225 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -80,7 +80,7 @@ class BaseSelect(Generic[SelectMenuT, SelectValueT, V_co], Item[V_co], ABC): _underlying: SelectMenuT = MISSING # Subclasses are expected to set this - _default_value_type_map: ClassVar[Mapping[Tuple[Type[Snowflake], ...], SelectDefaultValueType]] + _default_value_type_map: ClassVar[Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]]] def __init__( self, @@ -192,13 +192,12 @@ def _transform_default_values( cls, values: Sequence[SelectDefaultValueInputType[SelectValueT]] ) -> List[SelectDefaultValue]: result: List[SelectDefaultValue] = [] - default_value_types = set(cls._default_value_type_map.values()) for value in values: # If we have a SelectDefaultValue, just use it as-is if isinstance(value, SelectDefaultValue): - if value.type not in default_value_types: - allowed_types = [str(t) for t in default_value_types] + if value.type not in cls._default_value_type_map: + allowed_types = [str(t) for t in cls._default_value_type_map] raise ValueError( f"SelectDefaultValue.type should be {humanize_list(allowed_types, 'or')}, not {value.type}" ) @@ -208,14 +207,14 @@ def _transform_default_values( # Otherwise, look through the list of allowed input types and # get the associated SelectDefaultValueType for ( - types, value_type, # noqa: B007 # we use value_type outside of the loop + types, ) in cls._default_value_type_map.items(): if isinstance(value, types): break else: allowed_types = [ - t.__name__ for ts in cls._default_value_type_map.keys() for t in ts + t.__name__ for ts in cls._default_value_type_map.values() for t in ts ] allowed_types.append(SelectDefaultValue.__name__) raise TypeError( diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index b7d4418df9..405d59b5b4 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -80,7 +80,7 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "InteractionChannel", V_co]): __repr_attributes__: Tuple[str, ...] = BaseSelect.__repr_attributes__ + ("channel_types",) _default_value_type_map = { - (GuildChannel, Thread, PartialMessageable, Object): SelectDefaultValueType.channel, + SelectDefaultValueType.channel: (GuildChannel, Thread, PartialMessageable, Object), } @overload diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index 4bb5b1ca94..2636084c0b 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -62,8 +62,8 @@ class MentionableSelect(BaseSelect[MentionableSelectMenu, "Union[User, Member, R """ _default_value_type_map = { - (Member, User, ClientUser): SelectDefaultValueType.user, - (Role,): SelectDefaultValueType.role, + SelectDefaultValueType.user: (Member, User, ClientUser), + SelectDefaultValueType.role: (Role,), } @overload diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 1f9b8465b6..7b01b7ade9 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -61,7 +61,7 @@ class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]): """ _default_value_type_map = { - (Role, Object): SelectDefaultValueType.role, + SelectDefaultValueType.role: (Role, Object), } @overload diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index ce03658196..2cc6c50e44 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -62,7 +62,7 @@ class UserSelect(BaseSelect[UserSelectMenu, "Union[User, Member]", V_co]): """ _default_value_type_map = { - (Member, User, ClientUser, Object): SelectDefaultValueType.user, + SelectDefaultValueType.user: (Member, User, ClientUser, Object), } @overload From d170cc940144ceb249ae2df8310b3b6da79cf30a Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 16:06:15 +0200 Subject: [PATCH 07/24] docs(ui): add missing attribute documentation --- disnake/components.py | 15 ++++++++++----- disnake/ui/select/channel.py | 5 +++++ disnake/ui/select/mentionable.py | 7 +++++++ disnake/ui/select/role.py | 5 +++++ disnake/ui/select/user.py | 5 +++++ 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/disnake/components.py b/disnake/components.py index cd9177bac8..e0ceadc0c3 100644 --- a/disnake/components.py +++ b/disnake/components.py @@ -271,7 +271,8 @@ class BaseSelectMenu(Component): disabled: :class:`bool` Whether the select menu is disabled or not. default_values: List[:class:`SelectDefaultValue`] - A list of default values (users/roles/channels) that are selected by default. + The list of values (users/roles/channels) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. Only available for auto-populated select menus. .. versionadded:: 2.10 @@ -390,7 +391,8 @@ class UserSelectMenu(BaseSelectMenu): disabled: :class:`bool` Whether the select menu is disabled or not. default_values: List[:class:`SelectDefaultValue`] - A list of default values (users/members) that are selected by default. + The list of values (users/members) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. .. versionadded:: 2.10 """ @@ -427,7 +429,8 @@ class RoleSelectMenu(BaseSelectMenu): disabled: :class:`bool` Whether the select menu is disabled or not. default_values: List[:class:`SelectDefaultValue`] - A list of default values (roles) that are selected by default. + The list of values (roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. .. versionadded:: 2.10 """ @@ -464,7 +467,8 @@ class MentionableSelectMenu(BaseSelectMenu): disabled: :class:`bool` Whether the select menu is disabled or not. default_values: List[:class:`SelectDefaultValue`] - A list of default values (users/roles) that are selected by default. + The list of values (users/roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. .. versionadded:: 2.10 """ @@ -504,7 +508,8 @@ class ChannelSelectMenu(BaseSelectMenu): A list of channel types that can be selected in this select menu. If ``None``, channels of all types may be selected. default_values: List[:class:`SelectDefaultValue`] - A list of default values (channels) that are selected by default. + The list of values (channels) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. .. versionadded:: 2.10 """ diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index 405d59b5b4..11f1bb73e9 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -70,6 +70,11 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "InteractionChannel", V_co]): channel_types: Optional[List[:class:`.ChannelType`]] The list of channel types that can be selected in this select menu. Defaults to all types (i.e. ``None``). + default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (channels) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 Attributes ---------- diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index 2636084c0b..807c3b6f00 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -54,6 +54,13 @@ class MentionableSelect(BaseSelect[MentionableSelectMenu, "Union[User, Member, R 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). + default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.Role`, :class:`.SelectDefaultValue`]]] + The list of values (users/roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + Note that unlike other select menu types, this does not support :class:`.Object`\\s due to ambiguities. + + .. versionadded:: 2.10 Attributes ---------- diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 7b01b7ade9..08c50d8812 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -53,6 +53,11 @@ class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]): 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). + default_values: Optional[Sequence[Union[:class:`.Role`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 Attributes ---------- diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index 2cc6c50e44..7fd7ae4beb 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -54,6 +54,11 @@ class UserSelect(BaseSelect[UserSelectMenu, "Union[User, Member]", V_co]): 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). + default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (users/members) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 Attributes ---------- From 7e25f5a76df84702e67d88905de67ef7a9793ed4 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 16:09:16 +0200 Subject: [PATCH 08/24] fix: remove `default_values` from __repr__, way too verbose --- disnake/components.py | 3 ++- disnake/ui/select/base.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/components.py b/disnake/components.py index e0ceadc0c3..1a3f7d07b1 100644 --- a/disnake/components.py +++ b/disnake/components.py @@ -287,7 +287,8 @@ class BaseSelectMenu(Component): "default_values", ) - __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ + # FIXME: this isn't pretty; we should decouple __repr__ from slots + __repr_info__: ClassVar[Tuple[str, ...]] = tuple(s for s in __slots__ if s != "default_values") def __init__(self, data: BaseSelectMenuPayload) -> None: self.type: ComponentType = try_enum(ComponentType, data["type"]) diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index 62bc955225..b7931b2649 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -74,7 +74,6 @@ class BaseSelect(Generic[SelectMenuT, SelectValueT, V_co], Item[V_co], ABC): "min_values", "max_values", "disabled", - "default_values", ) # We have to set this to MISSING in order to overwrite the abstract property from WrappedComponent _underlying: SelectMenuT = MISSING From dc308c127ecda029c36686ed61b4704a3573ac4e Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 16:11:34 +0200 Subject: [PATCH 09/24] feat: add `BaseSelect.default_values` property --- disnake/ui/select/base.py | 12 ++++++++++++ docs/api/ui.rst | 1 + 2 files changed, 13 insertions(+) diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index b7931b2649..7f2e7d842f 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -160,6 +160,18 @@ def disabled(self) -> bool: def disabled(self, value: bool) -> None: self._underlying.disabled = bool(value) + @property + def default_values(self) -> List[SelectDefaultValue]: + """:class:`bool`: The list of values that are selected by default. + Only available for auto-populated select menus. + """ + return self._underlying.default_values + + # TODO: make this accept multiple types similar to constructor? + @default_values.setter + def default_values(self, value: List[SelectDefaultValue]) -> None: + self._underlying.default_values = value + @property def values(self) -> List[SelectValueT]: return self._selected_values diff --git a/docs/api/ui.rst b/docs/api/ui.rst index 85c85e37c4..11a0b757eb 100644 --- a/docs/api/ui.rst +++ b/docs/api/ui.rst @@ -71,6 +71,7 @@ StringSelect .. autoclass:: StringSelect :members: :inherited-members: + :exclude-members: default_values ChannelSelect ~~~~~~~~~~~~~ From 2dd4475e9723c7248d30348058f0b6667c6a7dcc Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 16:11:56 +0200 Subject: [PATCH 10/24] chore: fix `ChannelSelect` parameter order --- disnake/ui/select/channel.py | 20 ++++++++++---------- disnake/ui/select/mentionable.py | 12 ++++++------ disnake/ui/select/role.py | 10 +++++----- disnake/ui/select/user.py | 10 +++++----- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index 11f1bb73e9..71a96bb9b6 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -61,12 +61,6 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "InteractionChannel", V_co]): Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. - row: Optional[:class:`int`] - The relative row this select menu belongs to. A Discord component can only have 5 - rows. By default, items are arranged automatically into those 5 rows. If you'd - 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). channel_types: Optional[List[:class:`.ChannelType`]] The list of channel types that can be selected in this select menu. Defaults to all types (i.e. ``None``). @@ -75,6 +69,12 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "InteractionChannel", V_co]): If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. .. versionadded:: 2.10 + row: Optional[:class:`int`] + The relative row this select menu belongs to. A Discord component can only have 5 + rows. By default, items are arranged automatically into those 5 rows. If you'd + 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). Attributes ---------- @@ -97,8 +97,8 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, row: Optional[int] = None, ) -> None: ... @@ -112,8 +112,8 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, row: Optional[int] = None, ) -> None: ... @@ -126,8 +126,8 @@ def __init__( min_values: int = 1, max_values: int = 1, disabled: bool = False, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, row: Optional[int] = None, ) -> None: super().__init__( @@ -151,8 +151,8 @@ def from_component(cls, component: ChannelSelectMenu) -> Self: min_values=component.min_values, max_values=component.max_values, disabled=component.disabled, - default_values=component.default_values, channel_types=component.channel_types, + default_values=component.default_values, row=None, ) diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index 807c3b6f00..ae9ab0ed0c 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -48,12 +48,6 @@ class MentionableSelect(BaseSelect[MentionableSelectMenu, "Union[User, Member, R Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. - row: Optional[:class:`int`] - The relative row this select menu belongs to. A Discord component can only have 5 - rows. By default, items are arranged automatically into those 5 rows. If you'd - 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). default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.Role`, :class:`.SelectDefaultValue`]]] The list of values (users/roles) that are selected by default. If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. @@ -61,6 +55,12 @@ class MentionableSelect(BaseSelect[MentionableSelectMenu, "Union[User, Member, R Note that unlike other select menu types, this does not support :class:`.Object`\\s due to ambiguities. .. versionadded:: 2.10 + row: Optional[:class:`int`] + The relative row this select menu belongs to. A Discord component can only have 5 + rows. By default, items are arranged automatically into those 5 rows. If you'd + 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). Attributes ---------- diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 08c50d8812..2a26f3db00 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -47,17 +47,17 @@ class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]): Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. + default_values: Optional[Sequence[Union[:class:`.Role`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 row: Optional[:class:`int`] The relative row this select menu belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd 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). - default_values: Optional[Sequence[Union[:class:`.Role`, :class:`.SelectDefaultValue`, :class:`.Object`]]] - The list of values (roles) that are selected by default. - If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. - - .. versionadded:: 2.10 Attributes ---------- diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index 7fd7ae4beb..c628a47300 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -48,17 +48,17 @@ class UserSelect(BaseSelect[UserSelectMenu, "Union[User, Member]", V_co]): Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. + default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (users/members) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 row: Optional[:class:`int`] The relative row this select menu belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd 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). - default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.SelectDefaultValue`, :class:`.Object`]]] - The list of values (users/members) that are selected by default. - If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. - - .. versionadded:: 2.10 Attributes ---------- From c267fbe7fbfe49cd69a0ac8add870c892f568a0f Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 16:14:03 +0200 Subject: [PATCH 11/24] feat(ui): add `default_values` parameter to decorators --- disnake/ui/select/channel.py | 6 ++++++ disnake/ui/select/mentionable.py | 10 ++++++++++ disnake/ui/select/role.py | 6 ++++++ disnake/ui/select/user.py | 6 ++++++ docs/api/ui.rst | 8 ++++---- 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index 71a96bb9b6..b312e1d98c 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -184,6 +184,7 @@ def channel_select( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, row: Optional[int] = None, ) -> Callable[[ItemCallbackType[ChannelSelect[V_co]]], DecoratedItem[ChannelSelect[V_co]]]: ... @@ -240,5 +241,10 @@ def channel_select( channel_types: Optional[List[:class:`.ChannelType`]] The list of channel types that can be selected in this select menu. Defaults to all types (i.e. ``None``). + default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (channels) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 """ return _create_decorator(cls, ChannelSelect, **kwargs) diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index ae9ab0ed0c..ec30f009e3 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -154,6 +154,9 @@ def mentionable_select( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[ + Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]] + ] = None, row: Optional[int] = None, ) -> Callable[[ItemCallbackType[MentionableSelect[V_co]]], DecoratedItem[MentionableSelect[V_co]]]: ... @@ -207,5 +210,12 @@ def mentionable_select( Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. Defaults to ``False``. + default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.Role`, :class:`.SelectDefaultValue`]]] + The list of values (users/roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + Note that unlike other select menu types, this does not support :class:`.Object`\\s due to ambiguities. + + .. versionadded:: 2.10 """ return _create_decorator(cls, MentionableSelect, **kwargs) diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 2a26f3db00..ab18b00040 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -144,6 +144,7 @@ def role_select( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None, row: Optional[int] = None, ) -> Callable[[ItemCallbackType[RoleSelect[V_co]]], DecoratedItem[RoleSelect[V_co]]]: ... @@ -197,5 +198,10 @@ def role_select( Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. Defaults to ``False``. + default_values: Optional[Sequence[Union[:class:`.Role`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 """ return _create_decorator(cls, RoleSelect, **kwargs) diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index c628a47300..c1f90f0658 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -145,6 +145,7 @@ def user_select( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None, row: Optional[int] = None, ) -> Callable[[ItemCallbackType[UserSelect[V_co]]], DecoratedItem[UserSelect[V_co]]]: ... @@ -198,5 +199,10 @@ def user_select( Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. Defaults to ``False``. + default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (users/members) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 """ return _create_decorator(cls, UserSelect, **kwargs) diff --git a/docs/api/ui.rst b/docs/api/ui.rst index 11a0b757eb..770ba3ab3d 100644 --- a/docs/api/ui.rst +++ b/docs/api/ui.rst @@ -135,14 +135,14 @@ Functions .. autofunction:: string_select(cls=StringSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, options=..., disabled=False, row=None) :decorator: -.. autofunction:: channel_select(cls=ChannelSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, channel_types=None, row=None) +.. autofunction:: channel_select(cls=ChannelSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, channel_types=None, default_values=None, row=None) :decorator: -.. autofunction:: mentionable_select(cls=MentionableSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, row=None) +.. autofunction:: mentionable_select(cls=MentionableSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, default_values=None, row=None) :decorator: -.. autofunction:: role_select(cls=RoleSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, row=None) +.. autofunction:: role_select(cls=RoleSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, default_values=None, row=None) :decorator: -.. autofunction:: user_select(cls=UserSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, row=None) +.. autofunction:: user_select(cls=UserSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, disabled=False, default_values=None, row=None) :decorator: From 81bf599a470da15e099d02d06a1de3d43fe585fe Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 16:57:54 +0200 Subject: [PATCH 12/24] feat: update `ActionRow` factory methods --- disnake/ui/action_row.py | 47 +++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/disnake/ui/action_row.py b/disnake/ui/action_row.py index b8473badb0..efbd6c68f6 100644 --- a/disnake/ui/action_row.py +++ b/disnake/ui/action_row.py @@ -34,16 +34,21 @@ from .button import Button from .item import WrappedComponent from .select import ChannelSelect, MentionableSelect, RoleSelect, StringSelect, UserSelect -from .select.string import SelectOptionInput, V_co from .text_input import TextInput if TYPE_CHECKING: from typing_extensions import Self from ..emoji import Emoji + from ..interactions.base import InteractionChannel + from ..member import Member from ..message import Message from ..partial_emoji import PartialEmoji + from ..role import Role from ..types.components import ActionRow as ActionRowPayload + from ..user import User + from .select.base import SelectDefaultValueInputType, SelectDefaultValueMultiInputType + from .select.string import SelectOptionInput, V_co __all__ = ( "ActionRow", @@ -363,6 +368,7 @@ def add_user_select( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None, ) -> SelectCompatibleActionRowT: """Add a user select menu to the action row. Can only be used if the action row holds message components. @@ -388,7 +394,12 @@ def add_user_select( The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` - Whether the select is disabled or not. + Whether the select is disabled. Defaults to ``False``. + default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (users/members) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 Raises ------ @@ -402,6 +413,7 @@ def add_user_select( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, ), ) return self @@ -414,6 +426,7 @@ def add_role_select( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None, ) -> SelectCompatibleActionRowT: """Add a role select menu to the action row. Can only be used if the action row holds message components. @@ -439,7 +452,12 @@ def add_role_select( The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` - Whether the select is disabled or not. + Whether the select is disabled. Defaults to ``False``. + default_values: Optional[Sequence[Union[:class:`.Role`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 Raises ------ @@ -453,6 +471,7 @@ def add_role_select( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, ), ) return self @@ -465,6 +484,9 @@ def add_mentionable_select( min_values: int = 1, max_values: int = 1, disabled: bool = False, + default_values: Optional[ + Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]] + ] = None, ) -> SelectCompatibleActionRowT: """Add a mentionable (user/member/role) select menu to the action row. Can only be used if the action row holds message components. @@ -490,7 +512,14 @@ def add_mentionable_select( The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` - Whether the select is disabled or not. + Whether the select is disabled. Defaults to ``False``. + default_values: Optional[Sequence[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.Role`, :class:`.SelectDefaultValue`]]] + The list of values (users/roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + Note that unlike other select menu types, this does not support :class:`.Object`\\s due to ambiguities. + + .. versionadded:: 2.10 Raises ------ @@ -504,6 +533,7 @@ def add_mentionable_select( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, ), ) return self @@ -517,6 +547,7 @@ def add_channel_select( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, ) -> SelectCompatibleActionRowT: """Add a channel select menu to the action row. Can only be used if the action row holds message components. @@ -542,10 +573,15 @@ def add_channel_select( The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` - Whether the select is disabled or not. + Whether the select is disabled. Defaults to ``False``. channel_types: Optional[List[:class:`.ChannelType`]] The list of channel types that can be selected in this select menu. Defaults to all types (i.e. ``None``). + default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + The list of values (channels) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 Raises ------ @@ -560,6 +596,7 @@ def add_channel_select( max_values=max_values, disabled=disabled, channel_types=channel_types, + default_values=default_values, ), ) return self From 8d45ada6afd5373e09299719073fb9e018abc8dd Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 17:01:44 +0200 Subject: [PATCH 13/24] feat: support broader set of types in `BaseSelect.default_values` setter --- disnake/ui/select/base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index 7f2e7d842f..1ebc9b8189 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -167,10 +167,11 @@ def default_values(self) -> List[SelectDefaultValue]: """ return self._underlying.default_values - # TODO: make this accept multiple types similar to constructor? @default_values.setter - def default_values(self, value: List[SelectDefaultValue]) -> None: - self._underlying.default_values = value + def default_values( + self, value: Optional[Sequence[SelectDefaultValueInputType[SelectValueT]]] + ) -> None: + self._underlying.default_values = self._transform_default_values(value) if value else [] @property def values(self) -> List[SelectValueT]: From 96a2a471fddcaa9770fe8f3e75b3a92bc8ba967c Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 17:04:17 +0200 Subject: [PATCH 14/24] feat: add `SelectDefaultValue.__repr__` --- disnake/components.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/disnake/components.py b/disnake/components.py index 1a3f7d07b1..1d90537cc6 100644 --- a/disnake/components.py +++ b/disnake/components.py @@ -672,6 +672,9 @@ def to_dict(self) -> SelectDefaultValuePayload: "type": self.type.value, } + def __repr__(self) -> str: + return f"" + class TextInput(Component): """Represents a text input from the Discord Bot UI Kit. From 8551d6649a72c7e1e374fe51811f02a998cd3492 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 17:10:44 +0200 Subject: [PATCH 15/24] docs: add SelectDefaultValueType --- docs/api/components.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/api/components.rst b/docs/api/components.rst index 2d50e41957..628d6c6430 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -242,3 +242,24 @@ TextInputStyle .. attribute:: long An alias for :attr:`paragraph`. + +SelectDefaultValueType +~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: SelectDefaultValueType + + Represents the type of a :class:`SelectDefaultValue`. + + .. versionadded:: 2.10 + + .. attribute:: user + + Represents a user/member. + + .. attribute:: role + + Represents a role. + + .. attribute:: channel + + Represents a channel. From 79a75d8931d614a6b35b86becaab735236d2f05f Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 17:12:03 +0200 Subject: [PATCH 16/24] fix(docs): fix `default_values` property type --- disnake/ui/select/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index 1ebc9b8189..912a24ba1f 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -162,7 +162,7 @@ def disabled(self, value: bool) -> None: @property def default_values(self) -> List[SelectDefaultValue]: - """:class:`bool`: The list of values that are selected by default. + """List[:class:`.SelectDefaultValue`]: The list of values that are selected by default. Only available for auto-populated select menus. """ return self._underlying.default_values From 3f365d44011c4d1ed4c370a2f1712772a70da763 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 17:46:28 +0200 Subject: [PATCH 17/24] feat(types): add `Message.resolved` payloads --- disnake/types/interactions.py | 6 ++++-- disnake/types/message.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index b5c64a0aa7..c5913c9c09 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -104,7 +104,9 @@ class InteractionDataResolved(TypedDict, total=False): members: Dict[Snowflake, Member] roles: Dict[Snowflake, Role] channels: Dict[Snowflake, ResolvedPartialChannel] - # only in application commands + + +class ApplicationCommandInteractionDataResolved(InteractionDataResolved, total=False): messages: Dict[Snowflake, Message] attachments: Dict[Snowflake, Attachment] @@ -157,7 +159,7 @@ class ApplicationCommandInteractionData(TypedDict): id: Snowflake name: str type: ApplicationCommandType - resolved: NotRequired[InteractionDataResolved] + resolved: NotRequired[ApplicationCommandInteractionDataResolved] options: NotRequired[List[ApplicationCommandInteractionDataOption]] # this is the guild the command is registered to, not the guild the command was invoked in (see interaction.guild_id) guild_id: NotRequired[Snowflake] diff --git a/disnake/types/message.py b/disnake/types/message.py index 26f691c6bb..12fc2686d4 100644 --- a/disnake/types/message.py +++ b/disnake/types/message.py @@ -10,7 +10,7 @@ from .components import Component from .embed import Embed from .emoji import PartialEmoji -from .interactions import InteractionMessageReference +from .interactions import InteractionDataResolved, InteractionMessageReference from .member import Member, UserWithMember from .snowflake import Snowflake, SnowflakeList from .sticker import StickerItem @@ -114,6 +114,8 @@ class Message(TypedDict): sticker_items: NotRequired[List[StickerItem]] position: NotRequired[int] role_subscription_data: NotRequired[RoleSubscriptionData] + # contains resolved objects for `default_values` of select menus in this message; we currently don't have a use for this + resolved: NotRequired[InteractionDataResolved] # specific to MESSAGE_CREATE/MESSAGE_UPDATE events guild_id: NotRequired[Snowflake] From 69372859517cd806151a97232345eca97638255e Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Oct 2023 22:42:17 +0200 Subject: [PATCH 18/24] docs: add changelog entry --- changelog/1115.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1115.feature.rst diff --git a/changelog/1115.feature.rst b/changelog/1115.feature.rst new file mode 100644 index 0000000000..a64c25babb --- /dev/null +++ b/changelog/1115.feature.rst @@ -0,0 +1 @@ +Add :class:`SelectDefaultValue`, and add :attr:`~UserSelectMenu.default_values` to all auto-populated select menu types. From 3100aa2bef7efad786be854194c205743ba3d2eb Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 26 Mar 2024 23:18:30 +0100 Subject: [PATCH 19/24] chore: fix lint --- disnake/ui/select/channel.py | 8 ++++++-- disnake/ui/select/mentionable.py | 20 ++++++++++++++++++-- disnake/ui/select/role.py | 19 +++++++++++++++++-- disnake/ui/select/string.py | 9 +++++++-- disnake/ui/select/user.py | 20 ++++++++++++++++++-- 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index 5ff0e715ee..4f5e37990e 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -6,7 +6,9 @@ TYPE_CHECKING, Any, Callable, + ClassVar, List, + Mapping, Optional, Sequence, Tuple, @@ -15,7 +17,7 @@ overload, ) -from ...abc import GuildChannel +from ...abc import GuildChannel, Snowflake from ...channel import PartialMessageable from ...components import ChannelSelectMenu from ...enums import ChannelType, ComponentType, SelectDefaultValueType @@ -84,7 +86,9 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "InteractionChannel", V_co]): __repr_attributes__: Tuple[str, ...] = BaseSelect.__repr_attributes__ + ("channel_types",) - _default_value_type_map = { + _default_value_type_map: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { SelectDefaultValueType.channel: (GuildChannel, Thread, PartialMessageable, Object), } diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index d5079cd8e2..e98dfb29c9 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -2,8 +2,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, TypeVar, Union, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) +from ...abc import Snowflake from ...components import MentionableSelectMenu from ...enums import ComponentType, SelectDefaultValueType from ...member import Member @@ -68,7 +82,9 @@ class MentionableSelect(BaseSelect[MentionableSelectMenu, "Union[User, Member, R A list of users, members and/or roles that have been selected by the user. """ - _default_value_type_map = { + _default_value_type_map: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { SelectDefaultValueType.user: (Member, User, ClientUser), SelectDefaultValueType.role: (Role,), } diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 89eb57a235..4cb886168f 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -2,8 +2,21 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, TypeVar, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + overload, +) +from ...abc import Snowflake from ...components import RoleSelectMenu from ...enums import ComponentType, SelectDefaultValueType from ...object import Object @@ -65,7 +78,9 @@ class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]): A list of roles that have been selected by the user. """ - _default_value_type_map = { + _default_value_type_map: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { SelectDefaultValueType.role: (Role, Object), } diff --git a/disnake/ui/select/string.py b/disnake/ui/select/string.py index 25b0aafc51..3b12d80388 100644 --- a/disnake/ui/select/string.py +++ b/disnake/ui/select/string.py @@ -6,8 +6,10 @@ TYPE_CHECKING, Any, Callable, + ClassVar, Dict, List, + Mapping, Optional, Tuple, Type, @@ -16,8 +18,9 @@ overload, ) +from ...abc import Snowflake from ...components import SelectOption, StringSelectMenu -from ...enums import ComponentType +from ...enums import ComponentType, SelectDefaultValueType from ...utils import MISSING from .base import BaseSelect, P, V_co, _create_decorator @@ -99,7 +102,9 @@ class StringSelect(BaseSelect[StringSelectMenu, str, V_co]): __repr_attributes__: Tuple[str, ...] = BaseSelect.__repr_attributes__ + ("options",) # In practice this should never be used by anything, might as well have it anyway though. - _default_value_type_map = {} + _default_value_type_map: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = {} @overload def __init__( diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index a21f3b97c7..9ab9b803ce 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -2,8 +2,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, TypeVar, Union, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) +from ...abc import Snowflake from ...components import UserSelectMenu from ...enums import ComponentType, SelectDefaultValueType from ...member import Member @@ -66,7 +80,9 @@ class UserSelect(BaseSelect[UserSelectMenu, "Union[User, Member]", V_co]): A list of users/members that have been selected by the user. """ - _default_value_type_map = { + _default_value_type_map: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { SelectDefaultValueType.user: (Member, User, ClientUser, Object), } From 14b5a85dd839b155af104999725a6b47747688ec Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Sep 2024 18:22:12 +0200 Subject: [PATCH 20/24] fix: use new channel union type --- disnake/ui/action_row.py | 4 ++-- disnake/ui/select/channel.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/disnake/ui/action_row.py b/disnake/ui/action_row.py index 094693a2cb..0d0e7c7ec3 100644 --- a/disnake/ui/action_row.py +++ b/disnake/ui/action_row.py @@ -39,8 +39,8 @@ if TYPE_CHECKING: from typing_extensions import Self + from ..abc import AnyChannel from ..emoji import Emoji - from ..interactions.base import InteractionChannel from ..member import Member from ..message import Message from ..partial_emoji import PartialEmoji @@ -548,7 +548,7 @@ def add_channel_select( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, ) -> SelectCompatibleActionRowT: """Add a channel select menu to the action row. Can only be used if the action row holds message components. diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index 56fd77a7bf..049dad67f9 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -102,7 +102,7 @@ def __init__( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> None: ... @@ -117,7 +117,7 @@ def __init__( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> None: ... @@ -131,7 +131,7 @@ def __init__( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> None: super().__init__( @@ -188,7 +188,7 @@ def channel_select( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, - default_values: Optional[Sequence[SelectDefaultValueInputType[InteractionChannel]]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> Callable[[ItemCallbackType[ChannelSelect[V_co]]], DecoratedItem[ChannelSelect[V_co]]]: ... From de0ca0abb64a638ab9a9498ae6503b87802c7526 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 7 Sep 2024 19:03:30 +0200 Subject: [PATCH 21/24] fix: allow dm/gdm instances in default_values --- disnake/ui/action_row.py | 2 +- disnake/ui/select/channel.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/disnake/ui/action_row.py b/disnake/ui/action_row.py index 0d0e7c7ec3..8c5bf769ea 100644 --- a/disnake/ui/action_row.py +++ b/disnake/ui/action_row.py @@ -578,7 +578,7 @@ def add_channel_select( channel_types: Optional[List[:class:`.ChannelType`]] The list of channel types that can be selected in this select menu. Defaults to all types (i.e. ``None``). - default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.abc.PrivateChannel`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] The list of values (channels) that are selected by default. If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index 049dad67f9..f004308482 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -18,7 +18,7 @@ ) from ...abc import GuildChannel, Snowflake -from ...channel import PartialMessageable +from ...channel import DMChannel, GroupChannel, PartialMessageable from ...components import ChannelSelectMenu from ...enums import ChannelType, ComponentType, SelectDefaultValueType from ...object import Object @@ -66,7 +66,7 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "AnyChannel", V_co]): channel_types: Optional[List[:class:`.ChannelType`]] The list of channel types that can be selected in this select menu. Defaults to all types (i.e. ``None``). - default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.abc.PrivateChannel`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] The list of values (channels) that are selected by default. If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. @@ -89,7 +89,14 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "AnyChannel", V_co]): _default_value_type_map: ClassVar[ Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] ] = { - SelectDefaultValueType.channel: (GuildChannel, Thread, PartialMessageable, Object), + SelectDefaultValueType.channel: ( + GuildChannel, + Thread, + DMChannel, + GroupChannel, + PartialMessageable, + Object, + ), } @overload @@ -243,7 +250,7 @@ def channel_select( channel_types: Optional[List[:class:`.ChannelType`]] The list of channel types that can be selected in this select menu. Defaults to all types (i.e. ``None``). - default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] + default_values: Optional[Sequence[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.abc.PrivateChannel`, :class:`.PartialMessageable`, :class:`.SelectDefaultValue`, :class:`.Object`]]] The list of values (channels) that are selected by default. If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. From 117108fffb3988365f75098c96b5a30cc34e6aa4 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sun, 8 Sep 2024 11:55:02 +0200 Subject: [PATCH 22/24] chore: add some future fixmes re private channel types --- disnake/abc.py | 1 + disnake/channel.py | 1 + 2 files changed, 2 insertions(+) diff --git a/disnake/abc.py b/disnake/abc.py index 48a75f6b44..051931b346 100644 --- a/disnake/abc.py +++ b/disnake/abc.py @@ -178,6 +178,7 @@ def avatar(self) -> Optional[Asset]: raise NotImplementedError +# FIXME: this shouldn't be a protocol. isinstance(thread, PrivateChannel) returns true, and issubclass doesn't work. @runtime_checkable class PrivateChannel(Snowflake, Protocol): """An ABC that details the common operations on a private Discord channel. diff --git a/disnake/channel.py b/disnake/channel.py index 980a5ea1ce..5913a4539e 100644 --- a/disnake/channel.py +++ b/disnake/channel.py @@ -5044,6 +5044,7 @@ def _channel_type_factory( cls: Union[Type[disnake.abc.GuildChannel], Type[Thread]] ) -> List[ChannelType]: return { + # FIXME: this includes private channels; improve this once there's a common base type for all channels disnake.abc.GuildChannel: list(ChannelType.__members__.values()), VocalGuildChannel: [ChannelType.voice, ChannelType.stage_voice], disnake.abc.PrivateChannel: [ChannelType.private, ChannelType.group], From 4fada306c9babbc5ba3cb06a8d189c9280d15971 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 16 Sep 2024 16:14:12 +0200 Subject: [PATCH 23/24] fix: adjust `SelectDefaultValue` repr --- disnake/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/components.py b/disnake/components.py index 87c4f684db..09854d5ad1 100644 --- a/disnake/components.py +++ b/disnake/components.py @@ -689,7 +689,7 @@ def to_dict(self) -> SelectDefaultValuePayload: } def __repr__(self) -> str: - return f"" + return f"" class TextInput(Component): From bd85a82b0c43aab9cbfd96f590f147e1fa8fcfe4 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 16 Sep 2024 16:46:55 +0200 Subject: [PATCH 24/24] test: add a couple unittests --- tests/ui/test_select.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/ui/test_select.py diff --git a/tests/ui/test_select.py b/tests/ui/test_select.py new file mode 100644 index 0000000000..5c33cd5575 --- /dev/null +++ b/tests/ui/test_select.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: MIT + +from unittest import mock + +import pytest + +import disnake +from disnake import ui + + +class TestDefaultValues: + @pytest.mark.parametrize( + "value", + [ + disnake.Object(123), + disnake.SelectDefaultValue(123, disnake.SelectDefaultValueType.channel), + mock.Mock(disnake.TextChannel, id=123), + ], + ) + def test_valid(self, value) -> None: + s = ui.ChannelSelect(default_values=[value]) + assert s.default_values[0].id == 123 + assert s.default_values[0].type == disnake.SelectDefaultValueType.channel + + @pytest.mark.parametrize( + ("select_type", "value_type"), + [ + (ui.ChannelSelect, disnake.Member), + # MentionableSelect in particular should reject `Object` due to ambiguities + (ui.MentionableSelect, disnake.Object), + ], + ) + def test_invalid(self, select_type, value_type) -> None: + with pytest.raises(TypeError, match="Expected type of default value"): + select_type(default_values=[mock.Mock(value_type, id=123)]) + + @pytest.mark.parametrize( + ("value_type", "expected"), + [ + (disnake.Member, disnake.SelectDefaultValueType.user), + (disnake.ClientUser, disnake.SelectDefaultValueType.user), + (disnake.Role, disnake.SelectDefaultValueType.role), + ], + ) + def test_mentionable(self, value_type, expected) -> None: + s = ui.MentionableSelect(default_values=[mock.Mock(value_type, id=123)]) + assert s.default_values[0].type == expected