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. diff --git a/disnake/abc.py b/disnake/abc.py index c6c4c651cf..051931b346 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, StandardSticker, StickerItem -from .ui.action_row import components_to_dict from .utils import _overload_with_permissions from .voice_client import VoiceClient, VoiceProtocol @@ -179,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. @@ -1719,16 +1719,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 diff --git a/disnake/channel.py b/disnake/channel.py index a1a37a057f..90d5792a39 100644 --- a/disnake/channel.py +++ b/disnake/channel.py @@ -5092,6 +5092,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], diff --git a/disnake/components.py b/disnake/components.py index 7614fd424b..09854d5ad1 100644 --- a/disnake/components.py +++ b/disnake/components.py @@ -18,7 +18,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 @@ -35,6 +42,7 @@ Component as ComponentPayload, MentionableSelectMenu as MentionableSelectMenuPayload, RoleSelectMenu as RoleSelectMenuPayload, + SelectDefaultValue as SelectDefaultValuePayload, SelectOption as SelectOptionPayload, StringSelectMenu as StringSelectMenuPayload, TextInput as TextInputPayload, @@ -53,6 +61,7 @@ "MentionableSelectMenu", "ChannelSelectMenu", "SelectOption", + "SelectDefaultValue", "TextInput", ) @@ -264,6 +273,12 @@ 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`] + 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 """ __slots__: Tuple[str, ...] = ( @@ -272,9 +287,11 @@ class BaseSelectMenu(Component): "min_values", "max_values", "disabled", + "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") # n.b: ideally this would be `BaseSelectMenuPayload`, # but pyright made TypedDict keys invariant and doesn't @@ -288,6 +305,9 @@ def __init__(self, data: AnySelectMenuPayload) -> 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 = { @@ -301,6 +321,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 @@ -377,6 +400,11 @@ 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`] + 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 """ __slots__: Tuple[str, ...] = () @@ -412,6 +440,11 @@ 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`] + 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 """ __slots__: Tuple[str, ...] = () @@ -447,6 +480,11 @@ 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`] + 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 """ __slots__: Tuple[str, ...] = () @@ -485,6 +523,11 @@ 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`] + 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 """ __slots__: Tuple[str, ...] = ("channel_types",) @@ -613,6 +656,42 @@ 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, + } + + def __repr__(self) -> str: + return f"" + + class TextInput(Component): """Represents a text input from the Discord Bot UI Kit. diff --git a/disnake/enums.py b/disnake/enums.py index 5c917911f0..8c587ca902 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -47,6 +47,7 @@ "ComponentType", "ButtonStyle", "TextInputStyle", + "SelectDefaultValueType", "StagePrivacyLevel", "InteractionType", "InteractionResponseType", @@ -694,6 +695,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/disnake/types/interactions.py b/disnake/types/interactions.py index 9cb8393ea5..5aca5f3cf3 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -105,7 +105,9 @@ class InteractionDataResolved(TypedDict, total=False): members: Dict[Snowflake, Member] roles: Dict[Snowflake, Role] channels: Dict[Snowflake, InteractionChannel] - # only in application commands + + +class ApplicationCommandInteractionDataResolved(InteractionDataResolved, total=False): messages: Dict[Snowflake, Message] attachments: Dict[Snowflake, Attachment] @@ -158,7 +160,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 424b7ffd66..8d8431864e 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 .poll import Poll from .snowflake import Snowflake, SnowflakeList @@ -117,6 +117,8 @@ class Message(TypedDict): position: NotRequired[int] role_subscription_data: NotRequired[RoleSubscriptionData] poll: NotRequired[Poll] + # 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] diff --git a/disnake/ui/action_row.py b/disnake/ui/action_row.py index 21ea01cb74..8c5bf769ea 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 ..abc import AnyChannel from ..emoji import Emoji + 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", @@ -364,6 +369,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. @@ -389,7 +395,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 ------ @@ -403,6 +414,7 @@ def add_user_select( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, ), ) return self @@ -415,6 +427,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. @@ -440,7 +453,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 ------ @@ -454,6 +472,7 @@ def add_role_select( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, ), ) return self @@ -466,6 +485,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. @@ -491,7 +513,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 ------ @@ -505,6 +534,7 @@ def add_mentionable_select( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, ), ) return self @@ -518,6 +548,7 @@ def add_channel_select( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = 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. @@ -543,10 +574,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:`.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``. + + .. versionadded:: 2.10 Raises ------ @@ -561,6 +597,7 @@ def add_channel_select( max_values=max_values, disabled=disabled, channel_types=channel_types, + default_values=default_values, ), ) return self diff --git a/disnake/ui/button.py b/disnake/ui/button.py index a961ba29ab..9995013ebb 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, ...]] = Button[Any], **kwargs: Any + cls: Type[ItemShape[B_co, ...]] = 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 464eb4d588..c4d29c6417 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..912a24ba1f 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -9,25 +9,31 @@ 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 ..item import DecoratedItem, Item, Object +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",) 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. @@ -68,6 +78,9 @@ class BaseSelect(Generic[SelectMenuT, SelectValueT, V_co], Item[V_co], ABC): # 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[SelectDefaultValueType, Tuple[Type[Snowflake], ...]]] + def __init__( self, underlying_type: Type[SelectMenuT], @@ -78,6 +91,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 +105,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 @@ -145,6 +160,19 @@ def disabled(self) -> bool: def disabled(self, value: bool) -> None: self._underlying.disabled = bool(value) + @property + def default_values(self) -> List[SelectDefaultValue]: + """List[:class:`.SelectDefaultValue`]: The list of values that are selected by default. + Only available for auto-populated select menus. + """ + return self._underlying.default_values + + @default_values.setter + 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]: return self._selected_values @@ -171,9 +199,47 @@ def is_dispatchable(self) -> bool: """ return True + @classmethod + def _transform_default_values( + cls, values: Sequence[SelectDefaultValueInputType[SelectValueT]] + ) -> List[SelectDefaultValue]: + result: List[SelectDefaultValue] = [] + + for value in values: + # If we have a SelectDefaultValue, just use it as-is + if isinstance(value, SelectDefaultValue): + 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}" + ) + result.append(value) + continue + + # Otherwise, look through the list of allowed input types and + # get the associated SelectDefaultValueType + for ( + 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.values() 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[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 9214b71223..f004308482 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -2,18 +2,35 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Type, TypeVar, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + overload, +) +from ...abc import GuildChannel, Snowflake +from ...channel import DMChannel, GroupChannel, 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 from ...abc import AnyChannel - from ..item import DecoratedItem, ItemCallbackType, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -46,15 +63,20 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "AnyChannel", V_co]): Defaults to 1 and must be between 1 and 25. disabled: :class:`bool` Whether the select is disabled. + 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:`.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``. + + .. 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). - 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``). Attributes ---------- @@ -64,6 +86,19 @@ class ChannelSelect(BaseSelect[ChannelSelectMenu, "AnyChannel", V_co]): __repr_attributes__: Tuple[str, ...] = BaseSelect.__repr_attributes__ + ("channel_types",) + _default_value_type_map: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { + SelectDefaultValueType.channel: ( + GuildChannel, + Thread, + DMChannel, + GroupChannel, + PartialMessageable, + Object, + ), + } + @overload def __init__( self: ChannelSelect[None], @@ -74,6 +109,7 @@ def __init__( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> None: ... @@ -88,6 +124,7 @@ def __init__( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> None: ... @@ -101,6 +138,7 @@ def __init__( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> None: super().__init__( @@ -111,6 +149,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 @@ -124,6 +163,7 @@ def from_component(cls, component: ChannelSelectMenu) -> Self: max_values=component.max_values, disabled=component.disabled, channel_types=component.channel_types, + default_values=component.default_values, row=None, ) @@ -155,6 +195,7 @@ def channel_select( max_values: int = 1, disabled: bool = False, channel_types: Optional[List[ChannelType]] = None, + default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, ) -> Callable[[ItemCallbackType[ChannelSelect[V_co]]], DecoratedItem[ChannelSelect[V_co]]]: ... @@ -162,13 +203,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, ...]] = ChannelSelect[Any], **kwargs: Any + cls: Type[ItemShape[S_co, ...]] = ChannelSelect[Any], **kwargs: Any ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: """A decorator that attaches a channel select menu to a component. @@ -209,5 +250,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:`.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``. + + .. versionadded:: 2.10 """ return _create_decorator(cls, ChannelSelect, **kwargs) diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index 860903f7f1..e98dfb29c9 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -2,20 +2,34 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, 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 +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, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -48,6 +62,13 @@ 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. + 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 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 @@ -61,6 +82,13 @@ 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: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { + SelectDefaultValueType.user: (Member, User, ClientUser), + SelectDefaultValueType.role: (Role,), + } + @overload def __init__( self: MentionableSelect[None], @@ -70,6 +98,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 +114,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 +129,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 +142,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, row=row, ) @@ -116,6 +154,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, ) @@ -131,6 +170,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]]]: ... @@ -138,13 +180,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, ...]] = MentionableSelect[Any], **kwargs: Any + cls: Type[ItemShape[S_co, ...]] = MentionableSelect[Any], **kwargs: Any ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: """A decorator that attaches a mentionable (user/member/role) select menu to a component. @@ -182,5 +224,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 f3dbec4b17..4cb886168f 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -2,18 +2,32 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, 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 +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, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -46,6 +60,11 @@ 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 @@ -59,6 +78,12 @@ class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]): A list of roles that have been selected by the user. """ + _default_value_type_map: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { + SelectDefaultValueType.role: (Role, Object), + } + @overload def __init__( self: RoleSelect[None], @@ -68,6 +93,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 +107,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 +120,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 +131,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, row=row, ) @@ -114,6 +143,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, ) @@ -129,6 +159,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]]]: ... @@ -136,13 +167,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, ...]] = RoleSelect[Any], **kwargs: Any + cls: Type[ItemShape[S_co, ...]] = RoleSelect[Any], **kwargs: Any ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: """A decorator that attaches a role select menu to a component. @@ -180,5 +211,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/string.py b/disnake/ui/select/string.py index 3eeedc1f22..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 @@ -26,7 +29,7 @@ from ...emoji import Emoji from ...partial_emoji import PartialEmoji - from ..item import DecoratedItem, ItemCallbackType, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -98,6 +101,11 @@ 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: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = {} + @overload def __init__( self: StringSelect[None], @@ -145,6 +153,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) @@ -262,13 +271,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, ...]] = StringSelect[Any], **kwargs: Any + cls: Type[ItemShape[S_co, ...]] = StringSelect[Any], **kwargs: Any ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: """A decorator that attaches a string select menu to a component. diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index 4868894a83..9ab9b803ce 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -2,19 +2,34 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Optional, 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 +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, Object + from ..item import DecoratedItem, ItemCallbackType, ItemShape __all__ = ( @@ -47,6 +62,11 @@ 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 @@ -60,6 +80,12 @@ 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: ClassVar[ + Mapping[SelectDefaultValueType, Tuple[Type[Snowflake], ...]] + ] = { + SelectDefaultValueType.user: (Member, User, ClientUser, Object), + } + @overload def __init__( self: UserSelect[None], @@ -69,6 +95,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 +109,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 +122,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 +133,7 @@ def __init__( min_values=min_values, max_values=max_values, disabled=disabled, + default_values=default_values, row=row, ) @@ -115,6 +145,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, ) @@ -130,6 +161,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]]]: ... @@ -137,13 +169,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, ...]] = UserSelect[Any], **kwargs: Any + cls: Type[ItemShape[S_co, ...]] = UserSelect[Any], **kwargs: Any ) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: """A decorator that attaches a user select menu to a component. @@ -181,5 +213,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/components.rst b/docs/api/components.rst index 5702e55238..628d6c6430 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -98,25 +98,30 @@ UserSelectMenu :members: :inherited-members: -TextInput -~~~~~~~~~ +SelectOption +~~~~~~~~~~~~ -.. attributetable:: TextInput +.. attributetable:: SelectOption -.. autoclass:: TextInput() +.. autoclass:: SelectOption :members: - :inherited-members: -Data Classes -------------- +SelectDefaultValue +~~~~~~~~~~~~~~~~~~ -SelectOption -~~~~~~~~~~~~ +.. attributetable:: SelectDefaultValue -.. attributetable:: SelectOption +.. autoclass:: SelectDefaultValue + :members: -.. autoclass:: SelectOption +TextInput +~~~~~~~~~ + +.. attributetable:: TextInput + +.. autoclass:: TextInput() :members: + :inherited-members: Enumerations ------------ @@ -237,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. diff --git a/docs/api/ui.rst b/docs/api/ui.rst index c7c061f137..725c65fb77 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 ~~~~~~~~~~~~~ @@ -134,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: 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