8 changed files with 393 additions and 4 deletions
@ -0,0 +1,205 @@ |
|||
""" |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) 2015-present Rapptz |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a |
|||
copy of this software and associated documentation files (the "Software"), |
|||
to deal in the Software without restriction, including without limitation |
|||
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
|||
and/or sell copies of the Software, and to permit persons to whom the |
|||
Software is furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
|||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|||
DEALINGS IN THE SOFTWARE. |
|||
""" |
|||
|
|||
from __future__ import annotations |
|||
from typing import ClassVar, Dict, Generic, Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Any, Union |
|||
import re |
|||
|
|||
from .item import Item |
|||
from .._types import ClientT |
|||
|
|||
__all__ = ('DynamicItem',) |
|||
|
|||
BaseT = TypeVar('BaseT', bound='Item[Any]', covariant=True) |
|||
|
|||
if TYPE_CHECKING: |
|||
from typing_extensions import TypeVar, Self |
|||
from ..interactions import Interaction |
|||
from ..components import Component |
|||
from ..enums import ComponentType |
|||
from .view import View |
|||
|
|||
V = TypeVar('V', bound='View', covariant=True, default=View) |
|||
else: |
|||
V = TypeVar('V', bound='View', covariant=True) |
|||
|
|||
|
|||
class DynamicItem(Generic[BaseT], Item['View']): |
|||
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within |
|||
that ``custom_id``. |
|||
|
|||
The ``custom_id`` parsing is done using the ``re`` module by passing a ``template`` |
|||
parameter to the class parameter list. |
|||
|
|||
This item is generated every time the component is dispatched. This means that |
|||
any variable that holds an instance of this class will eventually be out of date |
|||
and should not be used long term. Their only purpose is to act as a "template" |
|||
for the actual dispatched item. |
|||
|
|||
When this item is generated, :attr:`view` is set to a regular :class:`View` instance |
|||
from the original message given from the interaction. This means that custom view |
|||
subclasses cannot be accessed from this item. |
|||
|
|||
.. versionadded:: 2.4 |
|||
|
|||
Parameters |
|||
------------ |
|||
item: :class:`Item` |
|||
The item to wrap with dynamic custom ID parsing. |
|||
template: Union[:class:`str`, ``re.Pattern``] |
|||
The template to use for parsing the ``custom_id``. This can be a string or a compiled |
|||
regular expression. This must be passed as a keyword argument to the class creation. |
|||
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). |
|||
|
|||
Attributes |
|||
----------- |
|||
item: :class:`Item` |
|||
The item that is wrapped with dynamic custom ID parsing. |
|||
""" |
|||
|
|||
__item_repr_attributes__: Tuple[str, ...] = ( |
|||
'item', |
|||
'template', |
|||
) |
|||
|
|||
__discord_ui_compiled_template__: ClassVar[re.Pattern[str]] |
|||
|
|||
def __init_subclass__(cls, *, template: Union[str, re.Pattern[str]]) -> None: |
|||
super().__init_subclass__() |
|||
cls.__discord_ui_compiled_template__ = re.compile(template) if isinstance(template, str) else template |
|||
if not isinstance(cls.__discord_ui_compiled_template__, re.Pattern): |
|||
raise TypeError('template must be a str or a re.Pattern') |
|||
|
|||
def __init__( |
|||
self, |
|||
item: BaseT, |
|||
*, |
|||
row: Optional[int] = None, |
|||
) -> None: |
|||
super().__init__() |
|||
self.item: BaseT = item |
|||
self.row = row |
|||
|
|||
if not self.item.is_dispatchable(): |
|||
raise TypeError('item must be dispatchable, e.g. not a URL button') |
|||
|
|||
if not self.template.match(self.custom_id): |
|||
raise ValueError(f'item custom_id must match the template {self.template.pattern!r}') |
|||
|
|||
@property |
|||
def template(self) -> re.Pattern[str]: |
|||
"""``re.Pattern``: The compiled regular expression that is used to parse the ``custom_id``.""" |
|||
return self.__class__.__discord_ui_compiled_template__ |
|||
|
|||
def to_component_dict(self) -> Dict[str, Any]: |
|||
return self.item.to_component_dict() |
|||
|
|||
def _refresh_component(self, component: Component) -> None: |
|||
self.item._refresh_component(component) |
|||
|
|||
def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None: |
|||
self.item._refresh_state(interaction, data) |
|||
|
|||
@classmethod |
|||
def from_component(cls: Type[Self], component: Component) -> Self: |
|||
raise TypeError('Dynamic items cannot be created from components') |
|||
|
|||
@property |
|||
def type(self) -> ComponentType: |
|||
return self.item.type |
|||
|
|||
def is_dispatchable(self) -> bool: |
|||
return self.item.is_dispatchable() |
|||
|
|||
def is_persistent(self) -> bool: |
|||
return True |
|||
|
|||
@property |
|||
def custom_id(self) -> str: |
|||
""":class:`str`: The ID of the dynamic item that gets received during an interaction.""" |
|||
return self.item.custom_id # type: ignore # This attribute exists for dispatchable items |
|||
|
|||
@custom_id.setter |
|||
def custom_id(self, value: str) -> None: |
|||
if not isinstance(value, str): |
|||
raise TypeError('custom_id must be a str') |
|||
|
|||
if not self.template.match(value): |
|||
raise ValueError(f'custom_id must match the template {self.template.pattern!r}') |
|||
|
|||
self.item.custom_id = value # type: ignore # This attribute exists for dispatchable items |
|||
self._provided_custom_id = True |
|||
|
|||
@property |
|||
def row(self) -> Optional[int]: |
|||
return self.item._row |
|||
|
|||
@row.setter |
|||
def row(self, value: Optional[int]) -> None: |
|||
self.item.row = value |
|||
|
|||
@property |
|||
def width(self) -> int: |
|||
return self.item.width |
|||
|
|||
@classmethod |
|||
async def from_custom_id(cls: Type[Self], interaction: Interaction[ClientT], match: re.Match[str], /) -> Self: |
|||
"""|coro| |
|||
|
|||
A classmethod that is called when the ``custom_id`` of a component matches the |
|||
``template`` of the class. This is called when the component is dispatched. |
|||
|
|||
It must return a new instance of the :class:`DynamicItem`. |
|||
|
|||
Subclasses *must* implement this method. |
|||
|
|||
Exceptions raised in this method are logged and ignored. |
|||
|
|||
.. warning:: |
|||
|
|||
This method is called before the callback is dispatched, therefore |
|||
it means that it is subject to the same timing restrictions as the callback. |
|||
Ergo, you must reply to an interaction within 3 seconds of it being |
|||
dispatched. |
|||
|
|||
Parameters |
|||
------------ |
|||
interaction: :class:`~discord.Interaction` |
|||
The interaction that the component belongs to. |
|||
match: ``re.Match`` |
|||
The match object that was created from the ``template`` |
|||
matching the ``custom_id``. |
|||
|
|||
Returns |
|||
-------- |
|||
:class:`DynamicItem` |
|||
The new instance of the :class:`DynamicItem` with information |
|||
from the ``match`` object. |
|||
""" |
|||
raise NotImplementedError |
Loading…
Reference in new issue