diff --git a/changelog/1234.feature.rst b/changelog/1234.feature.rst new file mode 100644 index 0000000000..08a2221e69 --- /dev/null +++ b/changelog/1234.feature.rst @@ -0,0 +1 @@ +Adding :method:`Embed.add_some_fields` and :metaclass:`ModalMeta` for :class:`Modal` diff --git a/disnake/embeds.py b/disnake/embeds.py index 1866d8d7eb..35ff6e3e52 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -675,6 +675,43 @@ def insert_field_at(self, index: int, name: Any, value: Any, *, inline: bool = T return self + def add_some_fields(self, *data: Dict[str, Any]) -> Self: + """Function allows you to create several fields at once + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + data: :class:`dict` + field data in dictionary + + Example: + add_some_fields( + {"name": "Jack", "value": "Barker", "inline": False} + {"name": "Sandra", "value": "Himenez", "inline": False} + ) + """ + fields: List[EmbedFieldPayload] = [] + for element in data: + if (element.get("name") is None) or (element.get("value") is None): + raise TypeError("Missing argument. Name and Value - required.") + + fields.append( + { + "inline": bool(element.get("inline")), + "name": str(element.get("name")), + "value": str(element.get("value")), + } + ) + + if self._fields is not None: + self._fields.extend(fields) + else: + self._fields = fields + + return self + def clear_fields(self) -> None: """Removes all fields from this embed.""" self._fields = None diff --git a/disnake/ui/modal.py b/disnake/ui/modal.py index adf21ffa9c..f49c3067cb 100644 --- a/disnake/ui/modal.py +++ b/disnake/ui/modal.py @@ -6,10 +6,9 @@ import os import sys import traceback -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, TypeVar, Union from ..enums import TextInputStyle -from ..utils import MISSING from .action_row import ActionRow, components_to_rows from .text_input import TextInput @@ -23,49 +22,91 @@ __all__ = ("Modal",) + ClientT = TypeVar("ClientT", bound="Client") -class Modal: - """Represents a UI Modal. +class ModalMeta(type): + """A metaclass for defining a modal""" + + def __new__(cls: Type[ModalMeta], *args: Any, **kwargs: Any) -> ModalMeta: + name, bases, attrs = args + if not bases: + return super().__new__(cls, name, bases, attrs) + + components: Components[ModalUIComponent] = [] + for value in attrs.values(): + if isinstance(value, TextInput): + components.append(value) + + if not components: + raise TypeError(f"No text inputs found for class {name}") + + rows: List[ActionRow] = components_to_rows(components) + if len(rows) > 5: + raise ValueError("Maximum number of components exceeded. Max components - 5") + + attrs.update({"components": rows}) + return super().__new__(cls, name, bases, attrs) + - .. versionadded:: 2.4 +class Modal(metaclass=ModalMeta): + """Represents a UI Modal. Parameters ---------- - title: :class:`str` + __title__: :class:`str` The title of the modal. - components: |components_type| - The components to display in the modal. Up to 5 action rows. - custom_id: :class:`str` + __custom_id__: :class:`str` The custom ID of the modal. - timeout: :class:`float` + __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. + + Example: + class MyModal(disnake.ui.Modal): + __title__ = "Register" + __custom_id__ = "register-modal" + __timeout__ = 100 + + username = TextInput( + label="Username", + custom_id="username" + ) + email = TextInput( + label="Email", + custom_id="email" + ) + age = TextInput( + label="Age", + custom_id="age", + required=False + ) """ + __title__: str + __custom_id__: str + __timeout__: float + __slots__ = ("title", "custom_id", "components", "timeout") - def __init__( - self, - *, - title: str, - components: Components[ModalUIComponent], - custom_id: str = MISSING, - timeout: float = 600, - ) -> None: - if timeout is None: # pyright: ignore[reportUnnecessaryComparison] - raise ValueError("Timeout may not be None") + def __init__(self) -> None: + modal_dict = self.__class__.__dict__ - rows = components_to_rows(components) - if len(rows) > 5: - raise ValueError("Maximum number of components exceeded.") + self.title: str = modal_dict.get("__title__", str) + self.custom_id: str = modal_dict.get("__custom_id__", str) + self.timeout: float = modal_dict.get("__timeout__", float) + self.components: List[ActionRow] = modal_dict.get("components", List[ActionRow]) - self.title: str = title - self.custom_id: str = os.urandom(16).hex() if custom_id is MISSING else custom_id - self.components: List[ActionRow] = rows - self.timeout: float = timeout + if not self.title: + raise TypeError("Missing required argument __title__") + + if not self.custom_id: + self.custom_id = os.urandom(16).hex() + + if not self.timeout: + self.timeout = 600 def __repr__(self) -> str: return ( @@ -203,7 +244,6 @@ def to_components(self) -> ModalPayload: "custom_id": self.custom_id, "components": [component.to_component_dict() for component in self.components], } - return payload async def _scheduled_task(self, interaction: ModalInteraction) -> None: diff --git a/disnake/webhook/async_.py b/disnake/webhook/async_.py index 98650f4bf1..70f69a0536 100644 --- a/disnake/webhook/async_.py +++ b/disnake/webhook/async_.py @@ -427,7 +427,6 @@ def create_interaction_response( if files: set_attachments(data, files) payload["data"] = data - if files: multipart = to_multipart(payload, files) return self.request(route, session=session, multipart=multipart, files=files) diff --git a/examples/interactions/modal.py b/examples/interactions/modal.py index f271c82f4c..37ee23c63b 100644 --- a/examples/interactions/modal.py +++ b/examples/interactions/modal.py @@ -24,26 +24,25 @@ class MyModal(disnake.ui.Modal): - def __init__(self) -> None: - components = [ - disnake.ui.TextInput( - label="Name", - placeholder="The name of the tag", - custom_id="name", - style=disnake.TextInputStyle.short, - min_length=5, - max_length=50, - ), - disnake.ui.TextInput( - label="Content", - placeholder="The content of the tag", - custom_id="content", - style=disnake.TextInputStyle.paragraph, - min_length=5, - max_length=1024, - ), - ] - super().__init__(title="Create Tag", custom_id="create_tag", components=components) + __title__ = "Create Tag" + __custom_id__ = "create_tag" + + name = disnake.ui.TextInput( + label="Name", + placeholder="The name of the tag", + custom_id="name", + style=disnake.TextInputStyle.short, + min_length=5, + max_length=50, + ) + content = disnake.ui.TextInput( + label="Content", + placeholder="The content of the tag", + custom_id="content", + style=disnake.TextInputStyle.paragraph, + min_length=5, + max_length=1024, + ) async def callback(self, inter: disnake.ModalInteraction) -> None: tag_name = inter.text_values["name"] diff --git a/test_bot/cogs/modals.py b/test_bot/cogs/modals.py index c5d514a25c..68106380a8 100644 --- a/test_bot/cogs/modals.py +++ b/test_bot/cogs/modals.py @@ -6,23 +6,23 @@ class MyModal(disnake.ui.Modal): - def __init__(self) -> None: - components = [ - disnake.ui.TextInput( - label="Name", - placeholder="The name of the tag", - custom_id="name", - style=TextInputStyle.short, - max_length=50, - ), - disnake.ui.TextInput( - label="Description", - placeholder="The description of the tag", - custom_id="description", - style=TextInputStyle.paragraph, - ), - ] - super().__init__(title="Create Tag", custom_id="create_tag", components=components) + __title__ = "Create Tag" + __custom_id__ = "create_tag" + + name = disnake.ui.TextInput( + label="Name", + placeholder="The name of the tag", + custom_id="name", + style=TextInputStyle.short, + max_length=50, + ) + + description = disnake.ui.TextInput( + label="Description", + placeholder="The description of the tag", + custom_id="description", + style=TextInputStyle.paragraph, + ) async def callback(self, inter: disnake.ModalInteraction[commands.Bot]) -> None: embed = disnake.Embed(title="Tag Creation")