From 529f9d5f0b5496ac6bb68bb685b531b2d944b5b3 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:31:53 +0300 Subject: [PATCH 01/11] feat: Add ComponentLimits and EmbedLimits enums for constraint definitions Co-authored-by: Copilot --- discord/enums.py | 99 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 802bb41535..ac2c110ddb 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,83 @@ 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 = float("inf") # 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_max = 25 + select_max_value_max = 25 + select_options_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_max = 4000 + 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 + + +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") From 4069993cf817d2b367ffb270588458e76d6f5eab Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:33:46 +0300 Subject: [PATCH 02/11] fix: Update changelog to include `ComponentLimits` and `EmbedLimits` enums Co-authored-by: Copilot --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b8942837..9a316b9b4e 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 From ff6e8e8cbe73edb1a6b71a9a961084dfc030ec0b Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:19:17 +0300 Subject: [PATCH 03/11] feat: Add ComponentLimits and EmbedLimits classes with detailed constraints --- docs/api/enums.rst | 156 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/docs/api/enums.rst b/docs/api/enums.rst index efa16a4a5e..8c5d75f7cb 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2682,6 +2682,162 @@ 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_max + + The maximum value for select's minimum value constraint (25). + + .. 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_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_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). + +.. 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. From e91d7d908418dad2cb598b41d90d0b8e96166b13 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:24:09 +0300 Subject: [PATCH 04/11] feat: Integrate ComponentLimits into UI components for enhanced validation Co-authored-by: Copilot --- discord/components.py | 37 +++++++------- discord/enums.py | 21 ++++++++ discord/ui/button.py | 18 +++---- discord/ui/checkbox.py | 6 +-- discord/ui/checkbox_group.py | 54 +++++++++++++++------ discord/ui/file_upload.py | 52 +++++++++++++++----- discord/ui/input_text.py | 94 ++++++++++++++++++++++++++---------- discord/ui/label.py | 11 ++--- discord/ui/media_gallery.py | 22 ++++++--- discord/ui/modal.py | 35 ++++++++------ discord/ui/radio_group.py | 10 ++-- discord/ui/select.py | 67 ++++++++++++++++++------- discord/ui/view.py | 6 +-- 13 files changed, 292 insertions(+), 141 deletions(-) diff --git a/discord/components.py b/discord/components.py index d6e1eb89a8..f5f95768cc 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,14 @@ 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 @@ -1527,14 +1528,14 @@ 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 +1688,14 @@ 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 ac2c110ddb..bf49f8bf2b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1232,9 +1232,12 @@ class ComponentLimits(Enum): # 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 @@ -1264,6 +1267,24 @@ class ComponentLimits(Enum): 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 = 10 + + # Modal constraints + modal_title_max = 45 + modal_rows_max = 5 + class EmbedLimits(Enum): # Embed field constraints diff --git a/discord/ui/button.py b/discord/ui/button.py index 7ba2a3a34e..bc80f9155d 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,10 @@ 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 +206,8 @@ 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 +238,8 @@ 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..4793be0c31 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,8 @@ 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..e48c021232 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,18 @@ 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 < 0 or min_values > ComponentLimits.checkbox_options_max.value + ): + raise ValueError( + f"min_values must be between 0 and {ComponentLimits.checkbox_options_max.value}" + ) + if max_values and ( + max_values < 1 or max_values > ComponentLimits.checkbox_options_max.value + ): + raise ValueError( + f"max_values must be between 1 and {ComponentLimits.checkbox_options_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 +148,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 +165,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 +185,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 +219,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 +284,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..b5150e88a1 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.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.file_upload_min_files.value} and {ComponentLimits.file_upload_max_files.value}" + ) + if max_values and ( + max_values < ComponentLimits.file_upload_min_files.value + or max_values > ComponentLimits.file_upload_max_files.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.file_upload_min_files.value} and {ComponentLimits.file_upload_max_files.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.value + ): + raise ValueError( + f"min_values must be between {ComponentLimits.file_upload_min_files.value} and {ComponentLimits.file_upload_max_files.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_min_files.value + or value > ComponentLimits.file_upload_max_files.value + ): + raise ValueError( + f"max_values must be between {ComponentLimits.file_upload_min_files.value} and {ComponentLimits.file_upload_max_files.value}" + ) self.underlying.max_values = value @property diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 5b632e933c..46e6a30d5c 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_max.value + or min_length > ComponentLimits.text_input_max_length_max.value + ): + raise ValueError( + f"min_length must be between {ComponentLimits.text_input_min_length_max.value} and {ComponentLimits.text_input_max_length_max.value}" + ) + if max_length and ( + max_length < ComponentLimits.text_input_min_length_max.value + or max_length > ComponentLimits.text_input_max_length_max.value + ): + raise ValueError( + f"max_length must be between {ComponentLimits.text_input_min_length_max.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_max.value + or value > ComponentLimits.text_input_max_length_max.value + ): + raise ValueError( + f"min_length must be between {ComponentLimits.text_input_min_length_max.value} and {ComponentLimits.text_input_max_length_max.value}" + ) self.underlying.min_length = value @property @@ -231,9 +265,15 @@ 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 <= 0 or value > ComponentLimits.text_input_max_length_max.value + ): + raise ValueError( + f"max_length must be between 1 and {ComponentLimits.text_input_max_length_max.value}" + ) self.underlying.max_length = value @property @@ -259,8 +299,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..dc1f660e20 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,8 @@ 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 +212,8 @@ 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..47f314bf24 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 + or min_values > ComponentLimits.select_min_value_max + ): + raise ValueError( + f"min_values must be between {ComponentLimits.select_min_value_min} and {ComponentLimits.select_min_value_max}" + ) + if ( + max_values < ComponentLimits.select_max_value_min + or max_values > ComponentLimits.select_max_value_max + ): + raise ValueError( + f"max_values must be between {ComponentLimits.select_max_value_min} and {ComponentLimits.select_max_value_max}" + ) + 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..9b0381e605 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -62,7 +62,7 @@ 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, SeparatorSpacingSize from ..errors import Forbidden, NotFound from ..utils import find from .core import ItemInterface @@ -598,8 +598,8 @@ 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 From ec89add98188a654bc39b674dbb47a3ff81f4d74 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:24:21 +0300 Subject: [PATCH 05/11] refactor: Improve error messages for component limits in UI elements --- discord/components.py | 66 ++++++++++++++++++++++++++++++--------- discord/ui/button.py | 21 ++++++++++--- discord/ui/checkbox.py | 4 ++- discord/ui/radio_group.py | 8 +++-- discord/ui/view.py | 16 +++++----- 5 files changed, 84 insertions(+), 31 deletions(-) diff --git a/discord/components.py b/discord/components.py index f5f95768cc..41739192b5 100644 --- a/discord/components.py +++ b/discord/components.py @@ -726,13 +726,25 @@ def __init__( default: bool = False, ) -> None: if len(label) > ComponentLimits.select_option_label_max.value: - raise ValueError(f"label must be {ComponentLimits.select_option_label_max.value} characters or fewer") + raise ValueError( + f"label must be {ComponentLimits.select_option_label_max.value} 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 ( + 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) > ComponentLimits.select_option_description_max.value: - raise ValueError(f"description must be {ComponentLimits.select_option_description_max.value} 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 @@ -1529,13 +1541,25 @@ def __init__( default: bool = False, ) -> None: if len(label) > ComponentLimits.select_option_label_max.value: - raise ValueError(f"label must be {ComponentLimits.select_option_label_max.value} characters or fewer") + raise ValueError( + f"label must be {ComponentLimits.select_option_label_max.value} 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 ( + 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) > ComponentLimits.select_option_description_max.value: - raise ValueError(f"description must be {ComponentLimits.select_option_description_max.value} 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 @@ -1689,13 +1713,25 @@ def __init__( default: bool = False, ) -> None: if len(label) > ComponentLimits.select_option_label_max.value: - raise ValueError(f"label must be {ComponentLimits.select_option_label_max.value} characters or fewer") + raise ValueError( + f"label must be {ComponentLimits.select_option_label_max.value} 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 ( + 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) > ComponentLimits.select_option_description_max.value: - raise ValueError(f"description must be {ComponentLimits.select_option_description_max.value} 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/ui/button.py b/discord/ui/button.py index bc80f9155d..7c62b3921d 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -114,9 +114,16 @@ def __init__( self._rendered_row: int | None = None super().__init__() 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") + 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: @@ -207,7 +214,9 @@ 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) > ComponentLimits.custom_id_max.value: - raise ValueError(f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer") + 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 @@ -239,7 +248,9 @@ def label(self) -> str | None: @label.setter def label(self, value: str | None): 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") + 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 4793be0c31..1a33931d9b 100644 --- a/discord/ui/checkbox.py +++ b/discord/ui/checkbox.py @@ -106,7 +106,9 @@ 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) > ComponentLimits.custom_id_max.value: - raise ValueError(f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer") + 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/radio_group.py b/discord/ui/radio_group.py index dc1f660e20..693cc93b63 100644 --- a/discord/ui/radio_group.py +++ b/discord/ui/radio_group.py @@ -119,7 +119,9 @@ 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) > ComponentLimits.custom_id_max.value: - raise ValueError(f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer") + raise ValueError( + f"custom_id must be {ComponentLimits.custom_id_max.value} characters or fewer" + ) self.underlying.custom_id = value @property @@ -213,7 +215,9 @@ def append_option(self, option: RadioGroupOption) -> Self: """ if len(self.underlying.options) >= ComponentLimits.radio_options_max.value: - raise ValueError(f"maximum number of options already provided ({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/view.py b/discord/ui/view.py index 9b0381e605..e7bd78ad24 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, ComponentLimits, 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 @@ -599,7 +597,9 @@ def __init_subclass__(cls) -> None: children.append(member) if len(children) > ComponentLimits.view_children_max.value: - raise TypeError(f"View cannot have more than {ComponentLimits.view_children_max.value} children") + 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) @@ -961,9 +961,9 @@ def is_components_v2(self) -> bool: class ViewStore: def __init__(self, state: ConnectionState): # (component_type, message_id, custom_id): (BaseView, ViewItem) - self._views: dict[tuple[int, int | None, str], tuple[BaseView, ViewItem[V]]] = ( - {} - ) + self._views: dict[ + tuple[int, int | None, str], tuple[BaseView, ViewItem[V]] + ] = {} # message_id: View self._synced_message_views: dict[int, BaseView] = {} self._state: ConnectionState = state From c03ba1a1df37dea51768ff32f54baeb161e08f26 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:24:53 +0000 Subject: [PATCH 06/11] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ui/view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index e7bd78ad24..3e15d348ed 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -961,9 +961,9 @@ def is_components_v2(self) -> bool: class ViewStore: def __init__(self, state: ConnectionState): # (component_type, message_id, custom_id): (BaseView, ViewItem) - self._views: dict[ - tuple[int, int | None, str], tuple[BaseView, ViewItem[V]] - ] = {} + self._views: dict[tuple[int, int | None, str], tuple[BaseView, ViewItem[V]]] = ( + {} + ) # message_id: View self._synced_message_views: dict[int, BaseView] = {} self._state: ConnectionState = state From acc6001154c931e0fa69b5525438666f8431df8b Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:28:04 +0300 Subject: [PATCH 07/11] feat: Update ComponentLimits for text input and checkbox group validation Co-authored-by: Copilot --- discord/enums.py | 2 ++ discord/ui/checkbox_group.py | 10 ++++++---- discord/ui/input_text.py | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index bf49f8bf2b..15823a08d6 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1253,7 +1253,9 @@ class ComponentLimits(Enum): 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 diff --git a/discord/ui/checkbox_group.py b/discord/ui/checkbox_group.py index e48c021232..d6d16e030e 100644 --- a/discord/ui/checkbox_group.py +++ b/discord/ui/checkbox_group.py @@ -88,16 +88,18 @@ def __init__( ): super().__init__() if min_values and ( - min_values < 0 or min_values > ComponentLimits.checkbox_options_max.value + 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 0 and {ComponentLimits.checkbox_options_max.value}" + 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 < 1 or max_values > ComponentLimits.checkbox_options_max.value + 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 1 and {ComponentLimits.checkbox_options_max.value}" + 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( diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 46e6a30d5c..12dd30c776 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -269,10 +269,11 @@ def max_length(self, value: int | None): f"max_length must be None or int not {value.__class__.__name__}" ) # type: ignore if value and ( - value <= 0 or value > ComponentLimits.text_input_max_length_max.value + 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 1 and {ComponentLimits.text_input_max_length_max.value}" + 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 From d32872855e272f73b26c342055d2c42bb8ff1ada Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:53:40 +0300 Subject: [PATCH 08/11] fix: Update component limits for thumbnail and gallery descriptions, input text min/max length, and select min/max values Co-authored-by: Copilot --- discord/components.py | 4 ++-- discord/ui/input_text.py | 18 +++++++++--------- discord/ui/select.py | 14 +++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/discord/components.py b/discord/components.py index 41739192b5..6c0603862e 100644 --- a/discord/components.py +++ b/discord/components.py @@ -1003,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. """ @@ -1052,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. """ diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 12dd30c776..6ad1cef6d2 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -106,18 +106,18 @@ def __init__( f"label must be {ComponentLimits.text_input_label_max.value} characters or fewer" ) if min_length and ( - min_length < ComponentLimits.text_input_min_length_max.value - or min_length > ComponentLimits.text_input_max_length_max.value + 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_max.value} and {ComponentLimits.text_input_max_length_max.value}" + 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_min_length_max.value + 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_min_length_max.value} and {ComponentLimits.text_input_max_length_max.value}" + 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( @@ -249,11 +249,11 @@ def min_length(self, value: int | None): f"min_length must be None or int not {value.__class__.__name__}" ) # type: ignore if value and ( - value < ComponentLimits.text_input_min_length_max.value - or value > ComponentLimits.text_input_max_length_max.value + 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_max.value} and {ComponentLimits.text_input_max_length_max.value}" + 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 @@ -269,7 +269,7 @@ def max_length(self, value: int | None): 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 + value < ComponentLimits.text_input_max_length_min.value or value > ComponentLimits.text_input_max_length_max.value ): raise ValueError( diff --git a/discord/ui/select.py b/discord/ui/select.py index 47f314bf24..d799c39334 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -266,18 +266,18 @@ def __init__( self._selected_values: list[str] = [] self._interaction: Interaction | None = None if ( - min_values < ComponentLimits.select_min_value_min - or min_values > ComponentLimits.select_min_value_max + 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} and {ComponentLimits.select_min_value_max}" + 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 - or max_values > ComponentLimits.select_max_value_max + 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} and {ComponentLimits.select_max_value_max}" + f"max_values must be between {ComponentLimits.select_max_value_min.value} and {ComponentLimits.select_max_value_max.value}" ) if ( placeholder @@ -685,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) > ComponentLimits.select_options_max.value: + if len(self.underlying.options) >= ComponentLimits.select_options_max.value: raise ValueError("maximum number of options already provided") self.underlying.options.append(option) From 6fe7a571041a5771a124ccdc7add8c30c2d59417 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Thu, 14 May 2026 20:01:55 +0200 Subject: [PATCH 09/11] Update discord/enums.py Co-authored-by: Michael Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/enums.py b/discord/enums.py index 15823a08d6..8eaee70c02 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1221,7 +1221,7 @@ class ComponentLimits(Enum): button_label_max = 80 # Container constraints - container_children_max = float("inf") # No limit + container_children_max = -1 # No limit # MediaGallery constraints media_gallery_items_min = 1 From 6048caa9946212dc96fb703139f168579a6385b1 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Thu, 14 May 2026 20:02:26 +0200 Subject: [PATCH 10/11] Update discord/ui/select.py Co-authored-by: Michael Signed-off-by: Lumouille <144063653+Lumabots@users.noreply.github.com> --- discord/ui/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/select.py b/discord/ui/select.py index d799c39334..3998707dea 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -685,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) >= ComponentLimits.select_options_max.value: + if len(self.underlying.options) > ComponentLimits.select_options_max.value: raise ValueError("maximum number of options already provided") self.underlying.options.append(option) From d9c8691761d21719405730a3a4d75a01eba89cda Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Thu, 14 May 2026 21:11:56 +0300 Subject: [PATCH 11/11] feat: Update file upload limits and add new constraints in enums --- discord/enums.py | 3 +- discord/ui/file_upload.py | 20 ++++++------- docs/api/enums.rst | 60 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 8eaee70c02..34befdcbaa 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -1281,7 +1281,8 @@ class ComponentLimits(Enum): # FileUpload constraints file_upload_min_files = 0 - file_upload_max_files = 10 + file_upload_max_files_max = 10 + file_upload_max_values_min = 1 # Modal constraints modal_title_max = 45 diff --git a/discord/ui/file_upload.py b/discord/ui/file_upload.py index b5150e88a1..6c723b6a8f 100644 --- a/discord/ui/file_upload.py +++ b/discord/ui/file_upload.py @@ -80,17 +80,17 @@ def __init__( super().__init__() if min_values and ( min_values < ComponentLimits.file_upload_min_files.value - or min_values > ComponentLimits.file_upload_max_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.value}" + 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_min_files.value - or max_values > ComponentLimits.file_upload_max_files.value + 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_min_files.value} and {ComponentLimits.file_upload_max_files.value}" + 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( @@ -155,10 +155,10 @@ def min_values(self, value: int | None): ) # type: ignore if value and ( value < ComponentLimits.file_upload_min_files.value - or value > ComponentLimits.file_upload_max_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.value}" + 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 @@ -174,11 +174,11 @@ def max_values(self, value: int | None): f"max_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.value + 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_min_files.value} and {ComponentLimits.file_upload_max_files.value}" + 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 diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 8c5d75f7cb..7650bc7c44 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2720,10 +2720,18 @@ of :class:`enum.Enum`. 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). @@ -2732,6 +2740,10 @@ of :class:`enum.Enum`. 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). @@ -2768,6 +2780,10 @@ of :class:`enum.Enum`. 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). @@ -2796,6 +2812,50 @@ of :class:`enum.Enum`. 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.