Adding all files

This commit is contained in:
2026-02-03 20:32:43 +02:00
parent 2588d10ba0
commit 77b70b600f
1457 changed files with 184865 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
# SPDX-License-Identifier: MIT
"""disnake.ui
~~~~~~~~~~~
Bot UI Kit helper for the Discord API
:copyright: (c) 2015-2021 Rapptz, 2021-present Disnake Development
:license: MIT, see LICENSE for more details.
"""
from .action_row import *
from .button import *
from .container import *
from .file import *
from .item import *
from .label import *
from .media_gallery import *
from .modal import *
from .section import *
from .select import *
from .separator import *
from .text_display import *
from .text_input import *
from .thumbnail import *
from .view import *

View File

@@ -0,0 +1,93 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeVar, Union
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from . import (
ActionRow,
Button,
Container,
File,
Label,
MediaGallery,
Section,
Separator,
TextDisplay,
TextInput,
)
from .item import WrappedComponent
from .select import ChannelSelect, MentionableSelect, RoleSelect, StringSelect, UserSelect
from .view import View
V_co = TypeVar("V_co", bound="Optional[View]", covariant=True)
AnySelect = Union[
"ChannelSelect[V_co]",
"MentionableSelect[V_co]",
"RoleSelect[V_co]",
"StringSelect[V_co]",
"UserSelect[V_co]",
]
# valid `ActionRow.components` item types in a message/modal
ActionRowMessageComponent = Union["Button[Any]", "AnySelect[Any]"]
ActionRowModalComponent: TypeAlias = "TextInput" # deprecated
# valid message component types (v1/v2)
MessageTopLevelComponentV1: TypeAlias = "ActionRow[ActionRowMessageComponent]"
MessageTopLevelComponentV2 = Union[
"Section",
"TextDisplay",
"MediaGallery",
"File",
"Separator",
"Container",
]
MessageTopLevelComponent = Union[MessageTopLevelComponentV1, MessageTopLevelComponentV2]
# valid modal component types (separate type with ActionRow until fully deprecated)
ModalTopLevelComponent_ = Union[
"TextDisplay",
"Label",
]
ModalTopLevelComponent = Union[
ModalTopLevelComponent_,
"ActionRow[ActionRowModalComponent]", # deprecated
]
ActionRowChildT = TypeVar("ActionRowChildT", bound="WrappedComponent")
NonActionRowChildT = TypeVar(
"NonActionRowChildT",
bound=Union[MessageTopLevelComponentV2, ModalTopLevelComponent_],
)
# generic utility type for any single ui component (within some generic bounds)
AnyUIComponentInput = Union[
ActionRowChildT, # action row child component
"ActionRow[ActionRowChildT]", # action row with given child types
NonActionRowChildT, # some subset of (v2) components that work outside of action rows
]
# The generic to end all generics.
# This represents valid input types where components are expected,
# providing some shortcuts/quality-of-life input shapes.
ComponentInput = Union[
AnyUIComponentInput[ActionRowChildT, NonActionRowChildT], # any single component
Sequence[ # or, a sequence of either -
Union[
AnyUIComponentInput[ActionRowChildT, NonActionRowChildT], # - any single component
Sequence[ActionRowChildT], # - a sequence of action row child types
]
],
]
MessageComponents = ComponentInput[ActionRowMessageComponent, MessageTopLevelComponentV2]
ModalComponents = ComponentInput[
ActionRowModalComponent, # deprecated
ModalTopLevelComponent_,
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,364 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Tuple, TypeVar, Union, overload
from ..components import Button as ButtonComponent
from ..enums import ButtonStyle, ComponentType
from ..partial_emoji import PartialEmoji, _EmojiTag
from ..utils import MISSING, iscoroutinefunction
from .item import DecoratedItem, Item
__all__ = (
"Button",
"button",
)
if TYPE_CHECKING:
from typing_extensions import ParamSpec, Self
from ..emoji import Emoji
from .item import ItemCallbackType
from .view import View
else:
ParamSpec = TypeVar
B = TypeVar("B", bound="Button")
B_co = TypeVar("B_co", bound="Button", covariant=True)
V_co = TypeVar("V_co", bound="Optional[View]", covariant=True)
P = ParamSpec("P")
class Button(Item[V_co]):
"""Represents a UI button.
.. versionadded:: 2.0
Parameters
----------
style: :class:`disnake.ButtonStyle`
The style of the button.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
If this button is for a URL or an SKU, it does not have a custom ID.
url: Optional[:class:`str`]
The URL this button sends you to.
disabled: :class:`bool`
Whether the button is disabled.
label: Optional[:class:`str`]
The label of the button, if any.
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]]
The emoji of the button, if available.
sku_id: Optional[:class:`int`]
The ID of a purchasable SKU, for premium buttons.
Premium buttons additionally cannot have a ``label``, ``url``, or ``emoji``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
row: Optional[:class:`int`]
The relative row this button 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).
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"style",
"url",
"disabled",
"label",
"emoji",
"sku_id",
"row",
)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: ButtonComponent = MISSING
@overload
def __init__(
self: Button[None],
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
sku_id: Optional[int] = None,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
@overload
def __init__(
self: Button[V_co],
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
sku_id: Optional[int] = None,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
def __init__(
self,
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
sku_id: Optional[int] = None,
id: int = 0,
row: Optional[int] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not None
mutually_exclusive = 3 - (custom_id, url, sku_id).count(None)
if mutually_exclusive == 0:
custom_id = os.urandom(16).hex()
elif mutually_exclusive != 1:
raise TypeError("cannot mix url, sku_id and custom_id with Button")
if url is not None:
style = ButtonStyle.link
if sku_id is not None:
style = ButtonStyle.premium
if emoji is not None:
if isinstance(emoji, str):
emoji = PartialEmoji.from_str(emoji)
elif isinstance(emoji, _EmojiTag):
emoji = emoji._to_partial()
else:
raise TypeError(
f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}"
)
self._underlying = ButtonComponent._raw_construct(
type=ComponentType.button,
id=id,
custom_id=custom_id,
url=url,
disabled=disabled,
label=label,
style=style,
emoji=emoji,
sku_id=sku_id,
)
self.row = row
@property
def width(self) -> int:
return 1
@property
def style(self) -> ButtonStyle:
""":class:`disnake.ButtonStyle`: The style of the button."""
return self._underlying.style
@style.setter
def style(self, value: ButtonStyle) -> None:
self._underlying.style = value
@property
def custom_id(self) -> Optional[str]:
"""Optional[:class:`str`]: The ID of the button that gets received during an interaction.
If this button is for a URL or an SKU, it does not have a custom ID.
"""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: Optional[str]) -> None:
if value is not None and not isinstance(value, str):
raise TypeError("custom_id must be None or str")
self._underlying.custom_id = value
@property
def url(self) -> Optional[str]:
"""Optional[:class:`str`]: The URL this button sends you to."""
return self._underlying.url
@url.setter
def url(self, value: Optional[str]) -> None:
if value is not None and not isinstance(value, str):
raise TypeError("url must be None or str")
self._underlying.url = value
@property
def disabled(self) -> bool:
""":class:`bool`: Whether the button is disabled."""
return self._underlying.disabled
@disabled.setter
def disabled(self, value: bool) -> None:
self._underlying.disabled = bool(value)
@property
def label(self) -> Optional[str]:
"""Optional[:class:`str`]: The label of the button, if available."""
return self._underlying.label
@label.setter
def label(self, value: Optional[str]) -> None:
self._underlying.label = str(value) if value is not None else value
@property
def emoji(self) -> Optional[PartialEmoji]:
"""Optional[:class:`.PartialEmoji`]: The emoji of the button, if available."""
return self._underlying.emoji
@emoji.setter
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None:
if value is not None:
if isinstance(value, str):
self._underlying.emoji = PartialEmoji.from_str(value)
elif isinstance(value, _EmojiTag):
self._underlying.emoji = value._to_partial()
else:
raise TypeError(
f"expected str, Emoji, or PartialEmoji, received {value.__class__} instead"
)
else:
self._underlying.emoji = None
@property
def sku_id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of a purchasable SKU, for premium buttons.
.. versionadded:: 2.11
"""
return self._underlying.sku_id
@sku_id.setter
def sku_id(self, value: Optional[int]) -> None:
if value is not None and not isinstance(value, int):
raise TypeError("sku_id must be None or int")
self._underlying.sku_id = value
@classmethod
def from_component(cls, button: ButtonComponent) -> Self:
return cls(
style=button.style,
label=button.label,
disabled=button.disabled,
custom_id=button.custom_id,
url=button.url,
emoji=button.emoji,
sku_id=button.sku_id,
id=button.id,
row=None,
)
def is_dispatchable(self) -> bool:
return self.custom_id is not None
def is_persistent(self) -> bool:
if self.style is ButtonStyle.link:
return self.url is not None
elif self.style is ButtonStyle.premium:
return self.sku_id is not None
return super().is_persistent()
def refresh_component(self, button: ButtonComponent) -> None:
self._underlying = button
@overload
def button(
*,
label: Optional[str] = None,
custom_id: Optional[str] = None,
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
id: int = 0,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType[V_co, Button[V_co]]], DecoratedItem[Button[V_co]]]: ...
@overload
def button(
cls: Callable[P, B_co], *_: P.args, **kwargs: P.kwargs
) -> Callable[[ItemCallbackType[V_co, B_co]], DecoratedItem[B_co]]: ...
def button(
cls: Callable[..., B_co] = Button[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[V_co, B_co]], DecoratedItem[B_co]]:
"""A decorator that attaches a button to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`disnake.ui.View`, the :class:`disnake.ui.Button` that was
interacted with, and the :class:`disnake.MessageInteraction`.
.. note::
Link/Premium buttons cannot be created with this function,
since these buttons do not have a callback associated with them.
Consider creating a :class:`Button` manually instead, and adding it
using :meth:`View.add_item`.
Parameters
----------
cls: Callable[..., :class:`Button`]
A callable (may be a :class:`Button` subclass) to create a new instance of this component.
If provided, the other parameters described below do not apply.
Instead, this decorator will accept the same keywords as the passed callable/class does.
.. versionadded:: 2.6
label: Optional[:class:`str`]
The label of the button, if any.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
style: :class:`.ButtonStyle`
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
disabled: :class:`bool`
Whether the button is disabled. Defaults to ``False``.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
or a full :class:`.Emoji`.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
row: Optional[:class:`int`]
The relative row this button 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).
"""
if not callable(cls):
raise TypeError("cls argument must be callable")
def decorator(func: ItemCallbackType[V_co, B_co]) -> DecoratedItem[B_co]:
if not iscoroutinefunction(func):
raise TypeError("button function must be a coroutine function")
func.__discord_ui_model_type__ = cls
func.__discord_ui_model_kwargs__ = kwargs
return func # type: ignore
return decorator

View File

@@ -0,0 +1,138 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, List, Optional, Tuple, Union, cast
from ..colour import Colour
from ..components import Container as ContainerComponent
from ..enums import ComponentType
from ..utils import copy_doc
from .item import UIComponent, ensure_ui_component
if TYPE_CHECKING:
from typing_extensions import Self
from .action_row import ActionRow, ActionRowMessageComponent
from .file import File
from .media_gallery import MediaGallery
from .section import Section
from .separator import Separator
from .text_display import TextDisplay
ContainerChildUIComponent = Union[
ActionRow[ActionRowMessageComponent],
Section,
TextDisplay,
MediaGallery,
File,
Separator,
]
__all__ = ("Container",)
class Container(UIComponent):
"""Represents a UI container.
This is visually similar to :class:`.Embed`\\s, and contains other components.
.. versionadded:: 2.11
Parameters
----------
*components: Union[:class:`~.ui.ActionRow`, :class:`~.ui.Section`, :class:`~.ui.TextDisplay`, :class:`~.ui.MediaGallery`, :class:`~.ui.File`, :class:`~.ui.Separator`]
The components in this container.
accent_colour: Optional[:class:`.Colour`]
The accent colour of the container.
spoiler: :class:`bool`
Whether the container is marked as a spoiler. Defaults to ``False``.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
Attributes
----------
children: List[Union[:class:`~.ui.ActionRow`, :class:`~.ui.Section`, :class:`~.ui.TextDisplay`, :class:`~.ui.MediaGallery`, :class:`~.ui.File`, :class:`~.ui.Separator`]]
The list of child components in this container.
accent_colour: Optional[:class:`.Colour`]
The accent colour of the container.
An alias exists under ``accent_color``.
spoiler: :class:`bool`
Whether the container is marked as a spoiler.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"children",
"accent_colour",
"spoiler",
)
def __init__(
self,
*components: ContainerChildUIComponent,
accent_colour: Optional[Colour] = None,
spoiler: bool = False,
id: int = 0,
) -> None:
self._id: int = id
# this list can be modified without any runtime checks later on,
# just assume the user knows what they're doing at that point
self.children: List[ContainerChildUIComponent] = [
ensure_ui_component(c, "components") for c in components
]
self._accent_colour: Optional[Colour] = accent_colour
self.spoiler: bool = spoiler
# these are reimplemented here to store the value in a separate attribute,
# since `Container` lazily constructs `_underlying`, unlike most components
@property
@copy_doc(UIComponent.id)
def id(self) -> int:
return self._id
@id.setter
def id(self, value: int) -> None:
self._id = value
@property
def accent_colour(self) -> Optional[Colour]:
return self._accent_colour
@accent_colour.setter
def accent_colour(self, value: Optional[Union[int, Colour]]) -> None:
if isinstance(value, int):
self._accent_colour = Colour(value)
elif value is None or isinstance(value, Colour):
self._accent_colour = value
else:
raise TypeError(
f"Expected Colour, int, or None but received {type(value).__name__} instead."
)
accent_color = accent_colour
@property
def _underlying(self) -> ContainerComponent:
return ContainerComponent._raw_construct(
type=ComponentType.container,
id=self._id,
children=[comp._underlying for comp in self.children],
accent_colour=self._accent_colour,
spoiler=self.spoiler,
)
@classmethod
def from_component(cls, container: ContainerComponent) -> Self:
from .action_row import _to_ui_component
return cls(
*cast(
"List[ContainerChildUIComponent]",
[_to_ui_component(c) for c in container.children],
),
accent_colour=container.accent_colour,
spoiler=container.spoiler,
id=container.id,
)

View File

@@ -0,0 +1,121 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import copy
from typing import TYPE_CHECKING, ClassVar, Optional, Tuple
from ..components import FileComponent, UnfurledMediaItem, handle_media_item_input
from ..enums import ComponentType
from ..utils import MISSING
from .item import UIComponent
if TYPE_CHECKING:
from typing_extensions import Self
from ..components import LocalMediaItemInput
__all__ = ("File",)
class File(UIComponent):
"""Represents a UI file component.
.. versionadded:: 2.11
Parameters
----------
file: Union[:class:`str`, :class:`.UnfurledMediaItem`]
The file to display. This **only** supports attachment references (i.e.
using the ``attachment://<filename>`` syntax), not arbitrary URLs.
spoiler: :class:`bool`
Whether the file is marked as a spoiler. Defaults to ``False``.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"file",
"spoiler",
)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: FileComponent = MISSING
def __init__(
self,
file: LocalMediaItemInput,
*,
spoiler: bool = False,
id: int = 0,
) -> None:
file_media = handle_media_item_input(file)
if not file_media.url.startswith("attachment://"):
raise ValueError(
"File component only supports `attachment://` references, not external media URLs"
)
self._underlying = FileComponent._raw_construct(
type=ComponentType.file,
id=id,
file=file_media,
spoiler=spoiler,
name=None,
size=None,
)
@property
def file(self) -> UnfurledMediaItem:
""":class:`.UnfurledMediaItem`: The file to display."""
return self._underlying.file
@file.setter
def file(self, value: LocalMediaItemInput) -> None:
file_media = handle_media_item_input(value)
if not file_media.url.startswith("attachment://"):
raise ValueError(
"File component only supports `attachment://` references, not external media URLs"
)
self._underlying.file = file_media
@property
def spoiler(self) -> bool:
""":class:`bool`: Whether the file is marked as a spoiler."""
return self._underlying.spoiler
@spoiler.setter
def spoiler(self, value: bool) -> None:
self._underlying.spoiler = value
@property
def name(self) -> Optional[str]:
"""Optional[:class:`str`]: The name of the file.
This is available in objects from the API, and ignored when sending.
"""
return self._underlying.name
@property
def size(self) -> Optional[int]:
"""Optional[:class:`int`]: The size of the file.
This is available in objects from the API, and ignored when sending.
"""
return self._underlying.size
@classmethod
def from_component(cls, file: FileComponent) -> Self:
media = file.file
if not media.url.startswith("attachment://") and file.name:
# turn cdn url into `attachment://` url, retain other fields
media = copy.copy(media)
media.url = f"attachment://{file.name}"
self = cls(
file=media,
spoiler=file.spoiler,
id=file.id,
)
# copy read-only fields
self._underlying.name = file.name
self._underlying.size = file.size
return self

View File

