From bf3d823bde1abf4944e00286a064f64eb1aa122a Mon Sep 17 00:00:00 2001 From: Vlad_shayzi1 Date: Tue, 10 Dec 2024 09:43:33 +0300 Subject: [PATCH 1/2] feat(modal, embeds): new method for Embed and metaclass for class Modal --- changelog/1234.feature.rst | 1 + disnake/client.py | 1 - disnake/embeds.py | 41 ++++++++++++- disnake/ui/modal.py | 106 +++++++++++++++++++++++---------- disnake/webhook/async_.py | 1 - examples/interactions/modal.py | 39 ++++++------ test_bot/cogs/modals.py | 34 +++++------ 7 files changed, 151 insertions(+), 72 deletions(-) create mode 100644 changelog/1234.feature.rst 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/client.py b/disnake/client.py index 80b3d67c65..f7c4859000 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: MIT from __future__ import annotations - import asyncio import logging import signal diff --git a/disnake/embeds.py b/disnake/embeds.py index 1866d8d7eb..e284a8cf95 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -674,6 +674,45 @@ def insert_field_at(self, index: int, name: Any, value: Any, *, inline: bool = T self._fields = [field] return self + + + def add_some_fields(self, *data: Dict[str, Any]) -> Self: + """This 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": 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.""" @@ -892,4 +931,4 @@ def check_limits(self) -> None: ) if len(self) > 6000: - raise ValueError("Embed total size cannot be longer than 6000 characters") + raise ValueError("Embed total size cannot be longer than 6000 characters") \ No newline at end of file diff --git a/disnake/ui/modal.py b/disnake/ui/modal.py index adf21ffa9c..41192e235d 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, TypeVar, Union, Type from ..enums import TextInputStyle -from ..utils import MISSING from .action_row import ActionRow, components_to_rows from .text_input import TextInput @@ -23,50 +22,93 @@ __all__ = ("Modal",) + ClientT = TypeVar("ClientT", bound="Client") -class Modal: - """Represents a UI Modal. - .. versionadded:: 2.4 +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) + + + +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") - - rows = components_to_rows(components) - if len(rows) > 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[ActionRow] = rows - self.timeout: float = timeout - + def __init__(self) -> None: + modal_dict = self.__class__.__dict__ + + self.title: Union[str, None] = modal_dict.get("__title__") + self.custom_id: Union[str, None] = modal_dict.get("__custom_id__") + self.timeout: Union[float, None] = modal_dict.get("__timeout__") + self.components: Union[List[ActionRow], None] = modal_dict.get("components") + + if self.title is None: + raise TypeError("Missing required argument __title__") + + if self.custom_id is None: + self.custom_id = os.urandom(16).hex() + + if self.timeout is None: + self.timeout = 600 + def __repr__(self) -> str: return ( f" None: without an interaction being made. """ pass + def to_components(self) -> ModalPayload: payload: ModalPayload = { @@ -203,7 +246,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..cea4b630d4 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..36d605829a 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") From ba2eed8023eb0b64d7447ea678c6aa8c62672c61 Mon Sep 17 00:00:00 2001 From: Vlad_shayzi1 Date: Tue, 10 Dec 2024 11:35:57 +0300 Subject: [PATCH 2/2] style: fix annotations --- disnake/client.py | 1 + disnake/embeds.py | 22 ++++++++-------- disnake/ui/modal.py | 46 ++++++++++++++++------------------ examples/interactions/modal.py | 2 +- test_bot/cogs/modals.py | 4 +-- 5 files changed, 36 insertions(+), 39 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index f7c4859000..80b3d67c65 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MIT from __future__ import annotations + import asyncio import logging import signal diff --git a/disnake/embeds.py b/disnake/embeds.py index e284a8cf95..35ff6e3e52 100644 --- a/disnake/embeds.py +++ b/disnake/embeds.py @@ -674,19 +674,18 @@ def insert_field_at(self, index: int, name: Any, value: Any, *, inline: bool = T self._fields = [field] return self - - + def add_some_fields(self, *data: Dict[str, Any]) -> Self: - """This function allows you to create several fields at once + """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` + data: :class:`dict` field data in dictionary - + Example: add_some_fields( {"name": "Jack", "value": "Barker", "inline": False} @@ -697,22 +696,21 @@ def add_some_fields(self, *data: Dict[str, Any]) -> Self: 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": element.get("inline"), + "inline": bool(element.get("inline")), "name": str(element.get("name")), - "value": str(element.get("value")) + "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.""" @@ -931,4 +929,4 @@ def check_limits(self) -> None: ) if len(self) > 6000: - raise ValueError("Embed total size cannot be longer than 6000 characters") \ No newline at end of file + raise ValueError("Embed total size cannot be longer than 6000 characters") diff --git a/disnake/ui/modal.py b/disnake/ui/modal.py index 41192e235d..f49c3067cb 100644 --- a/disnake/ui/modal.py +++ b/disnake/ui/modal.py @@ -6,7 +6,7 @@ import os import sys import traceback -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar, Union, Type +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, TypeVar, Union from ..enums import TextInputStyle from .action_row import ActionRow, components_to_rows @@ -26,32 +26,30 @@ ClientT = TypeVar("ClientT", bound="Client") - 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) - class Modal(metaclass=ModalMeta): """Represents a UI Modal. @@ -65,13 +63,13 @@ class Modal(metaclass=ModalMeta): 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" @@ -86,29 +84,30 @@ class MyModal(disnake.ui.Modal): required=False ) """ + __title__: str __custom_id__: str __timeout__: float - + __slots__ = ("title", "custom_id", "components", "timeout") def __init__(self) -> None: modal_dict = self.__class__.__dict__ - - self.title: Union[str, None] = modal_dict.get("__title__") - self.custom_id: Union[str, None] = modal_dict.get("__custom_id__") - self.timeout: Union[float, None] = modal_dict.get("__timeout__") - self.components: Union[List[ActionRow], None] = modal_dict.get("components") - - if self.title is None: + + 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]) + + if not self.title: raise TypeError("Missing required argument __title__") - - if self.custom_id is None: + + if not self.custom_id: self.custom_id = os.urandom(16).hex() - - if self.timeout is None: + + if not self.timeout: self.timeout = 600 - + def __repr__(self) -> str: return ( f" None: without an interaction being made. """ pass - def to_components(self) -> ModalPayload: payload: ModalPayload = { diff --git a/examples/interactions/modal.py b/examples/interactions/modal.py index cea4b630d4..37ee23c63b 100644 --- a/examples/interactions/modal.py +++ b/examples/interactions/modal.py @@ -26,7 +26,7 @@ class MyModal(disnake.ui.Modal): __title__ = "Create Tag" __custom_id__ = "create_tag" - + name = disnake.ui.TextInput( label="Name", placeholder="The name of the tag", diff --git a/test_bot/cogs/modals.py b/test_bot/cogs/modals.py index 36d605829a..68106380a8 100644 --- a/test_bot/cogs/modals.py +++ b/test_bot/cogs/modals.py @@ -8,7 +8,7 @@ class MyModal(disnake.ui.Modal): __title__ = "Create Tag" __custom_id__ = "create_tag" - + name = disnake.ui.TextInput( label="Name", placeholder="The name of the tag", @@ -16,7 +16,7 @@ class MyModal(disnake.ui.Modal): style=TextInputStyle.short, max_length=50, ) - + description = disnake.ui.TextInput( label="Description", placeholder="The description of the tag",