Adding all files
This commit is contained in:
27
.local/lib/python3.14/site-packages/disnake/ui/__init__.py
Normal file
27
.local/lib/python3.14/site-packages/disnake/ui/__init__.py
Normal 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 *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
93
.local/lib/python3.14/site-packages/disnake/ui/_types.py
Normal file
93
.local/lib/python3.14/site-packages/disnake/ui/_types.py
Normal 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_,
|
||||
]
|
||||
1194
.local/lib/python3.14/site-packages/disnake/ui/action_row.py
Normal file
1194
.local/lib/python3.14/site-packages/disnake/ui/action_row.py
Normal file
File diff suppressed because it is too large
Load Diff
364
.local/lib/python3.14/site-packages/disnake/ui/button.py
Normal file
364
.local/lib/python3.14/site-packages/disnake/ui/button.py
Normal 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
|
||||
138
.local/lib/python3.14/site-packages/disnake/ui/container.py
Normal file
138
.local/lib/python3.14/site-packages/disnake/ui/container.py
Normal 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,
|
||||
)
|
||||
121
.local/lib/python3.14/site-packages/disnake/ui/file.py
Normal file
121
.local/lib/python3.14/site-packages/disnake/ui/file.py
Normal 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
|
||||
237
.local/lib/python3.14/site-packages/disnake/ui/item.py
Normal file
237
.local/lib/python3.14/site-packages/disnake/ui/item.py
Normal 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: ...
|
||||
106
.local/lib/python3.14/site-packages/disnake/ui/label.py
Normal file
106
.local/lib/python3.14/site-packages/disnake/ui/label.py
Normal 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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
339
.local/lib/python3.14/site-packages/disnake/ui/modal.py
Normal file
339
.local/lib/python3.14/site-packages/disnake/ui/modal.py
Normal 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)
|
||||
101
.local/lib/python3.14/site-packages/disnake/ui/section.py
Normal file
101
.local/lib/python3.14/site-packages/disnake/ui/section.py
Normal 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,
|
||||
)
|
||||
@@ -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__)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
278
.local/lib/python3.14/site-packages/disnake/ui/select/base.py
Normal file
278
.local/lib/python3.14/site-packages/disnake/ui/select/base.py
Normal 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
|
||||
288
.local/lib/python3.14/site-packages/disnake/ui/select/channel.py
Normal file
288
.local/lib/python3.14/site-packages/disnake/ui/select/channel.py
Normal 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)
|
||||
@@ -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)
|
||||
244
.local/lib/python3.14/site-packages/disnake/ui/select/role.py
Normal file
244
.local/lib/python3.14/site-packages/disnake/ui/select/role.py
Normal 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)
|
||||
358
.local/lib/python3.14/site-packages/disnake/ui/select/string.py
Normal file
358
.local/lib/python3.14/site-packages/disnake/ui/select/string.py
Normal 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
|
||||
246
.local/lib/python3.14/site-packages/disnake/ui/select/user.py
Normal file
246
.local/lib/python3.14/site-packages/disnake/ui/select/user.py
Normal 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)
|
||||
82
.local/lib/python3.14/site-packages/disnake/ui/separator.py
Normal file
82
.local/lib/python3.14/site-packages/disnake/ui/separator.py
Normal 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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
191
.local/lib/python3.14/site-packages/disnake/ui/text_input.py
Normal file
191
.local/lib/python3.14/site-packages/disnake/ui/text_input.py
Normal 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,
|
||||
)
|
||||
100
.local/lib/python3.14/site-packages/disnake/ui/thumbnail.py
Normal file
100
.local/lib/python3.14/site-packages/disnake/ui/thumbnail.py
Normal 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,
|
||||
)
|
||||
565
.local/lib/python3.14/site-packages/disnake/ui/view.py
Normal file
565
.local/lib/python3.14/site-packages/disnake/ui/view.py
Normal 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)
|
||||
Reference in New Issue
Block a user