@@ -0,0 +1,237 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Coroutine,
Dict,
Generic,
Optional,
Protocol,
Tuple,
Type,
TypeVar,
overload,
)
__all__ = (
"UIComponent",
"WrappedComponent",
"Item",
)
I = TypeVar("I", bound="Item[Any]")
V_co = TypeVar("V_co", bound="Optional[View]", covariant=True)
if TYPE_CHECKING:
from typing_extensions import Self
from ..client import Client
from ..components import ActionRowChildComponent, Component
from ..enums import ComponentType
from ..interactions import MessageInteraction
from ..types.components import ActionRowChildComponent as ActionRowChildComponentPayload
from .view import View
ItemCallbackType = Callable[[V_co, I, MessageInteraction], Coroutine[Any, Any, Any]]
ClientT = TypeVar("ClientT", bound="Client")
UIComponentT = TypeVar("UIComponentT", bound="UIComponent")
def ensure_ui_component(obj: UIComponentT, name: str = "component") -> UIComponentT:
if not isinstance(obj, UIComponent):
raise TypeError(f"{name} should be a valid UI component, got {type(obj).__name__}.")
return obj
class UIComponent(ABC):
"""Represents the base UI component that all UI components inherit from.
The following classes implement this ABC:
- :class:`disnake.ui.ActionRow`
- :class:`disnake.ui.Button`
- subtypes of :class:`disnake.ui.BaseSelect` (:class:`disnake.ui.ChannelSelect`, :class:`disnake.ui.MentionableSelect`, :class:`disnake.ui.RoleSelect`, :class:`disnake.ui.StringSelect`, :class:`disnake.ui.UserSelect`)
- :class:`disnake.ui.TextInput`
- :class:`disnake.ui.Section`
- :class:`disnake.ui.TextDisplay`
- :class:`disnake.ui.Thumbnail`
- :class:`disnake.ui.MediaGallery`
- :class:`disnake.ui.File`
- :class:`disnake.ui.Separator`
- :class:`disnake.ui.Container`
- :class:`disnake.ui.Label`
.. versionadded:: 2.11
"""
__repr_attributes__: ClassVar[Tuple[str, ...]]
@property
@abstractmethod
def _underlying(self) -> Component: ...
def __repr__(self) -> str:
attrs = " ".join(
f"{key.lstrip('_')}={getattr(self, key)!r}" for key in self.__repr_attributes__
)
return f"<{type(self).__name__} {attrs}>"
@property
def is_v2(self) -> bool:
return self._underlying.is_v2
@property
def type(self) -> ComponentType:
return self._underlying.type
@property
def id(self) -> int:
""":class:`int`: The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
return self._underlying.id
@id.setter
def id(self, value: int) -> None:
self._underlying.id = value
def to_component_dict(self) -> Dict[str, Any]:
return self._underlying.to_dict()
@classmethod
def from_component(cls, component: Component, /) -> Self:
return cls()
# Essentially the same as the base `UIComponent`, with the addition of `width`.
class WrappedComponent(UIComponent):
"""Represents the base UI component that all :class:`ActionRow`\\-compatible
UI components inherit from.
This class adds more functionality on top of the :class:`UIComponent` base class,
specifically for action rows.
The following classes implement this ABC:
- :class:`disnake.ui.Button`
- subtypes of :class:`disnake.ui.BaseSelect` (:class:`disnake.ui.ChannelSelect`, :class:`disnake.ui.MentionableSelect`, :class:`disnake.ui.RoleSelect`, :class:`disnake.ui.StringSelect`, :class:`disnake.ui.UserSelect`)
- :class:`disnake.ui.TextInput`
.. versionadded:: 2.4
"""
# the purpose of these two is just more precise typechecking compared to the base type
if TYPE_CHECKING:
@property
@abstractmethod
def _underlying(self) -> ActionRowChildComponent: ...
def to_component_dict(self) -> ActionRowChildComponentPayload: ...
@property
@abstractmethod
def width(self) -> int: ...
class Item(WrappedComponent, Generic[V_co]):
"""Represents the base UI item that all interactive UI items inherit from.
This class adds more functionality on top of the :class:`WrappedComponent` base class.
This functionality mostly relates to :class:`disnake.ui.View`.
The current UI items supported are:
- :class:`disnake.ui.Button`
- subtypes of :class:`disnake.ui.BaseSelect` (:class:`disnake.ui.ChannelSelect`, :class:`disnake.ui.MentionableSelect`, :class:`disnake.ui.RoleSelect`, :class:`disnake.ui.StringSelect`, :class:`disnake.ui.UserSelect`)
.. versionadded:: 2.0
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = ("row",)
@overload
def __init__(self: Item[None]) -> None: ...
@overload
def __init__(self: Item[V_co]) -> None: ...
def __init__(self) -> None:
self._view: V_co = None # type: ignore
self._row: Optional[int] = None
self._rendered_row: Optional[int] = None
# This works mostly well but there is a gotcha with
# the interaction with from_component, since that technically provides
# a custom_id most dispatchable items would get this set to True even though
# it might not be provided by the library user. However, this edge case doesn't
# actually affect the intended purpose of this check because from_component is
# only called upon edit and we're mainly interested during initial creation time.
self._provided_custom_id: bool = False
def refresh_component(self, component: ActionRowChildComponent) -> None:
return None
def refresh_state(self, interaction: MessageInteraction) -> None:
return None
def is_dispatchable(self) -> bool:
return False
def is_persistent(self) -> bool:
return self._provided_custom_id
@property
def row(self) -> Optional[int]:
return self._row
@row.setter
def row(self, value: Optional[int]) -> None:
if value is None:
self._row = None
elif 5 > value >= 0:
self._row = value
else:
raise ValueError("row cannot be negative or greater than or equal to 5")
@property
def view(self) -> V_co:
"""Optional[:class:`View`]: The underlying view for this item."""
return self._view
async def callback(self, interaction: MessageInteraction[ClientT], /) -> None:
"""|coro|
The callback associated with this UI item.
This can be overridden by subclasses.
Parameters
----------
interaction: :class:`.MessageInteraction`
The interaction that triggered this UI item.
"""
pass
SelfViewT = TypeVar("SelfViewT", bound="Optional[View]")
# While the decorators don't actually return a descriptor that matches this protocol,
# this protocol ensures that type checkers don't complain about statements like `self.button.disabled = True`,
# which work as `View.__init__` replaces the handler with the item.
class DecoratedItem(Protocol[I]):
@overload
def __get__(self, obj: None, objtype: Type[SelfViewT]) -> ItemCallbackType[SelfViewT, I]: ...
@overload
def __get__(self, obj: Any, objtype: Any) -> I: ...

View File

@@ -0,0 +1,106 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Tuple, Union, cast
from ..components import Label as LabelComponent
from ..enums import ComponentType
from ..utils import copy_doc
from .item import UIComponent, ensure_ui_component
if TYPE_CHECKING:
from typing_extensions import Self
from ._types import AnySelect
from .text_input import TextInput
LabelChildUIComponent = Union[TextInput, AnySelect[Any]]
__all__ = ("Label",)
class Label(UIComponent):
"""Represents a UI label.
This wraps other components with a label and an optional description,
and can only be used in modals.
.. versionadded:: 2.11
Parameters
----------
text: :class:`str`
The label text.
component: Union[:class:`TextInput`, :class:`BaseSelect`]
The component within the label.
Currently supports :class:`.ui.TextInput` and
select menus (e.g. :class:`.ui.StringSelect`).
description: Optional[:class:`str`]
The description text for the label.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
Attributes
----------
text: :class:`str`
The label text.
component: Union[:class:`TextInput`, :class:`BaseSelect`]
The component within the label.
description: Optional[:class:`str`]
The description text for the label.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"text",
"description",
"component",
)
def __init__(
self,
text: str,
component: LabelChildUIComponent,
*,
description: Optional[str] = None,
id: int = 0,
) -> None:
self._id: int = id
self.text: str = text
self.description: Optional[str] = description
self.component: LabelChildUIComponent = ensure_ui_component(component)
# these are reimplemented here to store the value in a separate attribute,
# since `Label` lazily constructs `_underlying`, unlike most components
@property
@copy_doc(UIComponent.id)
def id(self) -> int:
return self._id
@id.setter
def id(self, value: int) -> None:
self._id = value
@property
def _underlying(self) -> LabelComponent:
return LabelComponent._raw_construct(
type=ComponentType.label,
id=self._id,
text=self.text,
description=self.description,
component=self.component._underlying,
)
@classmethod
def from_component(cls, label: LabelComponent) -> Self:
from .action_row import _to_ui_component
return cls(
text=label.text,
description=label.description,
component=cast("LabelChildUIComponent", _to_ui_component(label.component)),
id=label.id,
)

View File

@@ -0,0 +1,60 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, List, Sequence, Tuple
from ..components import MediaGallery as MediaGalleryComponent, MediaGalleryItem
from ..enums import ComponentType
from ..utils import MISSING
from .item import UIComponent
if TYPE_CHECKING:
from typing_extensions import Self
__all__ = ("MediaGallery",)
class MediaGallery(UIComponent):
"""Represents a UI media gallery.
This allows displaying up to 10 images in a gallery.
.. versionadded:: 2.11
Parameters
----------
*items: :class:`.MediaGalleryItem`
The list of images in this gallery (up to 10).
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = ("items",)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: MediaGalleryComponent = MISSING
def __init__(self, *items: MediaGalleryItem, id: int = 0) -> None:
self._underlying = MediaGalleryComponent._raw_construct(
type=ComponentType.media_gallery,
id=id,
items=list(items),
)
@property
def items(self) -> List[MediaGalleryItem]:
"""List[:class:`.MediaGalleryItem`]: The images in this gallery."""
return self._underlying.items
@items.setter
def items(self, values: Sequence[MediaGalleryItem]) -> None:
self._underlying.items = list(values)
@classmethod
def from_component(cls, media_gallery: MediaGalleryComponent) -> Self:
return cls(
*media_gallery.items,
id=media_gallery.id,
)

View File

