# SPDX-License-Identifier: MIT
from __future__ import annotations
import inspect
from enum import Enum
from typing import TYPE_CHECKING, Literal, Optional, Type, Union
from ..abc import GuildChannel, Mentionable
from ..channel import (
CategoryChannel,
DMChannel,
ForumChannel,
StageChannel,
TextChannel,
Thread,
VoiceChannel,
)
from ..enums import ChannelType
from ..enums import Enum as DiscordEnum
from ..enums import SlashCommandOptionType
from ..utils import MISSING
if TYPE_CHECKING:
from ..ext.commands import Converter
from ..member import Member
from ..message import Attachment
from ..role import Role
from ..user import User
InputType = Union[
Type[str],
Type[bool],
Type[int],
Type[float],
Type[GuildChannel],
Type[Thread],
Type[Member],
Type[User],
Type[Attachment],
Type[Role],
Type[Mentionable],
SlashCommandOptionType,
Converter,
Type[Converter],
Type[Enum],
Type[DiscordEnum],
]
__all__ = (
"ThreadOption",
"Option",
"OptionChoice",
"option",
)
CHANNEL_TYPE_MAP = {
TextChannel: ChannelType.text,
VoiceChannel: ChannelType.voice,
StageChannel: ChannelType.stage_voice,
CategoryChannel: ChannelType.category,
Thread: ChannelType.public_thread,
ForumChannel: ChannelType.forum,
DMChannel: ChannelType.private,
}
[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]
[docs]class Option:
"""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`.
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.
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`.
autocomplete: Optional[:class:`Any`]
The autocomplete handler for the option. Accepts an iterable of :class:`str` or :class:`OptionChoice`, a callable (sync or async)
that takes a single argument of :class:`AutocompleteContext`, or a coroutine.
Must resolve to an iterable of :class:`str` or :class:`OptionChoice`.
.. note::
Does not validate the input value against the autocomplete results.
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
"""
input_type: SlashCommandOptionType
converter: Converter | type[Converter] | None = None
def __init__(
self, input_type: InputType = str, /, description: str | None = None, **kwargs
) -> None:
self.name: str | None = kwargs.pop("name", None)
if self.name is not None:
self.name = str(self.name)
self._parameter_name = self.name # default
self._raw_type: InputType | tuple = input_type
enum_choices = []
input_type_is_class = isinstance(input_type, type)
if input_type_is_class and issubclass(input_type, (Enum, DiscordEnum)):
if description is None:
description = inspect.getdoc(input_type)
enum_choices = [OptionChoice(e.name, e.value) for e in input_type]
value_class = enum_choices[0].value.__class__
if all(isinstance(elem.value, value_class) for elem in enum_choices):
input_type = SlashCommandOptionType.from_datatype(
enum_choices[0].value.__class__
)
else:
enum_choices = [OptionChoice(e.name, str(e.value)) for e in input_type]
input_type = SlashCommandOptionType.string
self.description = description or "No description provided"
self.channel_types: list[ChannelType] = kwargs.pop("channel_types", [])
if isinstance(input_type, SlashCommandOptionType):
self.input_type = input_type
else:
from ..ext.commands import Converter
if (
isinstance(input_type, Converter)
or input_type_is_class
and issubclass(input_type, Converter)
):
self.converter = input_type
self._raw_type = str
self.input_type = SlashCommandOptionType.string
else:
try:
self.input_type = SlashCommandOptionType.from_datatype(input_type)
except TypeError as exc:
from ..ext.commands.converter import CONVERTER_MAPPING
if input_type not in CONVERTER_MAPPING:
raise exc
self.converter = CONVERTER_MAPPING[input_type]
self._raw_type = str
self.input_type = SlashCommandOptionType.string
else:
if self.input_type == SlashCommandOptionType.channel:
if not isinstance(self._raw_type, tuple):
if hasattr(input_type, "__args__"):
self._raw_type = input_type.__args__ # type: ignore # Union.__args__
else:
self._raw_type = (input_type,)
if not self.channel_types:
self.channel_types = [
CHANNEL_TYPE_MAP[t]
for t in self._raw_type
if t is not GuildChannel
]
self.required: bool = (
kwargs.pop("required", True) if "default" not in kwargs else False
)
self.default = kwargs.pop("default", None)
self.choices: list[OptionChoice] = enum_choices or [
o if isinstance(o, OptionChoice) else OptionChoice(o)
for o in kwargs.pop("choices", [])
]
if self.input_type == SlashCommandOptionType.integer:
minmax_types = (int, type(None))
minmax_typehint = Optional[int]
elif self.input_type == SlashCommandOptionType.number:
minmax_types = (int, float, type(None))
minmax_typehint = Optional[Union[int, float]]
else:
minmax_types = (type(None),)
minmax_typehint = type(None)
if self.input_type == SlashCommandOptionType.string:
minmax_length_types = (int, type(None))
minmax_length_typehint = Optional[int]
else:
minmax_length_types = (type(None),)
minmax_length_typehint = type(None)
self.min_value: int | float | None = kwargs.pop("min_value", None)
self.max_value: int | float | None = kwargs.pop("max_value", None)
self.min_length: int | None = kwargs.pop("min_length", None)
self.max_length: int | None = kwargs.pop("max_length", None)
if (
self.input_type != SlashCommandOptionType.integer
and self.input_type != SlashCommandOptionType.number
and (self.min_value or self.max_value)
):
raise AttributeError(
"Option does not take min_value or max_value if not of type "
"SlashCommandOptionType.integer or SlashCommandOptionType.number"
)
if self.input_type != SlashCommandOptionType.string and (
self.min_length or self.max_length
):
raise AttributeError(
"Option does not take min_length or max_length if not of type str"
)
if self.min_value is not None and not isinstance(self.min_value, minmax_types):
raise TypeError(
f"Expected {minmax_typehint} for min_value, got"
f' "{type(self.min_value).__name__}"'
)
if self.max_value is not None and not isinstance(self.max_value, minmax_types):
raise TypeError(
f"Expected {minmax_typehint} for max_value, got"
f' "{type(self.max_value).__name__}"'
)
if self.min_length is not None:
if not isinstance(self.min_length, minmax_length_types):
raise TypeError(
f"Expected {minmax_length_typehint} for min_length,"
f' got "{type(self.min_length).__name__}"'
)
if self.min_length < 0 or self.min_length > 6000:
raise AttributeError(
"min_length must be between 0 and 6000 (inclusive)"
)
if self.max_length is not None:
if not isinstance(self.max_length, minmax_length_types):
raise TypeError(
f"Expected {minmax_length_typehint} for max_length,"
f' got "{type(self.max_length).__name__}"'
)
if self.max_length < 1 or self.max_length > 6000:
raise AttributeError("max_length must between 1 and 6000 (inclusive)")
self.autocomplete = kwargs.pop("autocomplete", None)
self.name_localizations = kwargs.pop("name_localizations", MISSING)
self.description_localizations = kwargs.pop(
"description_localizations", MISSING
)
def to_dict(self) -> dict:
as_dict = {
"name": self.name,
"description": self.description,
"type": self.input_type.value,
"required": self.required,
"choices": [c.to_dict() for c in self.choices],
"autocomplete": bool(self.autocomplete),
}
if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
if self.description_localizations is not MISSING:
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
def __repr__(self):
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"
[docs]class OptionChoice:
"""
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: Optional[Union[: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: str | int | float | None = None,
name_localizations: dict[str, str] = MISSING,
):
self.name = str(name)
self.value = value if value is not None else name
self.name_localizations = name_localizations
def to_dict(self) -> dict[str, str | int | float]:
as_dict = {"name": self.name, "value": self.value}
if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
return as_dict
[docs]def option(name, type=None, **kwargs):
"""A decorator that can be used instead of typehinting :class:`.Option`.
.. versionadded:: 2.0
Attributes
----------
parameter_name: :class:`str`
The name of the target parameter this option is mapped to.
This allows you to have a separate UI ``name`` and parameter name.
"""
def decorator(func):
nonlocal type
type = type or func.__annotations__.get(name, str)
if parameter := kwargs.get("parameter_name"):
func.__annotations__[parameter] = Option(type, name=name, **kwargs)
else:
func.__annotations__[name] = Option(type, **kwargs)
return func
return decorator