diff --git a/CHANGELOG.md b/CHANGELOG.md index 337a1bf865..a9a1deff6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ These changes are available on the `master` branch, but have not yet been releas - Support for **Python 3.14**. ([#2948](https://github.com/Pycord-Development/pycord/pull/2948)) +- Added `ComponentLimits` and `EmbedLimits` enums for Discord API constraints. + ([#3217](https://github.com/Pycord-Development/pycord/pull/3217)) ### Changed diff --git a/discord/components.py b/discord/components.py index d6e1eb89a8..6c0603862e 100644 --- a/discord/components.py +++ b/discord/components.py @@ -32,6 +32,7 @@ from .enums import ( ButtonStyle, ChannelType, + ComponentLimits, ComponentType, InputTextStyle, SelectDefaultValueType, @@ -724,14 +725,26 @@ def __init__( emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, default: bool = False, ) -> None: - if len(label) > 100: - raise ValueError("label must be 100 characters or fewer") + if len(label) > ComponentLimits.select_option_label_max.value: + raise ValueError( + f"label must be {ComponentLimits.select_option_label_max.value} characters or fewer" + ) - if value is not MISSING and len(value) > 100: - raise ValueError("value must be 100 characters or fewer") + if ( + value is not MISSING + and len(value) > ComponentLimits.select_option_value_max.value + ): + raise ValueError( + f"value must be {ComponentLimits.select_option_value_max.value} characters or fewer" + ) - if description is not None and len(description) > 100: - raise ValueError("description must be 100 characters or fewer") + if ( + description is not None + and len(description) > ComponentLimits.select_option_description_max.value + ): + raise ValueError( + f"description must be {ComponentLimits.select_option_description_max.value} characters or fewer" + ) self.label = label self.value = label if value is MISSING else value @@ -990,7 +1003,7 @@ class Thumbnail(Component): media: :class:`UnfurledMediaItem` The component's underlying media object. description: Optional[:class:`str`] - The thumbnail's description, up to 1024 characters. + The thumbnail's description, up to 256 characters. spoiler: Optional[:class:`bool`] Whether the thumbnail has the spoiler overlay. """ @@ -1039,7 +1052,7 @@ class MediaGalleryItem: url: :class:`str` The URL of this gallery item. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files. description: Optional[:class:`str`] - The gallery item's description, up to 1024 characters. + The gallery item's description, up to 256 characters. spoiler: Optional[:class:`bool`] Whether the gallery item is a spoiler. """ @@ -1527,14 +1540,26 @@ def __init__( description: str | None = None, default: bool = False, ) -> None: - if len(label) > 100: - raise ValueError("label must be 100 characters or fewer") + if len(label) > ComponentLimits.select_option_label_max.value: + raise ValueError( + f"label must be {ComponentLimits.select_option_label_max.value} characters or fewer" + ) - if value is not MISSING and len(value) > 100: - raise ValueError("value must be 100 characters or fewer") + if ( + value is not MISSING + and len(value) > ComponentLimits.select_option_value_max.value + ): + raise ValueError( + f"value must be {ComponentLimits.select_option_value_max.value} characters or fewer" + ) - if description is not None and len(description) > 100: - raise ValueError("description must be 100 characters or fewer") + if ( + description is not None + and len(description) > ComponentLimits.select_option_description_max.value + ): + raise ValueError( + f"description must be {ComponentLimits.select_option_description_max.value} characters or fewer" + ) self.label = label self.value = label if value is MISSING else value @@ -1687,14 +1712,26 @@ def __init__( description: str | None = None, default: bool = False, ) -> None: - if len(label) > 100: - raise ValueError("label must be 100 characters or fewer") + if len(label) > ComponentLimits.select_option_label_max.value: + raise ValueError( + f"label must be {ComponentLimits.select_option_label_max.value} characters or fewer" + ) - if value is not MISSING and len(value) > 100: - raise ValueError("value must be 100 characters or fewer") + if ( + value is not MISSING + and len(value) > ComponentLimits.select_option_value_max.value + ): + raise ValueError( + f"value must be {ComponentLimits.select_option_value_max.value} characters or fewer" + ) - if description is not None and len(description) > 100: - raise ValueError("description must be 100 characters or fewer") + if ( + description is not None + and len(description) > ComponentLimits.select_option_description_max.value + ): + raise ValueError( + f"description must be {ComponentLimits.select_option_description_max.value} characters or fewer" + ) self.label = label self.value = label if value is MISSING else value diff --git a/discord/enums.py b/discord/enums.py index 802bb41535..34befdcbaa 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -88,6 +88,8 @@ "SelectDefaultValueType", "ApplicationEventWebhookStatus", "InviteTargetUsersJobStatusCode", + "ComponentLimits", + "EmbedLimits", ) @@ -96,21 +98,17 @@ def _create_value_cls(name, comparable): cls.__repr__ = lambda self: f"<{name}.{self.name}: {self.value!r}>" cls.__str__ = lambda self: f"{name}.{self.name}" if comparable: - cls.__le__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value <= other.value + cls.__le__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value <= other.value ) - cls.__ge__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value >= other.value + cls.__ge__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value >= other.value ) - cls.__lt__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value < other.value + cls.__lt__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value < other.value ) - cls.__gt__ = ( - lambda self, other: isinstance(other, self.__class__) - and self.value > other.value + cls.__gt__ = lambda self, other: ( + isinstance(other, self.__class__) and self.value > other.value ) return cls @@ -1212,6 +1210,107 @@ class InviteTargetUsersJobStatusCode(Enum): failed = 3 +class ComponentLimits(Enum): + # View constraints + view_children_max = 40 + + # ActionRow constraints + action_row_children_max = 5 + + # Button constraints + button_label_max = 80 + + # Container constraints + container_children_max = -1 # No limit + + # MediaGallery constraints + media_gallery_items_min = 1 + media_gallery_items_max = 10 + + # MediaGalleryItem constraints + media_gallery_item_description_max = 256 + + # Select constraints + select_placeholder_max = 150 + select_min_value_min = 0 + select_min_value_max = 25 + select_max_value_min = 1 + select_max_value_max = 25 + select_options_max = 25 + select_default_values_max = 25 + + # Select option constraints + select_option_label_max = 100 + select_option_value_max = 100 + select_option_description_max = 100 + + # Section constraints + section_accessory_max = 1 + section_children_min = 1 + section_children_max = 3 + + # TextInput constraints + text_input_max_count = 5 + text_input_label_max = 45 + text_input_placeholder_max = 100 + text_input_min_length_min = 0 + text_input_min_length_max = 4000 + text_input_max_length_min = 1 + text_input_max_length_max = 4000 + text_input_value_max = 4000 + + # TextDisplay constraints + text_display_content_max = 4000 + + # Thumbnail constraints + thumbnail_description_max = 256 + + # Custom ID constraints + custom_id_min = 1 + custom_id_max = 100 + + # RadioGroup constraints + radio_options_max = 10 + + # CheckboxGroup constraints + checkbox_options_max = 10 + checkbox_min_values_min = 0 + checkbox_min_values_max = 10 + checkbox_max_values_min = 1 + checkbox_max_values_max = 10 + + # FileUpload constraints + file_upload_min_files = 0 + file_upload_max_files_max = 10 + file_upload_max_values_min = 1 + + # Modal constraints + modal_title_max = 45 + modal_rows_max = 5 + + +class EmbedLimits(Enum): + # Embed field constraints + fields_max = 25 + + # Field title/name constraints + field_name_max = 256 + + # Field value constraints + field_value_max = 1024 + + # Embed description constraints + description_max = 4096 + + # Embed footer constraints + footer_text_max = 2048 + + # Embed author constraints + author_name_max = 256 + title_max = 256 + total_max = 6000 + + T = TypeVar("T") diff --git a/discord/ui/button.py b/discord/ui/button.py index 7ba2a3a34e..7c62b3921d 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -30,7 +30,7 @@ from typing import TYPE_CHECKING, Callable, TypeVar from ..components import Button as ButtonComponent -from ..enums import ButtonStyle, ComponentType +from ..enums import ButtonStyle, ComponentLimits, ComponentType from ..partial_emoji import PartialEmoji, _EmojiTag from .item import ItemCallbackType, ViewItem @@ -113,10 +113,17 @@ def __init__( self._row: int | None = None self._rendered_row: int | None = None super().__init__() - if label and len(str(label)) > 80: - raise ValueError("label must be 80 characters or fewer") - if custom_id is not None and len(str(custom_id)) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if label and len(str(label)) > ComponentLimits.button_label_max.value: + raise ValueError( + f"label must be {ComponentLimits.button_label_max.value} characters or fewer" + ) + if ( + custom_id is not None + and len(str(custom_id)) > ComponentLimits.custom_id_max.value + ): + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) if custom_id is not None and url is not None: raise TypeError("cannot mix both url and custom_id with Button") if sku_id is not None and url is not None: @@ -206,8 +213,10 @@ def custom_id(self) -> str | None: def custom_id(self, value: str | None): if value is not None and not isinstance(value, str): raise TypeError("custom_id must be None or str") - if value and len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if value and len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value self._provided_custom_id = value is not None @@ -238,8 +247,10 @@ def label(self) -> str | None: @label.setter def label(self, value: str | None): - if value and len(str(value)) > 80: - raise ValueError("label must be 80 characters or fewer") + if value and len(str(value)) > ComponentLimits.button_label_max.value: + raise ValueError( + f"label must be {ComponentLimits.button_label_max.value} characters or fewer" + ) self.underlying.label = str(value) if value is not None else value @property diff --git a/discord/ui/checkbox.py b/discord/ui/checkbox.py index 5915f9938d..1a33931d9b 100644 --- a/discord/ui/checkbox.py +++ b/discord/ui/checkbox.py @@ -28,7 +28,7 @@ from typing import TYPE_CHECKING from ..components import Checkbox as CheckboxComponent -from ..enums import ComponentType +from ..enums import ComponentLimits, ComponentType from .item import ModalItem __all__ = ("Checkbox",) @@ -105,8 +105,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError(f"custom_id must be str not {value.__class__.__name__}") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value @property diff --git a/discord/ui/checkbox_group.py b/discord/ui/checkbox_group.py index ed155898ab..d6d16e030e 100644 --- a/discord/ui/checkbox_group.py +++ b/discord/ui/checkbox_group.py @@ -31,7 +31,7 @@ from ..components import CheckboxGroup as CheckboxGroupComponent from ..components import CheckboxGroupOption -from ..enums import ComponentType +from ..enums import ComponentLimits, ComponentType from ..utils import MISSING from .item import ModalItem @@ -87,10 +87,20 @@ def __init__( id: int | None = None, ): super().__init__() - if min_values and (min_values < 0 or min_values > 10): - raise ValueError("min_values must be between 0 and 10") - if max_values and (max_values < 1 or max_values > 10): - raise ValueError("max_values must be between 1 and 10") + if min_values and ( + min_values < ComponentLimits.checkbox_min_values_min.value + or min_values > ComponentLimits.checkbox_min_values_max.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.checkbox_min_values_min.value} and {ComponentLimits.checkbox_min_values_max.value}" + ) + if max_values and ( + max_values < ComponentLimits.checkbox_max_values_min.value + or max_values > ComponentLimits.checkbox_max_values_max.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.checkbox_max_values_min.value} and {ComponentLimits.checkbox_max_values_max.value}" + ) if custom_id is not None and not isinstance(custom_id, str): raise TypeError( f"expected custom_id to be str, not {custom_id.__class__.__name__}" @@ -140,8 +150,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError(f"custom_id must be str not {value.__class__.__name__}") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value @property @@ -155,8 +167,13 @@ def min_values(self, value: int | None): raise TypeError( f"min_values must be None or int not {value.__class__.__name__}" ) - if value and (value < 0 or value > 10): - raise ValueError("min_values must be between 0 and 10") + if value and ( + value < ComponentLimits.checkbox_min_values_min.value + or value > ComponentLimits.checkbox_min_values_max.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.checkbox_min_values_min.value} and {ComponentLimits.checkbox_min_values_max.value}" + ) self.underlying.min_values = value @property @@ -170,8 +187,13 @@ def max_values(self, value: int | None): raise TypeError( f"max_values must be None or int not {value.__class__.__name__}" ) - if value and (value < 1 or value > 10): - raise ValueError("max_values must be between 1 and 10") + if value and ( + value < ComponentLimits.checkbox_max_values_min.value + or value > ComponentLimits.checkbox_max_values_max.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.checkbox_max_values_min.value} and {ComponentLimits.checkbox_max_values_max.value}" + ) self.underlying.max_values = value @property @@ -199,8 +221,10 @@ def options(self) -> list[CheckboxGroupOption]: def options(self, value: list[CheckboxGroupOption]): if not isinstance(value, list): raise TypeError("options must be a list of CheckboxGroupOption") - if len(value) > 10: - raise ValueError("you may only provide up to 10 options.") + if len(value) > ComponentLimits.checkbox_options_max.value: + raise ValueError( + f"you may only provide up to {ComponentLimits.checkbox_options_max.value} options." + ) if not all(isinstance(obj, CheckboxGroupOption) for obj in value): raise TypeError("all list items must subclass CheckboxGroupOption") @@ -262,8 +286,10 @@ def append_option(self, option: CheckboxGroupOption) -> Self: The number of options exceeds 10. """ - if len(self.underlying.options) >= 10: - raise ValueError("maximum number of options already provided") + if len(self.underlying.options) >= ComponentLimits.checkbox_options_max.value: + raise ValueError( + f"maximum number of options already provided ({ComponentLimits.checkbox_options_max.value})" + ) self.underlying.options.append(option) return self diff --git a/discord/ui/file_upload.py b/discord/ui/file_upload.py index 784b8f6733..6c723b6a8f 100644 --- a/discord/ui/file_upload.py +++ b/discord/ui/file_upload.py @@ -28,7 +28,7 @@ from typing import TYPE_CHECKING from ..components import FileUpload as FileUploadComponent -from ..enums import ComponentType +from ..enums import ComponentLimits, ComponentType from ..message import Attachment from .item import ModalItem @@ -78,10 +78,20 @@ def __init__( id: int | None = None, ): super().__init__() - if min_values and (min_values < 0 or min_values > 10): - raise ValueError("min_values must be between 0 and 10") - if max_values and (max_values < 1 or max_values > 10): - raise ValueError("max_values must be between 1 and 10") + if min_values and ( + min_values < ComponentLimits.file_upload_min_files.value + or min_values > ComponentLimits.file_upload_max_files_max.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.file_upload_min_files.value} and {ComponentLimits.file_upload_max_files_max.value}" + ) + if max_values and ( + max_values < ComponentLimits.file_upload_max_values_min.value + or max_values > ComponentLimits.file_upload_max_files_max.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.file_upload_max_values_min.value} and {ComponentLimits.file_upload_max_files_max.value}" + ) if custom_id is not None and not isinstance(custom_id, str): raise TypeError( f"expected custom_id to be str, not {custom_id.__class__.__name__}" @@ -126,8 +136,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError(f"custom_id must be str not {value.__class__.__name__}") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value @property @@ -138,9 +150,16 @@ def min_values(self) -> int | None: @min_values.setter def min_values(self, value: int | None): if value and not isinstance(value, int): - raise TypeError(f"min_values must be None or int not {value.__class__.__name__}") # type: ignore - if value and (value < 0 or value > 10): - raise ValueError("min_values must be between 0 and 10") + raise TypeError( + f"min_values must be None or int not {value.__class__.__name__}" + ) # type: ignore + if value and ( + value < ComponentLimits.file_upload_min_files.value + or value > ComponentLimits.file_upload_max_files_max.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.file_upload_min_files.value} and {ComponentLimits.file_upload_max_files_max.value}" + ) self.underlying.min_values = value @property @@ -151,9 +170,16 @@ def max_values(self) -> int | None: @max_values.setter def max_values(self, value: int | None): if value and not isinstance(value, int): - raise TypeError(f"max_values must be None or int not {value.__class__.__name__}") # type: ignore - if value and (value < 1 or value > 10): - raise ValueError("max_values must be between 1 and 10") + raise TypeError( + f"max_values must be None or int not {value.__class__.__name__}" + ) # type: ignore + if value and ( + value < ComponentLimits.file_upload_max_values_min.value + or value > ComponentLimits.file_upload_max_files_max.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.file_upload_max_values_min.value} and {ComponentLimits.file_upload_max_files_max.value}" + ) self.underlying.max_values = value @property diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 5b632e933c..6ad1cef6d2 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -28,7 +28,7 @@ from typing import TYPE_CHECKING from ..components import InputText as InputTextComponent -from ..enums import ComponentType, InputTextStyle +from ..enums import ComponentLimits, ComponentType, InputTextStyle from .item import ModalItem __all__ = ("InputText", "TextInput") @@ -101,16 +101,35 @@ def __init__( id: int | None = None, ): super().__init__() - if label and len(str(label)) > 45: - raise ValueError("label must be 45 characters or fewer") - if min_length and (min_length < 0 or min_length > 4000): - raise ValueError("min_length must be between 0 and 4000") - if max_length and (max_length < 0 or max_length > 4000): - raise ValueError("max_length must be between 1 and 4000") - if value and len(str(value)) > 4000: - raise ValueError("value must be 4000 characters or fewer") - if placeholder and len(str(placeholder)) > 100: - raise ValueError("placeholder must be 100 characters or fewer") + if label and len(str(label)) > ComponentLimits.text_input_label_max.value: + raise ValueError( + f"label must be {ComponentLimits.text_input_label_max.value} characters or fewer" + ) + if min_length and ( + min_length < ComponentLimits.text_input_min_length_min.value + or min_length > ComponentLimits.text_input_min_length_max.value + ): + raise ValueError( + f"min_length must be between {ComponentLimits.text_input_min_length_min.value} and {ComponentLimits.text_input_min_length_max.value}" + ) + if max_length and ( + max_length < ComponentLimits.text_input_max_length_min.value + or max_length > ComponentLimits.text_input_max_length_max.value + ): + raise ValueError( + f"max_length must be between {ComponentLimits.text_input_max_length_min.value} and {ComponentLimits.text_input_max_length_max.value}" + ) + if value and len(str(value)) > ComponentLimits.text_input_max_length_max.value: + raise ValueError( + f"value must be {ComponentLimits.text_input_max_length_max.value} characters or fewer" + ) + if ( + placeholder + and len(str(placeholder)) > ComponentLimits.text_input_placeholder_max.value + ): + raise ValueError( + f"placeholder must be {ComponentLimits.text_input_placeholder_max.value} characters or fewer" + ) if not isinstance(custom_id, str) and custom_id is not None: raise TypeError( f"expected custom_id to be str, not {custom_id.__class__.__name__}" @@ -180,8 +199,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError(f"custom_id must be str not {value.__class__.__name__}") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value @property @@ -193,8 +214,10 @@ def label(self) -> str: def label(self, value: str): if not isinstance(value, str): raise TypeError(f"label should be str not {value.__class__.__name__}") - if len(value) > 45: - raise ValueError("label must be 45 characters or fewer") + if len(value) > ComponentLimits.text_input_label_max.value: + raise ValueError( + f"label must be {ComponentLimits.text_input_label_max.value} characters or fewer" + ) self.underlying.label = value @property @@ -205,9 +228,13 @@ def placeholder(self) -> str | None: @placeholder.setter def placeholder(self, value: str | None): if value and not isinstance(value, str): - raise TypeError(f"placeholder must be None or str not {value.__class__.__name__}") # type: ignore - if value and len(value) > 100: - raise ValueError("placeholder must be 100 characters or fewer") + raise TypeError( + f"placeholder must be None or str not {value.__class__.__name__}" + ) # type: ignore + if value and len(value) > ComponentLimits.text_input_placeholder_max.value: + raise ValueError( + f"placeholder must be {ComponentLimits.text_input_placeholder_max.value} characters or fewer" + ) self.underlying.placeholder = value @property @@ -218,9 +245,16 @@ def min_length(self) -> int | None: @min_length.setter def min_length(self, value: int | None): if value and not isinstance(value, int): - raise TypeError(f"min_length must be None or int not {value.__class__.__name__}") # type: ignore - if value and (value < 0 or value) > 4000: - raise ValueError("min_length must be between 0 and 4000") + raise TypeError( + f"min_length must be None or int not {value.__class__.__name__}" + ) # type: ignore + if value and ( + value < ComponentLimits.text_input_min_length_min.value + or value > ComponentLimits.text_input_min_length_max.value + ): + raise ValueError( + f"min_length must be between {ComponentLimits.text_input_min_length_min.value} and {ComponentLimits.text_input_min_length_max.value}" + ) self.underlying.min_length = value @property @@ -231,9 +265,16 @@ def max_length(self) -> int | None: @max_length.setter def max_length(self, value: int | None): if value and not isinstance(value, int): - raise TypeError(f"min_length must be None or int not {value.__class__.__name__}") # type: ignore - if value and (value <= 0 or value > 4000): - raise ValueError("max_length must be between 1 and 4000") + raise TypeError( + f"max_length must be None or int not {value.__class__.__name__}" + ) # type: ignore + if value and ( + value < ComponentLimits.text_input_max_length_min.value + or value > ComponentLimits.text_input_max_length_max.value + ): + raise ValueError( + f"max_length must be between {ComponentLimits.text_input_max_length_min.value} and {ComponentLimits.text_input_max_length_max.value}" + ) self.underlying.max_length = value @property @@ -259,8 +300,10 @@ def value(self) -> str | None: def value(self, value: str | None): if value and not isinstance(value, str): raise TypeError(f"value must be None or str not {value.__class__.__name__}") # type: ignore - if value and len(str(value)) > 4000: - raise ValueError("value must be 4000 characters or fewer") + if value and len(str(value)) > ComponentLimits.text_input_max_length_max.value: + raise ValueError( + f"value must be {ComponentLimits.text_input_max_length_max.value} characters or fewer" + ) self.underlying.value = value @property diff --git a/discord/ui/label.py b/discord/ui/label.py index 69c519f0cd..c3baf900fa 100644 --- a/discord/ui/label.py +++ b/discord/ui/label.py @@ -35,16 +35,13 @@ RadioGroupOption, SelectDefaultValue, SelectOption, - _component_factory, ) -from ..enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle -from ..utils import find, get -from .button import Button +from ..enums import ChannelType, ComponentType, InputTextStyle from .checkbox import Checkbox from .checkbox_group import CheckboxGroup from .file_upload import FileUpload from .input_text import InputText -from .item import ItemCallbackType, ModalItem +from .item import ModalItem from .radio_group import RadioGroup from .select import Select @@ -53,9 +50,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from ..emoji import AppEmoji, GuildEmoji from ..interactions import Interaction - from ..partial_emoji import PartialEmoji, _EmojiTag from ..types.components import LabelComponent as LabelComponentPayload from .modal import DesignerModal @@ -168,7 +163,7 @@ def set_item(self, item: ModalItem) -> Self: if not isinstance(item, ModalItem): raise TypeError(f"expected ModalItem not {item.__class__!r}") if isinstance(item, InputText) and item.label: - raise ValueError(f"InputText.label cannot be set inside Label") + raise ValueError("InputText.label cannot be set inside Label") if self.modal: item.modal = self.modal item.parent = self diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 98ad8c965a..e26bcbc0ae 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -28,7 +28,7 @@ from ..components import MediaGallery as MediaGalleryComponent from ..components import MediaGalleryItem -from ..enums import ComponentType +from ..enums import ComponentLimits, ComponentType from .item import ViewItem __all__ = ("MediaGallery",) @@ -89,11 +89,13 @@ def items(self) -> list[MediaGalleryItem]: @items.setter def items(self, value: list[MediaGalleryItem]) -> None: - if len(value) > 10: - raise ValueError("may not set more than 10 items in a gallery.") + if len(value) > ComponentLimits.media_gallery_items_max.value: + raise ValueError( + f"may not set more than {ComponentLimits.media_gallery_items_max.value} items in a gallery." + ) if not all(isinstance(i, MediaGalleryItem) for i in value): - raise TypeError(f"items must be a list of MediaGalleryItem.") + raise TypeError("items must be a list of MediaGalleryItem.") self.underlying.items = value @@ -113,8 +115,10 @@ def append_item(self, item: MediaGalleryItem) -> Self: Maximum number of items has been exceeded (10). """ - if len(self.items) >= 10: - raise ValueError("maximum number of items exceeded") + if len(self.items) >= ComponentLimits.media_gallery_items_max.value: + raise ValueError( + f"maximum number of items exceeded ({ComponentLimits.media_gallery_items_max.value})" + ) if not isinstance(item, MediaGalleryItem): raise TypeError(f"expected MediaGalleryItem not {item.__class__!r}") @@ -146,8 +150,10 @@ def add_item( Maximum number of items has been exceeded (10). """ - if len(self.items) >= 10: - raise ValueError("maximum number of items exceeded") + if len(self.items) >= ComponentLimits.media_gallery_items_max.value: + raise ValueError( + f"maximum number of items exceeded ({ComponentLimits.media_gallery_items_max.value})" + ) item = MediaGalleryItem(url, description=description, spoiler=spoiler) diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 20704eb278..20a77eaba7 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -32,14 +32,11 @@ from itertools import groupby from typing import TYPE_CHECKING, Any, Iterator, TypeVar -from ..enums import ComponentType -from ..utils import _get_event_loop, find +from ..enums import ComponentLimits +from ..utils import _get_event_loop from .core import ItemInterface from .input_text import InputText from .item import ModalItem -from .label import Label -from .select import Select -from .text_display import TextDisplay __all__ = ( "BaseModal", @@ -84,8 +81,10 @@ def __init__( f"expected custom_id to be str, not {custom_id.__class__.__name__}" ) self._custom_id: str | None = custom_id or os.urandom(16).hex() - if len(title) > 45: - raise ValueError("title must be 45 characters or fewer") + if len(title) > ComponentLimits.modal_title_max.value: + raise ValueError( + f"title must be {ComponentLimits.modal_title_max.value} characters or fewer" + ) self._children: list[ModalItem] = [] super().__init__(timeout=timeout, store=store) for item in children: @@ -125,8 +124,10 @@ def title(self) -> str: @title.setter def title(self, value: str): - if len(value) > 45: - raise ValueError("title must be 45 characters or fewer") + if len(value) > ComponentLimits.modal_title_max.value: + raise ValueError( + f"title must be {ComponentLimits.modal_title_max.value} characters or fewer" + ) if not isinstance(value, str): raise TypeError(f"expected title to be str, not {value.__class__.__name__}") self._title = value @@ -157,8 +158,10 @@ def custom_id(self, value: str): raise TypeError( f"expected custom_id to be str, not {value.__class__.__name__}" ) - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self._custom_id = value async def callback(self, interaction: Interaction): @@ -183,8 +186,10 @@ def add_item(self, item: ModalItem) -> Self: The item to add to the modal """ - if len(self._children) >= 5: - raise ValueError("You can only have up to 5 items in a modal.") + if len(self._children) >= ComponentLimits.modal_rows_max.value: + raise ValueError( + f"You can only have up to {ComponentLimits.modal_rows_max.value} items in a modal." + ) if not isinstance(item, ModalItem): raise TypeError(f"expected ModalItem, not {item.__class__!r}") @@ -427,7 +432,7 @@ def children(self, value: list[ModalItem]): ) if isinstance(item, (InputText,)): raise TypeError( - f"DesignerModal does not accept InputText directly. Use Label instead." + "DesignerModal does not accept InputText directly. Use Label instead." ) self._children = value @@ -442,7 +447,7 @@ def add_item(self, item: ModalItem) -> Self: if isinstance(item, (InputText,)): raise TypeError( - f"DesignerModal does not accept InputText directly. Use Label instead." + "DesignerModal does not accept InputText directly. Use Label instead." ) super().add_item(item) diff --git a/discord/ui/radio_group.py b/discord/ui/radio_group.py index 7a01501776..693cc93b63 100644 --- a/discord/ui/radio_group.py +++ b/discord/ui/radio_group.py @@ -31,7 +31,7 @@ from ..components import RadioGroup as RadioGroupComponent from ..components import RadioGroupOption -from ..enums import ComponentType +from ..enums import ComponentLimits, ComponentType from ..utils import MISSING from .item import ModalItem @@ -118,8 +118,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError(f"custom_id must be str not {value.__class__.__name__}") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value @property @@ -212,8 +214,10 @@ def append_option(self, option: RadioGroupOption) -> Self: The number of options exceeds 10. """ - if len(self.underlying.options) >= 10: - raise ValueError("maximum number of options already provided") + if len(self.underlying.options) >= ComponentLimits.radio_options_max.value: + raise ValueError( + f"maximum number of options already provided ({ComponentLimits.radio_options_max.value})" + ) self.underlying.options.append(option) return self diff --git a/discord/ui/select.py b/discord/ui/select.py index d6f5a105ca..3998707dea 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -36,7 +36,7 @@ from ..channel import _threaded_guild_channel_factory from ..components import SelectDefaultValue, SelectMenu, SelectOption from ..emoji import AppEmoji, GuildEmoji -from ..enums import ChannelType, ComponentType, SelectDefaultValueType +from ..enums import ChannelType, ComponentLimits, ComponentType, SelectDefaultValueType from ..errors import InvalidArgument from ..interactions import Interaction from ..member import Member @@ -265,12 +265,27 @@ def __init__( super().__init__() self._selected_values: list[str] = [] self._interaction: Interaction | None = None - if min_values < 0 or min_values > 25: - raise ValueError("min_values must be between 0 and 25") - if max_values < 1 or max_values > 25: - raise ValueError("max_values must be between 1 and 25") - if placeholder and len(placeholder) > 150: - raise ValueError("placeholder must be 150 characters or fewer") + if ( + min_values < ComponentLimits.select_min_value_min.value + or min_values > ComponentLimits.select_min_value_max.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.select_min_value_min.value} and {ComponentLimits.select_min_value_max.value}" + ) + if ( + max_values < ComponentLimits.select_max_value_min.value + or max_values > ComponentLimits.select_max_value_max.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.select_max_value_min.value} and {ComponentLimits.select_max_value_max.value}" + ) + if ( + placeholder + and len(placeholder) > ComponentLimits.select_placeholder_max.value + ): + raise ValueError( + f"placeholder must be {ComponentLimits.select_placeholder_max.value} characters or fewer" + ) if not isinstance(custom_id, str) and custom_id is not None: raise TypeError( f"expected custom_id to be str, not {custom_id.__class__.__name__}" @@ -377,8 +392,10 @@ def custom_id(self) -> str: def custom_id(self, value: str): if not isinstance(value, str): raise TypeError("custom_id must be None or str") - if len(value) > 100: - raise ValueError("custom_id must be 100 characters or fewer") + if len(value) > ComponentLimits.custom_id_max.value: + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value self._provided_custom_id = value is not None @@ -391,8 +408,10 @@ def placeholder(self) -> str | None: def placeholder(self, value: str | None): if value is not None and not isinstance(value, str): raise TypeError("placeholder must be None or str") - if value and len(value) > 150: - raise ValueError("placeholder must be 150 characters or fewer") + if value and len(value) > ComponentLimits.select_placeholder_max.value: + raise ValueError( + f"placeholder must be {ComponentLimits.select_placeholder_max.value} characters or fewer" + ) self.underlying.placeholder = value @@ -403,8 +422,13 @@ def min_values(self) -> int: @min_values.setter def min_values(self, value: int): - if value < 0 or value > 25: - raise ValueError("min_values must be between 0 and 25") + if ( + value < ComponentLimits.select_min_value_min.value + or value > ComponentLimits.select_min_value_max.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.select_min_value_min.value} and {ComponentLimits.select_min_value_max.value}" + ) self.underlying.min_values = int(value) @property @@ -414,8 +438,13 @@ def max_values(self) -> int: @max_values.setter def max_values(self, value: int): - if value < 1 or value > 25: - raise ValueError("max_values must be between 1 and 25") + if ( + value < ComponentLimits.select_max_value_min.value + or value > ComponentLimits.select_max_value_max.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.select_max_value_min.value} and {ComponentLimits.select_max_value_max.value}" + ) self.underlying.max_values = int(value) @property @@ -575,8 +604,10 @@ def append_default_value( if self.type is ComponentType.string_select: raise TypeError("string_select selects do not allow default_values") - if len(self.default_values) >= 25: - raise ValueError("maximum number of default values exceeded (25)") + if len(self.default_values) >= ComponentLimits.select_default_values_max.value: + raise ValueError( + f"maximum number of default values exceeded ({ComponentLimits.select_default_values_max.value})" + ) if not isinstance(value, SelectDefaultValue): value = SelectDefaultValue._handle_model(value) @@ -654,7 +685,7 @@ def append_option(self, option: SelectOption) -> Self: if self.underlying.type is not ComponentType.string_select: raise Exception("options can only be set on string selects") - if len(self.underlying.options) > 25: + if len(self.underlying.options) > ComponentLimits.select_options_max.value: raise ValueError("maximum number of options already provided") self.underlying.options.append(option) diff --git a/discord/ui/view.py b/discord/ui/view.py index 568240ffcf..3e15d348ed 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -62,9 +62,8 @@ from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent from ..components import _component_factory -from ..enums import ChannelType, SeparatorSpacingSize +from ..enums import ChannelType, ComponentLimits from ..errors import Forbidden, NotFound -from ..utils import find from .core import ItemInterface from .item import Item, ItemCallbackType, ModalItem, ViewItem @@ -78,7 +77,6 @@ if TYPE_CHECKING: - from ..components import MediaGalleryItem from ..interactions import Interaction, InteractionMessage from ..message import Message from ..state import ConnectionState @@ -598,8 +596,10 @@ def __init_subclass__(cls) -> None: if hasattr(member, "__discord_ui_model_type__"): children.append(member) - if len(children) > 40: - raise TypeError("View cannot have more than 40 children") + if len(children) > ComponentLimits.view_children_max.value: + raise TypeError( + f"View cannot have more than {ComponentLimits.view_children_max.value} children" + ) cls.__view_children_items__ = children @@ -932,7 +932,7 @@ def add_item(self, item: ViewItem[V]) -> Self: if isinstance(item._underlying, (SelectComponent, ButtonComponent)): raise ValueError( - f"cannot add Select or Button to DesignerView directly. Use ActionRow instead." + "cannot add Select or Button to DesignerView directly. Use ActionRow instead." ) super().add_item(item) diff --git a/docs/api/enums.rst b/docs/api/enums.rst index efa16a4a5e..7650bc7c44 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2682,6 +2682,222 @@ of :class:`enum.Enum`. .. attribute:: disabled_by_discord The application webhook is disabled by Discord. +.. class:: ComponentLimits + + Represents the limits for various Discord UI components. + + .. versionadded:: 2.8.1 + + .. attribute:: view_children_max + + The maximum number of children a View can have (40). + + .. attribute:: action_row_children_max + + The maximum number of children an ActionRow can have (5). + + .. attribute:: button_label_max + + The maximum length of a button label (80). + + .. attribute:: container_children_max + + The maximum number of children a Container can have (no limit). + + .. attribute:: media_gallery_items_min + + The minimum number of items in a MediaGallery (1). + + .. attribute:: media_gallery_items_max + + The maximum number of items in a MediaGallery (10). + + .. attribute:: media_gallery_item_description_max + + The maximum length of a MediaGalleryItem description (256). + + .. attribute:: select_placeholder_max + + The maximum length of a select placeholder (150). + + .. attribute:: select_min_value_min + + The minimum value for select's minimum value constraint (0). + + .. attribute:: select_min_value_max + + The maximum value for select's minimum value constraint (25). + + .. attribute:: select_max_value_min + + The minimum value for select's maximum value constraint (1). + + .. attribute:: select_max_value_max + + The maximum value for select's maximum value constraint (25). + + .. attribute:: select_options_max + + The maximum number of options in a select menu (25). + + .. attribute:: select_default_values_max + + The maximum number of default values in a select menu (25). + + .. attribute:: select_option_label_max + + The maximum length of a select option label (100). + + .. attribute:: select_option_value_max + + The maximum length of a select option value (100). + + .. attribute:: select_option_description_max + + The maximum length of a select option description (100). + + .. attribute:: section_accessory_max + + The maximum number of accessories in a Section (1). + + .. attribute:: section_children_min + + The minimum number of children in a Section (1). + + .. attribute:: section_children_max + + The maximum number of children in a Section (3). + + .. attribute:: text_input_max_count + + The maximum number of TextInputs in a modal (5). + + .. attribute:: text_input_label_max + + The maximum length of a TextInput label (45). + + .. attribute:: text_input_placeholder_max + + The maximum length of a TextInput placeholder (100). + + .. attribute:: text_input_min_length_min + + The minimum value for TextInput's minimum length (0). + + .. attribute:: text_input_min_length_max + + The maximum value for TextInput's minimum length (4000). + + .. attribute:: text_input_max_length_max + + The maximum value for TextInput's maximum length (4000). + + .. attribute:: text_input_value_max + + The maximum length of a TextInput value (4000). + + .. attribute:: text_display_content_max + + The maximum length of TextDisplay content (4000). + + .. attribute:: thumbnail_description_max + + The maximum length of a Thumbnail description (256). + + .. attribute:: custom_id_min + + The minimum length of a custom ID (1). + + .. attribute:: custom_id_max + + The maximum length of a custom ID (100). + + .. attribute:: radio_options_max + + The maximum number of options in a RadioGroup (10). + + .. attribute:: checkbox_options_max + + The maximum number of options in a CheckboxGroup (10). + + .. attribute:: checkbox_min_values_min + + The minimum value for checkbox's minimum value constraint (0). + + .. attribute:: checkbox_min_values_max + + The maximum value for checkbox's minimum value constraint (10). + + .. attribute:: checkbox_max_values_min + + The minimum value for checkbox's maximum value constraint (1). + + .. attribute:: checkbox_max_values_max + + The maximum value for checkbox's maximum value constraint (10). + + .. attribute:: file_upload_min_files + + The minimum number of files that can be uploaded (0). + + .. attribute:: file_upload_max_files_max + + The maximum number of files that can be uploaded (10). + + .. attribute:: file_upload_max_values_min + + The minimum value for file upload's maximum value constraint (1). + + .. attribute:: modal_title_max + + The maximum length of a modal title (45). + + .. attribute:: modal_rows_max + + The maximum number of rows in a modal (5). + +.. class:: EmbedLimits + + Represents the limits for Discord embeds. + + .. note:: + + The sum of all characters in an embed must be ≤ 6000. + + .. versionadded:: 2.8.1 + + .. attribute:: fields_max + + The maximum number of embed fields (25). + + .. attribute:: field_name_max + + The maximum length of a field name/title (256). + + .. attribute:: field_value_max + + The maximum length of a field value (1024). + + .. attribute:: description_max + + The maximum length of an embed description (4096). + + .. attribute:: footer_text_max + + The maximum length of an embed footer text (2048). + + .. attribute:: author_name_max + + The maximum length of an embed author name (256). + + .. attribute:: title_max + + The maximum length of an embed title (256). + + .. attribute:: total_max + + The maximum total character limit for an entire embed (6000). + .. class:: TeamRole Represents a app team role.