Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(modal, embeds): new method for Embed and metaclass for class Modal #1254

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1234.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adding :method:`Embed.add_some_fields` and :metaclass:`ModalMeta` for :class:`Modal`
37 changes: 37 additions & 0 deletions disnake/embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
96 changes: 68 additions & 28 deletions disnake/ui/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 (
Expand Down Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion disnake/webhook/async_.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 19 additions & 20 deletions examples/interactions/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
34 changes: 17 additions & 17 deletions test_bot/cogs/modals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading