Source code for discord.commands.options

"""
The MIT License (MIT)

Copyright (c) 2021-present Pycord Development

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""

from __future__ import annotations

import inspect
import logging
import sys
import types
from collections.abc import Awaitable, Callable, Iterable
from enum import Enum
from typing import (
    TYPE_CHECKING,
    Any,
    Generic,
    Literal,
    Optional,
    Sequence,
    Type,
    Union,
    get_args,
    overload,
)

from typing_extensions import TypeAlias, TypeVar, override

from discord.interactions import AutocompleteInteraction, Interaction

from ..utils.private import maybe_awaitable

if sys.version_info >= (3, 12):
    from typing import TypeAliasType
else:
    from typing_extensions import TypeAliasType

from ..abc import Mentionable
from ..channel import (
    BaseChannel,
    CategoryChannel,
    DMChannel,
    ForumChannel,
    GuildChannel,
    MediaChannel,
    StageChannel,
    TextChannel,
    Thread,
    VoiceChannel,
)
from ..commands import ApplicationContext, AutocompleteContext
from ..enums import ChannelType, SlashCommandOptionType
from ..enums import Enum as DiscordEnum
from ..utils import MISSING, Undefined, basic_autocomplete

if TYPE_CHECKING:
    from ..cog import Cog
    from ..ext.commands import Converter
    from ..member import Member
    from ..message import Attachment
    from ..role import Role
    from ..user import User

    InputType = (
        type[
            str | bool | int | float | GuildChannel | Thread | Member | User | Attachment | Role | Mentionable
            #            | Converter
        ]
        | SlashCommandOptionType
        #        | Converter
    )

    AutocompleteReturnType = Iterable["OptionChoice"] | Iterable[str] | Iterable[int] | Iterable[float]
    AR_T = TypeVar("AR_T", bound=AutocompleteReturnType)
    MaybeAwaitable = AR_T | Awaitable[AR_T]
    AutocompleteFunction: TypeAlias = (
        Callable[[AutocompleteInteraction], MaybeAwaitable[AutocompleteReturnType]]
        | Callable[[Any, AutocompleteInteraction], MaybeAwaitable[AutocompleteReturnType]]
        | Callable[
            [AutocompleteInteraction, Any],
            MaybeAwaitable[AutocompleteReturnType],
        ]
        | Callable[
            [Any, AutocompleteInteraction, Any],
            MaybeAwaitable[AutocompleteReturnType],
        ]
    )


__all__ = (
    "ThreadOption",
    "Option",
    "OptionChoice",
)

CHANNEL_TYPE_MAP = {
    TextChannel: ChannelType.text,
    VoiceChannel: ChannelType.voice,
    StageChannel: ChannelType.stage_voice,
    CategoryChannel: ChannelType.category,
    Thread: ChannelType.public_thread,
    ForumChannel: ChannelType.forum,
    MediaChannel: ChannelType.media,
    DMChannel: ChannelType.private,
}

_log = logging.getLogger(__name__)


[docs] class ThreadOption: """Represents a class that can be passed as the ``input_type`` for an :class:`Option` class. .. versionadded:: 2.0 Parameters ---------- thread_type: Literal["public", "private", "news"] The thread type to expect for this options input. """ def __init__(self, thread_type: Literal["public", "private", "news"]): type_map = { "public": ChannelType.public_thread, "private": ChannelType.private_thread, "news": ChannelType.news_thread, } self._type = type_map[thread_type]
T = TypeVar("T", bound="str | int | float", default="str") class ApplicationCommandOptionAutocomplete: def __init__(self, autocomplete_function: AutocompleteFunction) -> None: self.autocomplete_function: AutocompleteFunction = autocomplete_function self.self: Any | None = None async def __call__(self, interaction: AutocompleteInteraction) -> AutocompleteReturnType: if self.self is not None: return await maybe_awaitable(self.autocomplete_function(self.self, interaction)) return await maybe_awaitable(self.autocomplete_function(interaction))
[docs] class Option(Generic[T]): # TODO: Update docstring @Paillat-dev """Represents a selectable option for a slash command. Attributes ---------- input_type: Union[Type[:class:`str`], Type[:class:`bool`], Type[:class:`int`], Type[:class:`float`], Type[:class:`.abc.GuildChannel`], Type[:class:`Thread`], Type[:class:`Member`], Type[:class:`User`], Type[:class:`Attachment`], Type[:class:`Role`], Type[:class:`.abc.Mentionable`], :class:`SlashCommandOptionType`, Type[:class:`.ext.commands.Converter`], Type[:class:`enums.Enum`], Type[:class:`Enum`]] The type of input that is expected for this option. This can be a :class:`SlashCommandOptionType`, an associated class, a channel type, a :class:`Converter`, a converter class or an :class:`enum.Enum`. If a :class:`enum.Enum` is used and it has up to 25 values, :attr:`choices` will be automatically filled. If the :class:`enum.Enum` has more than 25 values, :attr:`autocomplete` will be implemented with :func:`discord.utils.basic_autocomplete` instead. name: :class:`str` The name of this option visible in the UI. Inherits from the variable name if not provided as a parameter. description: Optional[:class:`str`] The description of this option. Must be 100 characters or fewer. If :attr:`input_type` is a :class:`enum.Enum` and :attr:`description` is not specified, :attr:`input_type`'s docstring will be used. choices: Optional[List[Union[:class:`Any`, :class:`OptionChoice`]]] The list of available choices for this option. Can be a list of values or :class:`OptionChoice` objects (which represent a name:value pair). If provided, the input from the user must match one of the choices in the list. required: Optional[:class:`bool`] Whether this option is required. default: Optional[:class:`Any`] The default value for this option. If provided, ``required`` will be considered ``False``. min_value: Optional[:class:`int`] The minimum value that can be entered. Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`. max_value: Optional[:class:`int`] The maximum value that can be entered. Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`. min_length: Optional[:class:`int`] The minimum length of the string that can be entered. Must be between 0 and 6000 (inclusive). Only applies to Options with an :attr:`input_type` of :class:`str`. max_length: Optional[:class:`int`] The maximum length of the string that can be entered. Must be between 1 and 6000 (inclusive). Only applies to Options with an :attr:`input_type` of :class:`str`. channel_types: list[:class:`discord.ChannelType`] | None A list of channel types that can be selected in this option. Only applies to Options with an :attr:`input_type` of :class:`discord.SlashCommandOptionType.channel`. If this argument is used, :attr:`input_type` will be ignored. name_localizations: Dict[:class:`str`, :class:`str`] The name localizations for this option. The values of this should be ``"locale": "name"``. See `here <https://discord.com/developers/docs/reference#locales>`_ for a list of valid locales. description_localizations: Dict[:class:`str`, :class:`str`] The description localizations for this option. The values of this should be ``"locale": "description"``. See `here <https://discord.com/developers/docs/reference#locales>`_ for a list of valid locales. Examples -------- Basic usage: :: @bot.slash_command(guild_ids=[...]) async def hello( ctx: discord.ApplicationContext, name: Option(str, "Enter your name"), age: Option(int, "Enter your age", min_value=1, max_value=99, default=18), # passing the default value makes an argument optional # you also can create optional argument using: # age: Option(int, "Enter your age") = 18 ): await ctx.respond(f"Hello! Your name is {name} and you are {age} years old.") .. versionadded:: 2.0 """ # Overload for options with choices (str, int, or float types) @overload def __init__( self, name: str, input_type: type[T] = str, *, choices: Sequence[OptionChoice[T]], description: str | None = None, channel_types: None = None, required: bool = ..., default: Any | Undefined = ..., min_value: None = None, max_value: None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for channel options with optional channel_types filter @overload def __init__( self, name: str, input_type: type[GuildChannel | Thread] | Literal[SlashCommandOptionType.channel] = SlashCommandOptionType.channel, *, choices: None = None, description: str | None = None, channel_types: Sequence[ChannelType] | None = None, required: bool = ..., default: Any | Undefined = ..., min_value: None = None, max_value: None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for required string options with min_length/max_length constraints @overload def __init__( self, name: str, input_type: type[str] | Literal[SlashCommandOptionType.string] = str, *, description: str | None = None, choices: None = None, channel_types: None = None, required: Literal[True], default: Undefined = MISSING, min_length: int | None = None, max_length: int | None = None, min_value: None = None, max_value: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for optional string options with default value and min_length/max_length constraints @overload def __init__( self, name: str, input_type: type[str] | Literal[SlashCommandOptionType.string] = str, *, description: str | None = None, choices: None = None, channel_types: None = None, required: bool = False, default: Any, min_length: int | None = None, max_length: int | None = None, min_value: None = None, max_value: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for required integer options with min_value/max_value constraints (integers only) @overload def __init__( self, name: str, input_type: type[int] | Literal[SlashCommandOptionType.integer], *, description: str | None = None, choices: None = None, channel_types: None = None, required: Literal[True], default: Undefined = MISSING, min_value: int | None = None, max_value: int | None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for optional integer options with default value and min_value/max_value constraints (integers only) @overload def __init__( self, name: str, input_type: type[int] | Literal[SlashCommandOptionType.integer], *, description: str | None = None, choices: None = None, channel_types: None = None, required: bool = False, default: Any, min_value: int | None = None, max_value: int | None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for required float options with min_value/max_value constraints (integers or floats) @overload def __init__( self, name: str, input_type: type[float] | Literal[SlashCommandOptionType.number], *, description: str | None = None, choices: None = None, channel_types: None = None, required: Literal[True], default: Undefined = MISSING, min_value: int | float | None = None, max_value: int | float | None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for optional float options with default value and min_value/max_value constraints (integers or floats) @overload def __init__( self, name: str, input_type: type[float] | Literal[SlashCommandOptionType.number], *, description: str | None = None, choices: None = None, channel_types: None = None, required: bool = False, default: Any, min_value: int | float | None = None, max_value: int | float | None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for required options with autocomplete (no choices or min/max constraints allowed) @overload def __init__( self, name: str, input_type: type[str | int | float] = str, *, description: str | None = None, choices: None = None, channel_types: None = None, required: Literal[True], default: Undefined = MISSING, min_value: None = None, max_value: None = None, min_length: None = None, max_length: None = None, autocomplete: ApplicationCommandOptionAutocomplete, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, ) -> None: ... # Overload for optional options with autocomplete and default value (no choices or min/max constraints allowed) @overload def __init__( self, name: str, input_type: type[str | int | float] = str, *, description: str | None = None, choices: None = None, channel_types: None = None, required: bool = False, default: Any, min_value: None = None, max_value: None = None, min_length: None = None, max_length: None = None, autocomplete: ApplicationCommandOptionAutocomplete, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, ) -> None: ... # Overload for required options of other types (bool, User, Member, Role, Attachment, Mentionable, etc.) @overload def __init__( self, name: str, input_type: type[T] = str, *, description: str | None = None, choices: None = None, channel_types: None = None, required: Literal[True], default: Undefined = MISSING, min_value: None = None, max_value: None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... # Overload for optional options of other types with default value (bool, User, Member, Role, Attachment, Mentionable, etc.) @overload def __init__( self, name: str, input_type: type[T] = str, *, description: str | None = None, choices: None = None, channel_types: None = None, required: bool = False, default: Any, min_value: None = None, max_value: None = None, min_length: None = None, max_length: None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: None = None, ) -> None: ... def __init__( self, name: str, input_type: InputType | type[T] = str, *, description: str | None = None, choices: Sequence[OptionChoice[T]] | None = None, channel_types: Sequence[ChannelType] | None = None, required: bool = True, default: Any | Undefined = MISSING, min_value: int | float | None = None, max_value: int | float | None = None, min_length: int | None = None, max_length: int | None = None, name_localizations: dict[str, str] | None = None, description_localizations: dict[str, str] | None = None, autocomplete: ApplicationCommandOptionAutocomplete | None = None, ) -> None: self.name: str = name self.description: str | None = description self.choices: list[OptionChoice[T]] | None = list(choices) if choices is not None else None if self.choices is not None: if len(self.choices) > 25: raise ValueError("Option choices cannot exceed 25 items.") if not issubclass(input_type, str | int | float): raise TypeError("Option choices can only be used with str, int, or float input types.") self.channel_types: list[ChannelType] | None = list(channel_types) if channel_types is not None else None self.input_type: SlashCommandOptionType if isinstance(input_type, SlashCommandOptionType): self.input_type = input_type elif issubclass(input_type, str): self.input_type = SlashCommandOptionType.string elif issubclass(input_type, bool): self.input_type = SlashCommandOptionType.boolean elif issubclass(input_type, int): self.input_type = SlashCommandOptionType.integer elif issubclass(input_type, float): self.input_type = SlashCommandOptionType.number elif issubclass(input_type, Attachment): self.input_type = SlashCommandOptionType.attachment elif issubclass(input_type, User | Member): self.input_type = SlashCommandOptionType.user elif issubclass(input_type, Role): self.input_type = SlashCommandOptionType.role elif issubclass(input_type, GuildChannel | Thread): self.input_type = SlashCommandOptionType.channel elif issubclass(input_type, Mentionable): self.input_type = SlashCommandOptionType.mentionable self.required: bool = required if default is MISSING else False self.default: Any | Undefined = default self.autocomplete: ApplicationCommandOptionAutocomplete | None = autocomplete self.min_value: int | float | None = min_value self.max_value: int | float | None = max_value if self.input_type not in (SlashCommandOptionType.integer, SlashCommandOptionType.number) and ( self.min_value is not None or self.max_value is not None ): raise TypeError( f"min_value and max_value can only be used with int or float input types, not {self.input_type.name}" ) if self.input_type is not SlashCommandOptionType.integer and ( isinstance(self.min_value, float) or isinstance(self.max_value, float) ): raise TypeError("min_value and max_value must be integers when input_type is integer") self.min_length: int | None = min_length self.max_length: int | None = max_length if self.input_type is not SlashCommandOptionType.string and ( self.min_length is not None or self.max_length is not None ): raise TypeError( f"min_length and max_length can only be used with str input type, not {self.input_type.name}" ) self.name_localizations: dict[str, str] | None = name_localizations self.description_localizations: dict[str, str] | None = description_localizations def to_dict(self) -> dict[str, Any]: as_dict: dict[str, Any] = { "name": self.name, "description": self.description, "type": self.input_type.value, "required": self.required, "autocomplete": bool(self.autocomplete), } if self.choices: as_dict["choices"] = [choice.to_dict() for choice in self.choices] if self.name_localizations: as_dict["name_localizations"] = self.name_localizations if self.description_localizations: as_dict["description_localizations"] = self.description_localizations if self.channel_types: as_dict["channel_types"] = [t.value for t in self.channel_types] if self.min_value is not None: as_dict["min_value"] = self.min_value if self.max_value is not None: as_dict["max_value"] = self.max_value if self.min_length is not None: as_dict["min_length"] = self.min_length if self.max_length is not None: as_dict["max_length"] = self.max_length return as_dict @override def __repr__(self): return f"<Option name={self.name!r} input_type={self.input_type} required={self.required}>"
[docs] class OptionChoice(Generic[T]): """ Represents a name:value pairing for a selected :class:`.Option`. .. versionadded:: 2.0 Attributes ---------- name: :class:`str` The name of the choice. Shown in the UI when selecting an option. value: :class:`str` | :class:`int` | :class:`float` The value of the choice. If not provided, will use the value of ``name``. name_localizations: dict[:class:`str`, :class:`str`] The name localizations for this choice. The values of this should be ``"locale": "name"``. See `here <https://discord.com/developers/docs/reference#locales>`_ for a list of valid locales. """ def __init__( self, name: str, value: T | None = None, name_localizations: dict[str, str] | None = None, ): self.name: str = str(name) self.value: T = value if value is not None else name # pyright: ignore [reportAttributeAccessIssue] self.name_localizations: dict[str, str] | None = name_localizations def to_dict(self) -> dict[str, Any]: as_dict: dict[str, Any] = {"name": self.name, "value": self.value} if self.name_localizations is not None: as_dict["name_localizations"] = self.name_localizations return as_dict