@@ -0,0 +1,339 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import asyncio
import os
import sys
import traceback
from functools import partial
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, TypeVar, Union, cast
from ..enums import TextInputStyle
from ..utils import MISSING
from .action_row import ActionRow, normalize_components
from .item import ensure_ui_component
from .label import Label
from .text_input import TextInput
if TYPE_CHECKING:
from ..client import Client
from ..interactions.modal import ModalInteraction
from ..state import ConnectionState
from ..types.components import (
Modal as ModalPayload,
ModalTopLevelComponent as ModalTopLevelComponentPayload,
)
from ..ui._types import ModalComponents, ModalTopLevelComponent
# backwards compatibility, `TextInput` internally gets wrapped in an action row (deprecated)
ModalTopLevelComponentInput = Union[ModalTopLevelComponent, TextInput]
__all__ = ("Modal",)
ClientT = TypeVar("ClientT", bound="Client")
class Modal:
"""Represents a UI Modal.
.. versionadded:: 2.4
Parameters
----------
title: :class:`str`
The title of the modal.
components: |modal_components_type|
The components to display in the modal. A maximum of 5.
Currently supports the following components:
- :class:`.ui.TextDisplay`
- :class:`.ui.TextInput`, in a :class:`.ui.Label`
- select menus (e.g. :class:`.ui.StringSelect`), in a :class:`.ui.Label`
.. versionchanged:: 2.11
Using action rows in modals or passing :class:`.ui.TextInput` directly
(which implicitly wraps it in an action row) is deprecated.
Use :class:`.ui.TextInput` inside a :class:`.ui.Label` instead.
custom_id: :class:`str`
The custom ID of the modal. This is usually not required.
If not given, then a unique one is generated for you.
.. note::
:class:`Modal`\\s are identified based on the user ID that triggered the
modal, and this ``custom_id``.
This can result in collisions when a user opens a modal with the same ``custom_id`` on
two separate devices, for example.
To avoid such issues, consider not specifying a ``custom_id`` to use an automatically generated one,
or include a unique value in the custom ID (e.g. the original interaction ID).
timeout: :class:`float`
The time to wait until the modal is removed from cache, if no interaction is made.
Modals without timeouts are not supported, since there's no event for when a modal is closed.
Defaults to 600 seconds.
"""
__slots__ = (
"title",
"custom_id",
"components",
"timeout",
"__remove_callback",
"__timeout_handle",
)
def __init__(
self,
*,
title: str,
components: ModalComponents,
custom_id: str = MISSING,
timeout: float = 600,
) -> None:
if timeout is None: # pyright: ignore[reportUnnecessaryComparison]
raise ValueError("Timeout may not be None")
items = normalize_components(components)
if len(items) > 5:
raise ValueError("Maximum number of components exceeded.")
self.title: str = title
self.custom_id: str = os.urandom(16).hex() if custom_id is MISSING else custom_id
self.components: List[ModalTopLevelComponent] = list(items)
self.timeout: float = timeout
# function for the modal to remove itself from the store, if any
self.__remove_callback: Optional[Callable[[Modal], None]] = None
# timer handle for the scheduled timeout
self.__timeout_handle: Optional[asyncio.TimerHandle] = None
def __repr__(self) -> str:
return (
f"<Modal custom_id={self.custom_id!r} title={self.title!r} "
f"components={self.components!r}>"
)
def append_component(
self, component: Union[ModalTopLevelComponentInput, List[ModalTopLevelComponentInput]]
) -> None:
"""Adds one or multiple component(s) to the modal.
Parameters
----------
component: |modal_components_type|
The component(s) to add to the modal.
This can be a single component or a list of components.
See :class:`Modal.components <Modal>` for supported components.
.. versionchanged:: 2.11
Using action rows in modals or passing :class:`.ui.TextInput` directly
(which implicitly wraps it in an action row) is deprecated.
Use :class:`.ui.TextInput` inside a :class:`.ui.Label` instead.
Raises
------
ValueError
Maximum number of components (5) exceeded.
TypeError
An invalid component object was passed.
"""
if not isinstance(component, list):
component = [component]
if len(self.components) + len(component) >= 5:
raise ValueError("Maximum number of components exceeded.")
for c in component:
c = ensure_ui_component(c)
# backwards compatibility, action rows in modals are deprecated.
if isinstance(c, TextInput):
c = ActionRow(c)
self.components.append(c)
def add_text_input(
self,
*,
label: str,
custom_id: str = MISSING,
style: TextInputStyle = TextInputStyle.short,
placeholder: Optional[str] = None,
value: Optional[str] = None,
required: bool = True,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
) -> None:
"""Creates and adds a text input component to the modal.
To append an existing component instance, use :meth:`append_component`.
Parameters
----------
label: :class:`str`
The label of the text input.
custom_id: :class:`str`
The ID of the text input that gets received during an interaction.
If not given then one is generated for you.
style: :class:`.TextInputStyle`
The style of the text input.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is entered.
value: Optional[:class:`str`]
The pre-filled value of the text input.
required: :class:`bool`
Whether the text input is required. Defaults to ``True``.
min_length: Optional[:class:`int`]
The minimum length of the text input.
max_length: Optional[:class:`int`]
The maximum length of the text input.
Raises
------
ValueError
Maximum number of components (5) exceeded.
"""
self.append_component(
Label(
label,
TextInput(
custom_id=custom_id,
style=style,
placeholder=placeholder,
value=value,
required=required,
min_length=min_length,
max_length=max_length,
),
)
)
async def callback(self, interaction: ModalInteraction[ClientT], /) -> None:
"""|coro|
The callback associated with this modal.
This can be overridden by subclasses.
Parameters
----------
interaction: :class:`.ModalInteraction`
The interaction that triggered this modal.
"""
pass
async def on_error(self, error: Exception, interaction: ModalInteraction[ClientT]) -> None:
"""|coro|
A callback that is called when an error occurs.
The default implementation prints the traceback to stderr.
Parameters
----------
error: :class:`Exception`
The exception that was raised.
interaction: :class:`.ModalInteraction`
The interaction that triggered this modal.
"""
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)
async def on_timeout(self) -> None:
"""|coro|
A callback that is called when the modal is removed from the cache
without an interaction being made.
"""
pass
def to_components(self) -> ModalPayload:
return {
"title": self.title,
"custom_id": self.custom_id,
"components": cast(
"List[ModalTopLevelComponentPayload]",
[component.to_component_dict() for component in self.components],
),
}
async def _scheduled_task(self, interaction: ModalInteraction) -> None:
try:
await self.callback(interaction)
except Exception as e:
await self.on_error(e, interaction)
finally:
if interaction.response._response_type is None:
# If the interaction was not successfully responded to, the modal didn't close for the user.
# Since the timeout was already stopped at this point, restart it.
self._start_listening(self.__remove_callback)
else:
# Otherwise, the modal closed for the user; remove it from the store.
self._stop_listening()
def _start_listening(self, remove_callback: Optional[Callable[[Modal], None]]) -> None:
self.__remove_callback = remove_callback
loop = asyncio.get_running_loop()
if self.__timeout_handle is not None:
# shouldn't get here, but handled just in case
self.__timeout_handle.cancel()
# start timeout
self.__timeout_handle = loop.call_later(self.timeout, self._dispatch_timeout)
def _stop_listening(self) -> None:
# cancel timeout
if self.__timeout_handle is not None:
self.__timeout_handle.cancel()
self.__timeout_handle = None
# remove modal from store
if self.__remove_callback is not None:
self.__remove_callback(self)
self.__remove_callback = None
def _dispatch_timeout(self) -> None:
self._stop_listening()
asyncio.create_task(self.on_timeout(), name=f"disnake-ui-modal-timeout-{self.custom_id}")
def dispatch(self, interaction: ModalInteraction) -> None:
# stop the timeout, but don't remove the modal from the store yet in case the
# response fails and the modal stays open
if self.__timeout_handle is not None:
self.__timeout_handle.cancel()
asyncio.create_task(
self._scheduled_task(interaction), name=f"disnake-ui-modal-dispatch-{self.custom_id}"
)
class ModalStore:
def __init__(self, state: ConnectionState) -> None:
self._state = state
# (user_id, Modal.custom_id): Modal
self._modals: Dict[Tuple[int, str], Modal] = {}
def add_modal(self, user_id: int, modal: Modal) -> None:
key = (user_id, modal.custom_id)
# if another modal with the same user+custom_id already exists,
# stop its timeout to avoid overlaps/collisions
if (existing := self._modals.get(key)) is not None:
existing._stop_listening()
# start timeout, store modal
remove_callback = partial(self.remove_modal, user_id)
modal._start_listening(remove_callback)
self._modals[key] = modal
def remove_modal(self, user_id: int, modal: Modal) -> None:
self._modals.pop((user_id, modal.custom_id), None)
def dispatch(self, interaction: ModalInteraction) -> None:
key = (interaction.author.id, interaction.custom_id)
if (modal := self._modals.get(key)) is not None:
modal.dispatch(interaction)

View File

