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
Changes from 1 commit
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
Next Next commit
feat(modal, embeds): new method for Embed and metaclass for class Modal
shayzi3 committed Dec 10, 2024
commit bf3d823bde1abf4944e00286a064f64eb1aa122a
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`
1 change: 0 additions & 1 deletion disnake/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: MIT

from __future__ import annotations

import asyncio
import logging
import signal
41 changes: 40 additions & 1 deletion disnake/embeds.py
Original file line number Diff line number Diff line change
@@ -674,6 +674,45 @@
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."""
@@ -720,7 +759,7 @@
inline: :class:`bool`
Whether the field should be displayed inline.
Defaults to ``True``.

Check failure on line 762 in disnake/embeds.py

GitHub Actions / pyright (3.8, false)

Argument of type "dict[str, Any | str | None]" cannot be assigned to parameter "__object" of type "EmbedField" in function "append"   Type "Any | None" cannot be assigned to type "bool"     "None" is incompatible with "bool" (reportGeneralTypeIssues)
Raises
------
IndexError
@@ -892,4 +931,4 @@
)

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")
106 changes: 74 additions & 32 deletions disnake/ui/modal.py
Original file line number Diff line number Diff line change
@@ -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"<Modal custom_id={self.custom_id!r} title={self.title!r} "
@@ -89,7 +131,7 @@
TypeError
An object of type :class:`TextInput` was not passed.
"""
if len(self.components) >= 5:

Check failure on line 134 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

Argument of type "List[ActionRow[Unknown]] | None" cannot be assigned to parameter "__obj" of type "Sized" in function "len"   Type "List[ActionRow[Unknown]] | None" cannot be assigned to type "Sized"     "None" is incompatible with protocol "Sized"       "__len__" is not present (reportGeneralTypeIssues)
raise ValueError("Maximum number of components exceeded.")

if not isinstance(component, list):
@@ -101,9 +143,9 @@
f"component must be of type 'TextInput' or a list of 'TextInput' objects, not {type(c).__name__}."
)
try:
self.components[-1].append_item(c)

Check failure on line 146 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

Object of type "None" is not subscriptable (reportOptionalSubscript)
except (ValueError, IndexError):
self.components.append(ActionRow(c))

Check failure on line 148 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

"append" is not a known member of "None" (reportOptionalMemberAccess)

def add_text_input(
self,
@@ -196,14 +238,14 @@
without an interaction being made.
"""
pass


def to_components(self) -> ModalPayload:
payload: ModalPayload = {

Check failure on line 244 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

Expression of type "dict[str, str | list[ActionRow] | None]" cannot be assigned to declared type "Modal"   Type "str | None" cannot be assigned to type "str"     "None" is incompatible with "str"   Type "str | None" cannot be assigned to type "str"     "None" is incompatible with "str" (reportGeneralTypeIssues)
"title": self.title,
"custom_id": self.custom_id,
"components": [component.to_component_dict() for component in self.components],

Check failure on line 247 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

Object of type "None" cannot be used as iterable value (reportOptionalIterable)
}

return payload

async def _scheduled_task(self, interaction: ModalInteraction) -> None:
@@ -233,8 +275,8 @@

def add_modal(self, user_id: int, modal: Modal) -> None:
loop = asyncio.get_running_loop()
self._modals[(user_id, modal.custom_id)] = modal

Check failure on line 278 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

Argument of type "tuple[int, str | None]" cannot be assigned to parameter "__key" of type "Tuple[int, str]" in function "__setitem__"   "tuple[int, str | None]" is incompatible with "Tuple[int, str]"     Tuple entry 2 is incorrect type       Type "str | None" cannot be assigned to type "str"         "None" is incompatible with "str" (reportGeneralTypeIssues)
loop.create_task(self.handle_timeout(user_id, modal.custom_id, modal.timeout))

Check failure on line 279 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

Argument of type "str | None" cannot be assigned to parameter "modal_custom_id" of type "str" in function "handle_timeout"   Type "str | None" cannot be assigned to type "str"     "None" is incompatible with "str" (reportGeneralTypeIssues)

Check failure on line 279 in disnake/ui/modal.py

GitHub Actions / pyright (3.8, false)

Argument of type "float | None" cannot be assigned to parameter "timeout" of type "float" in function "handle_timeout"   Type "float | None" cannot be assigned to type "float"     "None" is incompatible with "float" (reportGeneralTypeIssues)

def remove_modal(self, user_id: int, modal_custom_id: str) -> Modal:
return self._modals.pop((user_id, modal_custom_id))
1 change: 0 additions & 1 deletion disnake/webhook/async_.py
Original file line number Diff line number Diff line change
@@ -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)
39 changes: 19 additions & 20 deletions examples/interactions/modal.py
Original file line number Diff line number Diff line change
@@ -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"]
34 changes: 17 additions & 17 deletions test_bot/cogs/modals.py
Original file line number Diff line number Diff line change
@@ -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")
Loading