# SPDX-License-Identifier: MIT from __future__ import annotations import os from abc import ABC, abstractmethod from typing import ( TYPE_CHECKING, Callable, ClassVar, Generic, List, Mapping, Optional, Sequence, Tuple, Type, TypeVar, Union, ) from ...components import AnySelectMenu, SelectDefaultValue from ...enums import ComponentType, SelectDefaultValueType from ...object import Object from ...utils import MISSING, humanize_list, iscoroutinefunction from ..item import DecoratedItem, Item __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 else: ParamSpec = TypeVar S_co = TypeVar("S_co", bound="BaseSelect", covariant=True) V_co = TypeVar("V_co", bound="Optional[View]", covariant=True) SelectMenuT = TypeVar("SelectMenuT", bound=AnySelectMenu) 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. This is usually represented as a drop down menu. This isn't meant to be used directly, instead use one of the concrete select menu types: - :class:`disnake.ui.StringSelect` - :class:`disnake.ui.UserSelect` - :class:`disnake.ui.RoleSelect` - :class:`disnake.ui.MentionableSelect` - :class:`disnake.ui.ChannelSelect` .. versionadded:: 2.7 """ __repr_attributes__: ClassVar[Tuple[str, ...]] = ( "placeholder", "min_values", "max_values", "disabled", "required", ) # We have to set this to MISSING in order to overwrite the abstract property from UIComponent _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], component_type: ComponentType, *, custom_id: str, placeholder: Optional[str], min_values: int, max_values: int, disabled: bool, default_values: Optional[Sequence[SelectDefaultValueInputType[SelectValueT]]], required: bool, id: int, row: Optional[int], ) -> None: super().__init__() self._selected_values: List[SelectValueT] = [] self._provided_custom_id = custom_id is not MISSING custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id self._underlying = underlying_type._raw_construct( type=component_type, id=id, custom_id=custom_id, placeholder=placeholder, min_values=min_values, max_values=max_values, disabled=disabled, default_values=self._transform_default_values(default_values) if default_values else [], required=required, ) self.row = row @property def custom_id(self) -> str: """:class:`str`: The ID of the select menu that gets received during an interaction.""" return self._underlying.custom_id @custom_id.setter def custom_id(self, value: str) -> None: if not isinstance(value, str): raise TypeError("custom_id must be None or str") self._underlying.custom_id = value @property def placeholder(self) -> Optional[str]: """Optional[:class:`str`]: The placeholder text that is shown if nothing is selected, if any.""" return self._underlying.placeholder @placeholder.setter def placeholder(self, value: Optional[str]) -> None: if value is not None and not isinstance(value, str): raise TypeError("placeholder must be None or str") self._underlying.placeholder = value @property def min_values(self) -> int: """:class:`int`: The minimum number of items that must be chosen for this select menu.""" return self._underlying.min_values @min_values.setter def min_values(self, value: int) -> None: self._underlying.min_values = int(value) @property def max_values(self) -> int: """:class:`int`: The maximum number of items that must be chosen for this select menu.""" return self._underlying.max_values @max_values.setter def max_values(self, value: int) -> None: self._underlying.max_values = int(value) @property def disabled(self) -> bool: """:class:`bool`: Whether the select menu is disabled.""" return self._underlying.disabled @disabled.setter 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 required(self) -> bool: """:class:`bool`: Whether the select menu is required. Only applies to components in modals. .. versionadded:: 2.11 """ return self._underlying.required @required.setter def required(self, value: bool) -> None: self._underlying.required = bool(value) @property def values(self) -> List[SelectValueT]: return self._selected_values @property def width(self) -> int: return 5 def refresh_component(self, component: SelectMenuT) -> None: self._underlying = component def refresh_state(self, interaction: MessageInteraction) -> None: self._selected_values = interaction.resolved_values # type: ignore @classmethod @abstractmethod def from_component(cls, component: SelectMenuT) -> Self: raise NotImplementedError def is_dispatchable(self) -> bool: """Whether the select menu is dispatchable. This will always return ``True``. :return type: :class:`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( # FIXME(3.0): rename `cls` parameter to more closely represent any callable argument type cls: Callable[P, S_co], /, *args: P.args, **kwargs: P.kwargs, ) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: if args: # the `*args` def above is just to satisfy the typechecker raise RuntimeError("expected no *args") if not callable(cls): raise TypeError("cls argument must be callable") def decorator(func: ItemCallbackType[V_co, S_co]) -> DecoratedItem[S_co]: if not iscoroutinefunction(func): raise TypeError("select function must be a coroutine function") func.__discord_ui_model_type__ = cls func.__discord_ui_model_kwargs__ = kwargs return func # type: ignore return decorator