@@ -0,0 +1,101 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar, List, Tuple, Union, cast
from ..components import Section as SectionComponent
from ..enums import ComponentType
from ..utils import copy_doc
from .item import UIComponent, ensure_ui_component
from .text_display import TextDisplay
if TYPE_CHECKING:
from typing_extensions import Self
from .button import Button
from .thumbnail import Thumbnail
SectionAccessoryUIComponent = Union[Thumbnail, Button[Any]]
__all__ = ("Section",)
class Section(UIComponent):
"""Represents a UI section.
This allows displaying an accessory (thumbnail or button) next to a block of text.
.. versionadded:: 2.11
Parameters
----------
*components: Union[:class:`str`, :class:`~.ui.TextDisplay`]
The text items in this section (up to 3).
accessory: Union[:class:`~.ui.Thumbnail`, :class:`~.ui.Button`]
The accessory component displayed next to the section text.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
Attributes
----------
children: List[:class:`~.ui.TextDisplay`]
The list of text items in this section.
accessory: Union[:class:`~.ui.Thumbnail`, :class:`~.ui.Button`]
The accessory component displayed next to the section text.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"children",
"accessory",
)
def __init__(
self,
*components: Union[str, TextDisplay],
accessory: SectionAccessoryUIComponent,
id: int = 0,
) -> None:
self._id: int = id
# this list can be modified without any runtime checks later on,
# just assume the user knows what they're doing at that point
self.children: List[TextDisplay] = [
TextDisplay(c) if isinstance(c, str) else ensure_ui_component(c, "components")
for c in components
]
self.accessory: SectionAccessoryUIComponent = ensure_ui_component(accessory, "accessory")
# these are reimplemented here to store the value in a separate attribute,
# since `Section` lazily constructs `_underlying`, unlike most components
@property
@copy_doc(UIComponent.id)
def id(self) -> int:
return self._id
@id.setter
def id(self, value: int) -> None:
self._id = value
@property
def _underlying(self) -> SectionComponent:
return SectionComponent._raw_construct(
type=ComponentType.section,
id=self._id,
children=[comp._underlying for comp in self.children],
accessory=self.accessory._underlying,
)
@classmethod
def from_component(cls, section: SectionComponent) -> Self:
from .action_row import _to_ui_component
return cls(
*cast(
"List[TextDisplay]",
[_to_ui_component(c) for c in section.children],
),
accessory=cast("SectionAccessoryUIComponent", _to_ui_component(section.accessory)),
id=section.id,
)

View File

@@ -0,0 +1,27 @@
# SPDX-License-Identifier: MIT
"""disnake.ui.select
~~~~~~~~~~~~~~~~~~
Select Menu UI Kit Types
:copyright: (c) 2021-present Disnake Development
:license: MIT, see LICENSE for more details.
"""
from . import base, channel, mentionable, role, string, user
from .base import *
from .channel import *
from .mentionable import *
from .role import *
from .string import *
from .user import *
__all__ = []
__all__.extend(base.__all__)
__all__.extend(channel.__all__)
__all__.extend(mentionable.__all__)
__all__.extend(role.__all__)
__all__.extend(string.__all__)
__all__.extend(user.__all__)

View File

@@ -0,0 +1,278 @@
# 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

View File

@@ -0,0 +1,288 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
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, SelectDefaultValueType
from ...object import Object
from ...threads import Thread
from ...utils import MISSING
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
__all__ = (
"ChannelSelect",
"channel_select",
)
class ChannelSelect(BaseSelect[ChannelSelectMenu, "AnyChannel", V_co]):
"""Represents a UI channel select menu.
This is usually represented as a drop down menu.
In order to get the selected items that the user has chosen, use :attr:`.values`.
.. versionadded:: 2.7
Parameters
----------
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
If not given then one is generated for you.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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.
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
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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
----------
values: List[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.abc.PrivateChannel`, :class:`.PartialMessageable`]]
A list of channels that have been selected by the user.
"""
__repr_attributes__: ClassVar[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],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
channel_types: Optional[List[ChannelType]] = None,
default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
@overload
def __init__(
self: ChannelSelect[V_co],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
channel_types: Optional[List[ChannelType]] = None,
default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
def __init__(
self,
*,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
channel_types: Optional[List[ChannelType]] = None,
default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None:
super().__init__(
ChannelSelectMenu,
ComponentType.channel_select,
custom_id=custom_id,
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
disabled=disabled,
default_values=default_values,
required=required,
id=id,
row=row,
)
self._underlying.channel_types = channel_types or None
@classmethod
def from_component(cls, component: ChannelSelectMenu) -> Self:
return cls(
custom_id=component.custom_id,
placeholder=component.placeholder,
min_values=component.min_values,
max_values=component.max_values,
disabled=component.disabled,
channel_types=component.channel_types,
default_values=component.default_values,
required=component.required,
id=component.id,
row=None,
)
@property
def channel_types(self) -> Optional[List[ChannelType]]:
"""Optional[List[:class:`disnake.ChannelType`]]: A list of channel types that can be selected in this select menu."""
return self._underlying.channel_types
@channel_types.setter
def channel_types(self, value: Optional[List[ChannelType]]) -> None:
if value is not None:
if not isinstance(value, list):
raise TypeError("channel_types must be a list of ChannelType")
if not all(isinstance(obj, ChannelType) for obj in value):
raise TypeError("all list items must be ChannelType")
self._underlying.channel_types = value
S_co = TypeVar("S_co", bound="ChannelSelect", covariant=True)
@overload
def channel_select(
*,
placeholder: Optional[str] = None,
custom_id: str = ...,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
channel_types: Optional[List[ChannelType]] = None,
default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None,
id: int = 0,
row: Optional[int] = None,
) -> Callable[
[ItemCallbackType[V_co, ChannelSelect[V_co]]], DecoratedItem[ChannelSelect[V_co]]
]: ...
@overload
def channel_select(
cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ...
def channel_select(
cls: Callable[..., S_co] = ChannelSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a channel select menu to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`disnake.ui.View`, the :class:`disnake.ui.ChannelSelect` that was
interacted with, and the :class:`disnake.MessageInteraction`.
In order to get the selected items that the user has chosen within the callback
use :attr:`ChannelSelect.values`.
.. versionadded:: 2.7
Parameters
----------
cls: Callable[..., :class:`ChannelSelect`]
A callable (may be a :class:`ChannelSelect` subclass) to create a new instance of this component.
If provided, the other parameters described below do not apply.
Instead, this decorator will accept the same keywords as the passed callable/class does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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. 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
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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).
"""
return _create_decorator(cls, **kwargs)

View File

@@ -0,0 +1,261 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
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
from ...role import Role
from ...user import ClientUser, User
from ...utils import MISSING
from .base import BaseSelect, P, SelectDefaultValueMultiInputType, V_co, _create_decorator
if TYPE_CHECKING:
from typing_extensions import Self
from ..item import DecoratedItem, ItemCallbackType
__all__ = (
"MentionableSelect",
"mentionable_select",
)
class MentionableSelect(BaseSelect[MentionableSelectMenu, "Union[User, Member, Role]", V_co]):
"""Represents a UI mentionable (user/member/role) select menu.
This is usually represented as a drop down menu.
In order to get the selected items that the user has chosen, use :attr:`.values`.
.. versionadded:: 2.7
Parameters
----------
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
If not given then one is generated for you.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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.
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
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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
----------
values: List[Union[:class:`~disnake.User`, :class:`.Member`, :class:`.Role`]]
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],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[
Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]]
] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
@overload
def __init__(
self: MentionableSelect[V_co],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[
Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]]
] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
def __init__(
self,
*,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[
Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]]
] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None:
super().__init__(
MentionableSelectMenu,
ComponentType.mentionable_select,
custom_id=custom_id,
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
disabled=disabled,
default_values=default_values,
required=required,
id=id,
row=row,
)
@classmethod
def from_component(cls, component: MentionableSelectMenu) -> Self:
return cls(
custom_id=component.custom_id,
placeholder=component.placeholder,
min_values=component.min_values,
max_values=component.max_values,
disabled=component.disabled,
default_values=component.default_values,
required=component.required,
id=component.id,
row=None,
)
S_co = TypeVar("S_co", bound="MentionableSelect", covariant=True)
@overload
def mentionable_select(
*,
placeholder: Optional[str] = None,
custom_id: str = ...,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[
Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]]
] = None,
id: int = 0,
row: Optional[int] = None,
) -> Callable[
[ItemCallbackType[V_co, MentionableSelect[V_co]]], DecoratedItem[MentionableSelect[V_co]]
]: ...
@overload
def mentionable_select(
cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ...
def mentionable_select(
cls: Callable[..., S_co] = MentionableSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a mentionable (user/member/role) select menu to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`disnake.ui.View`, the :class:`disnake.ui.MentionableSelect` that was
interacted with, and the :class:`disnake.MessageInteraction`.
In order to get the selected items that the user has chosen within the callback
use :attr:`MentionableSelect.values`.
.. versionadded:: 2.7
Parameters
----------
cls: Callable[..., :class:`MentionableSelect`]
A callable (may be a :class:`MentionableSelect` subclass) to create a new instance of this component.
If provided, the other parameters described below do not apply.
Instead, this decorator will accept the same keywords as the passed callable/class does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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. 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
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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).
"""
return _create_decorator(cls, **kwargs)

View File

@@ -0,0 +1,244 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
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
from ...role import Role
from ...utils import MISSING
from .base import BaseSelect, P, SelectDefaultValueInputType, V_co, _create_decorator
if TYPE_CHECKING:
from typing_extensions import Self
from ..item import DecoratedItem, ItemCallbackType
__all__ = (
"RoleSelect",
"role_select",
)
class RoleSelect(BaseSelect[RoleSelectMenu, "Role", V_co]):
"""Represents a UI role select menu.
This is usually represented as a drop down menu.
In order to get the selected items that the user has chosen, use :attr:`.values`.
.. versionadded:: 2.7
Parameters
----------
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
If not given then one is generated for you.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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.
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
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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
----------
values: List[:class:`.Role`]
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],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
@overload
def __init__(
self: RoleSelect[V_co],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
def __init__(
self,
*,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None:
super().__init__(
RoleSelectMenu,
ComponentType.role_select,
custom_id=custom_id,
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
disabled=disabled,
default_values=default_values,
required=required,
id=id,
row=row,
)
@classmethod
def from_component(cls, component: RoleSelectMenu) -> Self:
return cls(
custom_id=component.custom_id,
placeholder=component.placeholder,
min_values=component.min_values,
max_values=component.max_values,
disabled=component.disabled,
default_values=component.default_values,
required=component.required,
id=component.id,
row=None,
)
S_co = TypeVar("S_co", bound="RoleSelect", covariant=True)
@overload
def role_select(
*,
placeholder: Optional[str] = None,
custom_id: str = ...,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None,
id: int = 0,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType[V_co, RoleSelect[V_co]]], DecoratedItem[RoleSelect[V_co]]]: ...
@overload
def role_select(
cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ...
def role_select(
cls: Callable[..., S_co] = RoleSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a role select menu to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`disnake.ui.View`, the :class:`disnake.ui.RoleSelect` that was
interacted with, and the :class:`disnake.MessageInteraction`.
In order to get the selected items that the user has chosen within the callback
use :attr:`RoleSelect.values`.
.. versionadded:: 2.7
Parameters
----------
cls: Callable[..., :class:`RoleSelect`]
A callable (may be a :class:`RoleSelect` subclass) to create a new instance of this component.
If provided, the other parameters described below do not apply.
Instead, this decorator will accept the same keywords as the passed callable/class does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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. 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
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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).
"""
return _create_decorator(cls, **kwargs)

View File

@@ -0,0 +1,358 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Dict,
List,
Mapping,
Optional,
Tuple,
Type,
TypeVar,
Union,
overload,
)
from ...abc import Snowflake
from ...components import SelectOption, StringSelectMenu
from ...enums import ComponentType, SelectDefaultValueType
from ...utils import MISSING
from .base import BaseSelect, P, V_co, _create_decorator
if TYPE_CHECKING:
from typing_extensions import Self
from ...emoji import Emoji
from ...partial_emoji import PartialEmoji
from ..item import DecoratedItem, ItemCallbackType
__all__ = (
"StringSelect",
"Select",
"string_select",
"select",
)
SelectOptionInput = Union[List[SelectOption], List[str], Dict[str, str]]
def _parse_select_options(options: SelectOptionInput) -> List[SelectOption]:
if isinstance(options, dict):
return [SelectOption(label=key, value=val) for key, val in options.items()]
return [opt if isinstance(opt, SelectOption) else SelectOption(label=opt) for opt in options]
class StringSelect(BaseSelect[StringSelectMenu, str, V_co]):
"""Represents a UI string select menu.
This is usually represented as a drop down menu.
In order to get the selected items that the user has chosen, use :attr:`.values`.
.. versionadded:: 2.0
.. versionchanged:: 2.7
Renamed from ``Select`` to ``StringSelect``.
Parameters
----------
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
If not given then one is generated for you.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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.
options: Union[List[:class:`disnake.SelectOption`], List[:class:`str`], Dict[:class:`str`, :class:`str`]]
A list of options that can be selected in this menu. Use explicit :class:`.SelectOption`\\s
for fine-grained control over the options. Alternatively, a list of strings will be treated
as a list of labels, and a dict will be treated as a mapping of labels to values.
.. versionchanged:: 2.5
Now also accepts a list of str or a dict of str to str, which are then appropriately parsed as
:class:`.SelectOption` labels and values.
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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
----------
values: List[:class:`str`]
A list of values that have been selected by the user.
"""
__repr_attributes__: ClassVar[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],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
options: SelectOptionInput = ...,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
@overload
def __init__(
self: StringSelect[V_co],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
options: SelectOptionInput = ...,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
def __init__(
self,
*,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
options: SelectOptionInput = MISSING,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None:
super().__init__(
StringSelectMenu,
ComponentType.string_select,
custom_id=custom_id,
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
disabled=disabled,
default_values=None,
required=required,
id=id,
row=row,
)
self._underlying.options = [] if options is MISSING else _parse_select_options(options)
@classmethod
def from_component(cls, component: StringSelectMenu) -> Self:
return cls(
custom_id=component.custom_id,
placeholder=component.placeholder,
min_values=component.min_values,
max_values=component.max_values,
disabled=component.disabled,
options=component.options,
required=component.required,
id=component.id,
row=None,
)
@property
def options(self) -> List[SelectOption]:
"""List[:class:`disnake.SelectOption`]: A list of options that can be selected in this select menu."""
return self._underlying.options
@options.setter
def options(self, value: List[SelectOption]) -> None:
if not isinstance(value, list):
raise TypeError("options must be a list of SelectOption")
if not all(isinstance(obj, SelectOption) for obj in value):
raise TypeError("all list items must subclass SelectOption")
self._underlying.options = value
def add_option(
self,
*,
label: str,
value: str = MISSING,
description: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
default: bool = False,
) -> None:
"""Adds an option to the select menu.
To append a pre-existing :class:`.SelectOption` use the
:meth:`append_option` method instead.
Parameters
----------
label: :class:`str`
The label of the option. This is displayed to users.
Can only be up to 100 characters.
value: :class:`str`
The value of the option. This is not displayed to users.
If not given, defaults to the label. Can only be up to 100 characters.
description: Optional[:class:`str`]
An additional description of the option, if any.
Can only be up to 100 characters.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the option, if available. This can either be a string representing
the custom or unicode emoji or an instance of :class:`.PartialEmoji` or :class:`.Emoji`.
default: :class:`bool`
Whether this option is selected by default.
Raises
------
ValueError
The number of options exceeds 25.
"""
option = SelectOption(
label=label,
value=value,
description=description,
emoji=emoji,
default=default,
)
self.append_option(option)
def append_option(self, option: SelectOption) -> None:
"""Appends an option to the select menu.
Parameters
----------
option: :class:`disnake.SelectOption`
The option to append to the select menu.
Raises
------
ValueError
The number of options exceeds 25.
"""
if len(self._underlying.options) >= 25:
raise ValueError("maximum number of options already provided")
self._underlying.options.append(option)
Select = StringSelect # backwards compatibility
S_co = TypeVar("S_co", bound="StringSelect", covariant=True)
@overload
def string_select(
*,
placeholder: Optional[str] = None,
custom_id: str = ...,
min_values: int = 1,
max_values: int = 1,
options: SelectOptionInput = ...,
disabled: bool = False,
id: int = 0,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType[V_co, StringSelect[V_co]]], DecoratedItem[StringSelect[V_co]]]: ...
@overload
def string_select(
cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ...
def string_select(
cls: Callable[..., S_co] = StringSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a string select menu to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`disnake.ui.View`, the :class:`disnake.ui.StringSelect` that was
interacted with, and the :class:`disnake.MessageInteraction`.
In order to get the selected items that the user has chosen within the callback
use :attr:`StringSelect.values`.
.. versionchanged:: 2.7
Renamed from ``select`` to ``string_select``.
Parameters
----------
cls: Callable[..., :class:`StringSelect`]
A callable (may be a :class:`StringSelect` subclass) to create a new instance of this component.
If provided, the other parameters described below do not apply.
Instead, this decorator will accept the same keywords as the passed callable/class does.
.. versionadded:: 2.6
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
options: Union[List[:class:`disnake.SelectOption`], List[:class:`str`], Dict[:class:`str`, :class:`str`]]
A list of options that can be selected in this menu. Use explicit :class:`.SelectOption`\\s
for fine-grained control over the options. Alternatively, a list of strings will be treated
as a list of labels, and a dict will be treated as a mapping of labels to values.
.. versionchanged:: 2.5
Now also accepts a list of str or a dict of str to str, which are then appropriately parsed as
:class:`.SelectOption` labels and values.
disabled: :class:`bool`
Whether the select is disabled. Defaults to ``False``.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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).
"""
return _create_decorator(cls, **kwargs)
select = string_select # backwards compatibility

View File

@@ -0,0 +1,246 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
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
from ...object import Object
from ...user import ClientUser, User
from ...utils import MISSING
from .base import BaseSelect, P, SelectDefaultValueInputType, V_co, _create_decorator
if TYPE_CHECKING:
from typing_extensions import Self
from ..item import DecoratedItem, ItemCallbackType
__all__ = (
"UserSelect",
"user_select",
)
class UserSelect(BaseSelect[UserSelectMenu, "Union[User, Member]", V_co]):
"""Represents a UI user select menu.
This is usually represented as a drop down menu.
In order to get the selected items that the user has chosen, use :attr:`.values`.
.. versionadded:: 2.7
Parameters
----------
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
If not given then one is generated for you.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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.
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
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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
----------
values: List[:class:`~disnake.User`, :class:`.Member`]
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],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
@overload
def __init__(
self: UserSelect[V_co],
*,
custom_id: str = ...,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
def __init__(
self,
*,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None,
required: bool = True,
id: int = 0,
row: Optional[int] = None,
) -> None:
super().__init__(
UserSelectMenu,
ComponentType.user_select,
custom_id=custom_id,
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
disabled=disabled,
default_values=default_values,
required=required,
id=id,
row=row,
)
@classmethod
def from_component(cls, component: UserSelectMenu) -> Self:
return cls(
custom_id=component.custom_id,
placeholder=component.placeholder,
min_values=component.min_values,
max_values=component.max_values,
disabled=component.disabled,
default_values=component.default_values,
required=component.required,
id=component.id,
row=None,
)
S_co = TypeVar("S_co", bound="UserSelect", covariant=True)
@overload
def user_select(
*,
placeholder: Optional[str] = None,
custom_id: str = ...,
min_values: int = 1,
max_values: int = 1,
disabled: bool = False,
default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None,
id: int = 0,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType[V_co, UserSelect[V_co]]], DecoratedItem[UserSelect[V_co]]]: ...
@overload
def user_select(
cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ...
def user_select(
cls: Callable[..., S_co] = UserSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a user select menu to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`disnake.ui.View`, the :class:`disnake.ui.UserSelect` that was
interacted with, and the :class:`disnake.MessageInteraction`.
In order to get the selected items that the user has chosen within the callback
use :attr:`UserSelect.values`.
.. versionadded:: 2.7
Parameters
----------
cls: Callable[..., :class:`UserSelect`]
A callable (may be a :class:`UserSelect` subclass) to create a new instance of this component.
If provided, the other parameters described below do not apply.
Instead, this decorator will accept the same keywords as the passed callable/class does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
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. 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
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
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).
"""
return _create_decorator(cls, **kwargs)

View File

@@ -0,0 +1,82 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, Tuple
from ..components import Separator as SeparatorComponent
from ..enums import ComponentType, SeparatorSpacing
from ..utils import MISSING
from .item import UIComponent
if TYPE_CHECKING:
from typing_extensions import Self
__all__ = ("Separator",)
class Separator(UIComponent):
"""Represents a UI separator.
.. versionadded:: 2.11
Parameters
----------
divider: :class:`bool`
Whether the separator should be visible, instead of just being vertical padding/spacing.
Defaults to ``True``.
spacing: :class:`.SeparatorSpacing`
The size of the separator padding.
Defaults to :attr:`~.SeparatorSpacing.small`.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"divider",
"spacing",
)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: SeparatorComponent = MISSING
def __init__(
self,
*,
divider: bool = True,
spacing: SeparatorSpacing = SeparatorSpacing.small,
id: int = 0,
) -> None:
self._underlying = SeparatorComponent._raw_construct(
type=ComponentType.separator,
id=id,
divider=divider,
spacing=spacing,
)
@property
def divider(self) -> bool:
""":class:`bool`: Whether the separator should be visible, instead of just being vertical padding/spacing."""
return self._underlying.divider
@divider.setter
def divider(self, value: bool) -> None:
self._underlying.divider = value
@property
def spacing(self) -> SeparatorSpacing:
""":class:`.SeparatorSpacing`: The size of the separator."""
return self._underlying.spacing
@spacing.setter
def spacing(self, value: SeparatorSpacing) -> None:
self._underlying.spacing = value
@classmethod
def from_component(cls, separator: SeparatorComponent) -> Self:
return cls(
divider=separator.divider,
spacing=separator.spacing,
id=separator.id,
)

View File

@@ -0,0 +1,58 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, Tuple
from ..components import TextDisplay as TextDisplayComponent
from ..enums import ComponentType
from ..utils import MISSING
from .item import UIComponent
if TYPE_CHECKING:
from typing_extensions import Self
__all__ = ("TextDisplay",)
class TextDisplay(UIComponent):
"""Represents a UI text display.
.. versionadded:: 2.11
Parameters
----------
content: :class:`str`
The text displayed by this component.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = ("content",)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: TextDisplayComponent = MISSING
def __init__(self, content: str, *, id: int = 0) -> None:
self._underlying = TextDisplayComponent._raw_construct(
type=ComponentType.text_display,
id=id,
content=str(content),
)
@property
def content(self) -> str:
""":class:`str`: The text displayed by this component."""
return self._underlying.content
@content.setter
def content(self, value: str) -> None:
self._underlying.content = str(value)
@classmethod
def from_component(cls, text_display: TextDisplayComponent) -> Self:
return cls(
content=text_display.content,
id=text_display.id,
)

View File

@@ -0,0 +1,191 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import os
from typing import TYPE_CHECKING, ClassVar, Optional, Tuple
from ..components import TextInput as TextInputComponent
from ..enums import ComponentType, TextInputStyle
from ..utils import MISSING, deprecated
from .item import WrappedComponent
if TYPE_CHECKING:
from typing_extensions import Self
__all__ = ("TextInput",)
class TextInput(WrappedComponent):
"""Represents a UI text input.
This can only be used in a :class:`~.ui.Modal`.
.. versionadded:: 2.4
Parameters
----------
label: Optional[:class:`str`]
The label of the text input.
.. deprecated:: 2.11
This is deprecated in favor of :attr:`Label.text <.ui.Label.text>` and
:attr:`.description <.ui.Label.description>`.
custom_id: :class:`str`
The ID of the text input that gets received during an interaction.
If not given then one is generated for you.
style: :class:`.TextInputStyle`
The style of the text input.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is entered.
value: Optional[:class:`str`]
The pre-filled value of the text input.
required: :class:`bool`
Whether the text input is required. Defaults to ``True``.
min_length: Optional[:class:`int`]
The minimum length of the text input.
max_length: Optional[:class:`int`]
The maximum length of the text input.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"style",
"custom_id",
"placeholder",
"value",
"required",
"min_length",
"max_length",
)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: TextInputComponent = MISSING
def __init__(
self,
*,
label: Optional[str] = None,
custom_id: str = MISSING,
style: TextInputStyle = TextInputStyle.short,
placeholder: Optional[str] = None,
value: Optional[str] = None,
required: bool = True,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
id: int = 0,
) -> None:
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
self._underlying = TextInputComponent._raw_construct(
type=ComponentType.text_input,
id=id,
style=style,
label=label,
custom_id=custom_id,
placeholder=placeholder,
value=value,
required=required,
min_length=min_length,
max_length=max_length,
)
@property
def width(self) -> int:
return 5
@property
def style(self) -> TextInputStyle:
""":class:`.TextInputStyle`: The style of the text input."""
return self._underlying.style
@style.setter
def style(self, value: TextInputStyle) -> None:
self._underlying.style = value
@property
@deprecated('ui.Label("<text>", ui.TextInput(...))')
def label(self) -> Optional[str]:
""":class:`str`: The label of the text input.
.. deprecated:: 2.11
This is deprecated in favor of :class:`.ui.Label`.
"""
return self._underlying.label
@label.setter
@deprecated('ui.Label("<text>", ui.TextInput(...))')
def label(self, value: str) -> None:
self._underlying.label = value
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the text input that gets received during an interaction."""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: str) -> None:
self._underlying.custom_id = value
@property
def placeholder(self) -> Optional[str]:
"""Optional[:class:`str`]: The placeholder text that is shown if nothing is entered."""
return self._underlying.placeholder
@placeholder.setter
def placeholder(self, value: Optional[str]) -> None:
self._underlying.placeholder = value
@property
def value(self) -> Optional[str]:
"""Optional[:class:`str`]: The pre-filled text of the text input."""
return self._underlying.value
@value.setter
def value(self, value: Optional[str]) -> None:
self._underlying.value = value
@property
def required(self) -> bool:
""":class:`bool`: Whether the text input is required."""
return self._underlying.required
@required.setter
def required(self, value: bool) -> None:
self._underlying.required = value
@property
def min_length(self) -> Optional[int]:
"""Optional[:class:`int`]: The minimum length of the text input."""
return self._underlying.min_length
@min_length.setter
def min_length(self, value: Optional[int]) -> None:
self._underlying.min_length = value
@property
def max_length(self) -> Optional[int]:
"""Optional[:class:`int`]: The maximum length of the text input."""
return self._underlying.max_length
@max_length.setter
def max_length(self, value: Optional[int]) -> None:
self._underlying.max_length = value
@classmethod
def from_component(cls, text_input: TextInputComponent) -> Self:
return cls(
label=text_input.label or "",
custom_id=text_input.custom_id,
style=text_input.style,
placeholder=text_input.placeholder,
value=text_input.value,
required=text_input.required,
min_length=text_input.min_length,
max_length=text_input.max_length,
id=text_input.id,
)

View File

@@ -0,0 +1,100 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, Optional, Tuple
from ..components import Thumbnail as ThumbnailComponent, UnfurledMediaItem, handle_media_item_input
from ..enums import ComponentType
from ..utils import MISSING
from .item import UIComponent
if TYPE_CHECKING:
from typing_extensions import Self
from ..components import MediaItemInput
__all__ = ("Thumbnail",)
class Thumbnail(UIComponent):
"""Represents a UI thumbnail.
This is only supported as the :attr:`~.ui.Section.accessory` of a section component.
.. versionadded:: 2.11
Parameters
----------
media: Union[:class:`str`, :class:`.Asset`, :class:`.Attachment`, :class:`.UnfurledMediaItem`]
The media item to display. Can be an arbitrary URL or attachment
reference (``attachment://<filename>``).
description: Optional[:class:`str`]
The thumbnail's description ("alt text"), if any.
spoiler: :class:`bool`
Whether the thumbnail is marked as a spoiler. Defaults to ``False``.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"media",
"description",
"spoiler",
)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: ThumbnailComponent = MISSING
def __init__(
self,
media: MediaItemInput,
description: Optional[str] = None,
*,
spoiler: bool = False,
id: int = 0,
) -> None:
self._underlying = ThumbnailComponent._raw_construct(
type=ComponentType.thumbnail,
id=id,
media=handle_media_item_input(media),
description=description,
spoiler=spoiler,
)
@property
def media(self) -> UnfurledMediaItem:
""":class:`.UnfurledMediaItem`: The media item to display."""
return self._underlying.media
@media.setter
def media(self, value: MediaItemInput) -> None:
self._underlying.media = handle_media_item_input(value)
@property
def description(self) -> Optional[str]:
"""Optional[:class:`str`]: The thumbnail's description ("alt text"), if any."""
return self._underlying.description
@description.setter
def description(self, value: Optional[str]) -> None:
self._underlying.description = value
@property
def spoiler(self) -> bool:
""":class:`bool`: Whether the thumbnail is marked as a spoiler."""
return self._underlying.spoiler
@spoiler.setter
def spoiler(self, value: bool) -> None:
self._underlying.spoiler = value
@classmethod
def from_component(cls, thumbnail: ThumbnailComponent) -> Self:
return cls(
media=thumbnail.media,
description=thumbnail.description,
spoiler=thumbnail.spoiler,
id=thumbnail.id,
)

View File

@@ -0,0 +1,565 @@
# SPDX-License-Identifier: MIT
from __future__ import annotations
import asyncio
import logging
import os
import sys
import time
import traceback
from functools import partial
from itertools import groupby
from typing import TYPE_CHECKING, Callable, ClassVar, Dict, List, Optional, Sequence, Tuple
from ..components import (
VALID_ACTION_ROW_MESSAGE_COMPONENT_TYPES,
ActionRow as ActionRowComponent,
ActionRowMessageComponent,
Button as ButtonComponent,
_component_factory,
)
from ..enums import try_enum_to_int
from .action_row import _message_component_to_item, walk_components
from .button import Button
from .item import Item
__all__ = ("View",)
if TYPE_CHECKING:
from typing_extensions import Self
from ..interactions import MessageInteraction
from ..message import Message
from ..state import ConnectionState
from ..types.components import ActionRow as ActionRowPayload, Component as ComponentPayload
from .item import ItemCallbackType
_log = logging.getLogger(__name__)
def _component_to_item(component: ActionRowMessageComponent) -> Item:
if item := _message_component_to_item(component):
return item
else:
return Item.from_component(component)
class _ViewWeights:
__slots__ = ("weights",)
def __init__(self, children: List[Item]) -> None:
self.weights: List[int] = [0, 0, 0, 0, 0]
key: Callable[[Item[View]], int] = lambda i: sys.maxsize if i.row is None else i.row
children = sorted(children, key=key)
for _, group in groupby(children, key=key):
for item in group:
self.add_item(item)
def find_open_space(self, item: Item) -> int:
for index, weight in enumerate(self.weights):
if weight + item.width <= 5:
return index
raise ValueError("could not find open space for item")
def add_item(self, item: Item) -> None:
if item.row is not None:
total = self.weights[item.row] + item.width
if total > 5:
raise ValueError(f"item would not fit at row {item.row} ({total} > 5 width)")
self.weights[item.row] = total
item._rendered_row = item.row
else:
index = self.find_open_space(item)
self.weights[index] += item.width
item._rendered_row = index
def remove_item(self, item: Item) -> None:
if item._rendered_row is not None:
self.weights[item._rendered_row] -= item.width
item._rendered_row = None
def clear(self) -> None:
self.weights = [0, 0, 0, 0, 0]
class View:
"""Represents a UI view.
This object must be inherited to create a UI within Discord.
Alternatively, components can be handled with :class:`disnake.ui.ActionRow`\\s and event
listeners for a more low-level approach. Relevant events are :func:`disnake.on_button_click`,
:func:`disnake.on_dropdown`, and the more generic :func:`disnake.on_message_interaction`.
.. versionadded:: 2.0
Parameters
----------
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
Attributes
----------
timeout: Optional[:class:`float`]
Timeout from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
children: List[:class:`Item`]
The list of children attached to this view.
"""
__discord_ui_view__: ClassVar[bool] = True
__view_children_items__: ClassVar[List[ItemCallbackType[Self, Item[Self]]]] = []
def __init_subclass__(cls) -> None:
children: List[ItemCallbackType[Self, Item[Self]]] = []
for base in reversed(cls.__mro__):
for member in base.__dict__.values():
if hasattr(member, "__discord_ui_model_type__"):
children.append(member)
if len(children) > 25:
raise TypeError("View cannot have more than 25 children")
cls.__view_children_items__ = children
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
self.timeout = timeout
self.children: List[Item[Self]] = []
for func in self.__view_children_items__:
item: Item[Self] = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = partial(func, self, item)
item._view = self
setattr(self, func.__name__, item)
self.children.append(item)
self.__weights = _ViewWeights(self.children)
loop = asyncio.get_running_loop()
self.id: str = os.urandom(16).hex()
self.__cancel_callback: Optional[Callable[[View], None]] = None
self.__timeout_expiry: Optional[float] = None
self.__timeout_task: Optional[asyncio.Task[None]] = None
self.__stopped: asyncio.Future[bool] = loop.create_future()
def __repr__(self) -> str:
return f"<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>"
async def __timeout_task_impl(self) -> None:
while True:
# Guard just in case someone changes the value of the timeout at runtime
if self.timeout is None:
return
if self.__timeout_expiry is None:
return self._dispatch_timeout()
# Check if we've elapsed our currently set timeout
now = time.monotonic()
if now >= self.__timeout_expiry:
return self._dispatch_timeout()
# Wait N seconds to see if timeout data has been refreshed
await asyncio.sleep(self.__timeout_expiry - now)
def to_components(self) -> List[ActionRowPayload]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self.children, key=key)
components: List[ActionRowPayload] = []
for _, group in groupby(children, key=key):
children = [item.to_component_dict() for item in group]
if not children:
continue
components.append(
{
"type": 1,
"id": 0,
"components": children,
}
)
return components
@classmethod
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
"""Converts a message's components into a :class:`View`.
The :attr:`.Message.components` of a message are read-only
and separate types from those in the ``disnake.ui`` namespace.
In order to modify and edit message components they must be
converted into a :class:`View` first.
Parameters
----------
message: :class:`disnake.Message`
The message with components to convert into a view.
timeout: Optional[:class:`float`]
The timeout of the converted view.
Raises
------
TypeError
Message contains v2 components, which are not supported by :class:`View`.
See also :attr:`.MessageFlags.is_components_v2`.
Returns
-------
:class:`View`
The converted view. This always returns a :class:`View` and not
one of its subclasses.
"""
view = View(timeout=timeout)
# FIXME: preserve rows
for component in walk_components(message.components):
if isinstance(component, ActionRowComponent):
continue
elif not isinstance(component, VALID_ACTION_ROW_MESSAGE_COMPONENT_TYPES):
# can happen if message uses components v2
raise TypeError(
f"Cannot construct view from message - unexpected {type(component).__name__}"
)
view.add_item(_component_to_item(component))
return view
@property
def _expires_at(self) -> Optional[float]:
if self.timeout:
return time.monotonic() + self.timeout
return None
def add_item(self, item: Item) -> Self:
"""Adds an item to the view.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to add to the view.
Raises
------
TypeError
An :class:`Item` was not passed.
ValueError
Maximum number of children has been exceeded (25)
or the row the item is trying to be added to is full.
"""
if len(self.children) > 25:
raise ValueError("maximum number of children exceeded")
if not isinstance(item, Item):
raise TypeError(f"expected Item not {item.__class__!r}")
self.__weights.add_item(item)
item._view = self
self.children.append(item)
return self
def remove_item(self, item: Item) -> Self:
"""Removes an item from the view.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to remove from the view.
"""
try:
self.children.remove(item)
except ValueError:
pass
else:
self.__weights.remove_item(item)
return self
def clear_items(self) -> Self:
"""Removes all items from the view.
This function returns the class instance to allow for fluent-style
chaining.
"""
self.children.clear()
self.__weights.clear()
return self
async def interaction_check(self, interaction: MessageInteraction) -> bool:
"""|coro|
A callback that is called when an interaction happens within the view
that checks whether the view should process item callbacks for the interaction.
This is useful to override if, for example, you want to ensure that the
interaction author is a given user.
The default implementation of this returns ``True``.
.. note::
If an exception occurs within the body then the check
is considered a failure and :meth:`on_error` is called.
Parameters
----------
interaction: :class:`.MessageInteraction`
The interaction that occurred.
Returns
-------
:class:`bool`
Whether the view children's callbacks should be called.
"""
return True
async def on_timeout(self) -> None:
"""|coro|
A callback that is called when a view's timeout elapses without being explicitly stopped.
"""
pass
async def on_error(self, error: Exception, item: Item, interaction: MessageInteraction) -> None:
"""|coro|
A callback that is called when an item's callback or :meth:`interaction_check`
fails with an error.
The default implementation prints the traceback to stderr.
Parameters
----------
error: :class:`Exception`
The exception that was raised.
item: :class:`Item`
The item that failed the dispatch.
interaction: :class:`.MessageInteraction`
The interaction that led to the failure.
"""
print(f"Ignoring exception in view {self} for item {item}:", file=sys.stderr)
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)
async def _scheduled_task(self, item: Item, interaction: MessageInteraction) -> None:
try:
if self.timeout:
self.__timeout_expiry = time.monotonic() + self.timeout
allow = await self.interaction_check(interaction)
if not allow:
return
await item.callback(interaction)
except Exception as e:
return await self.on_error(e, item, interaction)
def _start_listening_from_store(self, store: ViewStore) -> None:
self.__cancel_callback = partial(store.remove_view)
if self.timeout:
loop = asyncio.get_running_loop()
if self.__timeout_task is not None:
self.__timeout_task.cancel()
self.__timeout_expiry = time.monotonic() + self.timeout
self.__timeout_task = loop.create_task(self.__timeout_task_impl())
def _dispatch_timeout(self) -> None:
if self.__stopped.done():
return
self.__stopped.set_result(True)
asyncio.create_task(self.on_timeout(), name=f"disnake-ui-view-timeout-{self.id}")
def _dispatch_item(self, item: Item, interaction: MessageInteraction) -> None:
if self.__stopped.done():
return
asyncio.create_task(
self._scheduled_task(item, interaction), name=f"disnake-ui-view-dispatch-{self.id}"
)
def refresh(self, components: List[ActionRowComponent[ActionRowMessageComponent]]) -> None:
# TODO: this is pretty hacky at the moment, see https://github.com/DisnakeDev/disnake/commit/9384a72acb8c515b13a600592121357e165368da
old_state: Dict[Tuple[int, str], Item] = {
(item.type.value, item.custom_id): item # type: ignore
for item in self.children
if item.is_dispatchable()
}
children: List[Item] = []
for component in (c for row in components for c in row.children):
older: Optional[Item] = None
try:
older = old_state[(component.type.value, component.custom_id)] # type: ignore
except (KeyError, AttributeError):
# workaround for non-interactive buttons, since they're not part of `old_state`
if isinstance(component, ButtonComponent):
for child in self.children:
if not isinstance(child, Button):
continue
# try finding the corresponding child in this view based on other attributes
if (
(child.label and child.label == component.label)
and (child.url and child.url == component.url)
) or (child.sku_id and child.sku_id == component.sku_id):
older = child
break
if older:
older.refresh_component(component) # type: ignore # this is fine, pyright is trying to be smart
children.append(older)
else:
# fallback, should not happen as long as implementation covers all cases
children.append(_component_to_item(component))
self.children = children
def stop(self) -> None:
"""Stops listening to interaction events from this view.
This operation cannot be undone.
"""
if not self.__stopped.done():
self.__stopped.set_result(False)
self.__timeout_expiry = None
if self.__timeout_task is not None:
self.__timeout_task.cancel()
self.__timeout_task = None
if self.__cancel_callback:
self.__cancel_callback(self)
self.__cancel_callback = None
def is_finished(self) -> bool:
"""Whether the view has finished interacting.
:return type: :class:`bool`
"""
return self.__stopped.done()
def is_dispatching(self) -> bool:
"""Whether the view has been added for dispatching purposes.
:return type: :class:`bool`
"""
return self.__cancel_callback is not None
def is_persistent(self) -> bool:
"""Whether the view is set up as persistent.
A persistent view only has components with a set ``custom_id``
(or non-interactive components such as :attr:`~.ButtonStyle.link` or :attr:`~.ButtonStyle.premium` buttons),
and a :attr:`timeout` set to ``None``.
:return type: :class:`bool`
"""
return self.timeout is None and all(item.is_persistent() for item in self.children)
async def wait(self) -> bool:
"""Waits until the view has finished interacting.
A view is considered finished when :meth:`stop` is called
or it times out.
Returns
-------
:class:`bool`
If ``True``, then the view timed out. If ``False`` then
the view finished normally.
"""
return await self.__stopped
class ViewStore:
def __init__(self, state: ConnectionState) -> None:
# (component_type, message_id, custom_id): (View, Item)
self._views: Dict[Tuple[int, Optional[int], str], Tuple[View, Item]] = {}
# message_id: View
self._synced_message_views: Dict[int, View] = {}
self._state: ConnectionState = state
@property
def persistent_views(self) -> Sequence[View]:
views = {view.id: view for view, _ in self._views.values() if view.is_persistent()}
return list(views.values())
def __verify_integrity(self) -> None:
to_remove: List[Tuple[int, Optional[int], str]] = []
for k, (view, _) in self._views.items():
if view.is_finished():
to_remove.append(k)
for k in to_remove:
del self._views[k]
def add_view(self, view: View, message_id: Optional[int] = None) -> None:
self.__verify_integrity()
view._start_listening_from_store(self)
for item in view.children:
if item.is_dispatchable():
self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore
if message_id is not None:
self._synced_message_views[message_id] = view
def remove_view(self, view: View) -> None:
for item in view.children:
if item.is_dispatchable():
self._views.pop((item.type.value, item.custom_id), None) # type: ignore
for key, value in self._synced_message_views.items():
if value.id == view.id:
del self._synced_message_views[key]
break
def dispatch(self, interaction: MessageInteraction) -> None:
self.__verify_integrity()
message_id: Optional[int] = interaction.message and interaction.message.id
component_type = try_enum_to_int(interaction.data.component_type)
custom_id = interaction.data.custom_id
key = (component_type, message_id, custom_id)
# Fallback to None message_id searches in case a persistent view
# was added without an associated message_id
value = self._views.get(key) or self._views.get((component_type, None, custom_id))
if value is None:
return
view, item = value
item.refresh_state(interaction)
view._dispatch_item(item, interaction)
def is_message_tracked(self, message_id: int) -> bool:
return message_id in self._synced_message_views
def remove_message_tracking(self, message_id: int) -> Optional[View]:
return self._synced_message_views.pop(message_id, None)
def update_from_message(self, message_id: int, components: Sequence[ComponentPayload]) -> None:
# pre-req: is_message_tracked == true
view = self._synced_message_views[message_id]
rows = [
_component_factory(d, type=ActionRowComponent[ActionRowMessageComponent])
for d in components
]
for row in rows:
if not isinstance(row, ActionRowComponent):
_log.warning(
"cannot update view for message %d, unexpected %s",
message_id,
type(row).__name__,
)
return
view.refresh(rows)