Files
2026-02-03 20:32:43 +02:00

279 lines
9.0 KiB
Python

# 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