# The MIT License (MIT) # Copyright (c) 2021-present EQUENOS # 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. """Repsonsible for handling Params for slash commands""" from __future__ import annotations import asyncio import collections.abc import inspect import itertools import math import sys from enum import Enum, EnumMeta from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, Dict, Final, FrozenSet, List, Literal, Optional, Tuple, Type, TypeVar, Union, get_args, get_origin, get_type_hints, overload, ) import disnake from disnake.app_commands import Option, OptionChoice from disnake.channel import _channel_type_factory from disnake.enums import ChannelType, OptionType, try_enum_to_int from disnake.ext import commands from disnake.i18n import Localized from disnake.interactions import CommandInteraction from disnake.utils import maybe_coroutine from . import errors from .converter import CONVERTER_MAPPING if TYPE_CHECKING: from disnake.app_commands import Choices from disnake.i18n import LocalizationValue, LocalizedOptional from disnake.types.interactions import ApplicationCommandOptionChoiceValue from .slash_core import InvokableSlashCommand, SubCommand AnySlashCommand = Union[InvokableSlashCommand, SubCommand] from typing_extensions import TypeGuard TChoice = TypeVar("TChoice", bound=ApplicationCommandOptionChoiceValue) if sys.version_info >= (3, 10): from types import UnionType else: UnionType = object() T = TypeVar("T", bound=Any) TypeT = TypeVar("TypeT", bound=Type[Any]) CallableT = TypeVar("CallableT", bound=Callable[..., Any]) __all__ = ( "Range", "LargeInt", "ParamInfo", "Param", "param", "inject", "option_enum", "register_injection", "converter_method", ) def issubclass_(obj: Any, tp: Union[TypeT, Tuple[TypeT, ...]]) -> TypeGuard[TypeT]: if not isinstance(tp, (type, tuple)): return False elif not isinstance(obj, type): # Assume we have a type hint if get_origin(obj) in (Union, UnionType, Optional): obj = get_args(obj) return any(issubclass(o, tp) for o in obj) else: # Other type hint specializations are not supported return False return issubclass(obj, tp) def remove_optionals(annotation: Any) -> Any: """remove unwanted optionals from an annotation""" if get_origin(annotation) in (Union, UnionType): args = tuple(i for i in annotation.__args__ if i not in (None, type(None))) if len(args) == 1: annotation = args[0] else: annotation = Union[args] # type: ignore return annotation def signature(func: Callable) -> inspect.Signature: """Get the signature with evaluated annotations wherever possible This is equivalent to `signature(..., eval_str=True)` in python 3.10 """ if sys.version_info >= (3, 10): return inspect.signature(func, eval_str=True) if inspect.isfunction(func) or inspect.ismethod(func): typehints = get_type_hints(func) else: typehints = get_type_hints(func.__call__) signature = inspect.signature(func) parameters = [] for name, param in signature.parameters.items(): if isinstance(param.annotation, str): param = param.replace(annotation=typehints.get(name, inspect.Parameter.empty)) if param.annotation is type(None): param = param.replace(annotation=None) parameters.append(param) return_annotation = typehints.get("return", inspect.Parameter.empty) if return_annotation is type(None): return_annotation = None return signature.replace(parameters=parameters, return_annotation=return_annotation) def _xt_to_xe(xe: Optional[float], xt: Optional[float], direction: float = 1) -> Optional[float]: """Function for combining xt and xe * x > xt && x >= xe ; x >= f(xt, xe, 1) * x < xt && x <= xe ; x <= f(xt, xe, -1) """ if xe is not None: if xt is not None: raise TypeError("Cannot combine lt and le or gt and le") return xe elif xt is not None: epsilon = math.ldexp(1.0, -1024) return xt + (epsilon * direction) else: return None class Injection: _registered: ClassVar[Dict[Any, Injection]] = {} function: Callable def __init__(self, function: Callable) -> None: self.function = function @classmethod def register(cls, function: CallableT, annotation: Any) -> CallableT: self = cls(function) cls._registered[annotation] = self return function class RangeMeta(type): """Custom Generic implementation for Range""" @overload def __getitem__(self, args: Tuple[Union[int, ellipsis], Union[int, ellipsis]]) -> Type[int]: ... @overload def __getitem__( self, args: Tuple[Union[float, ellipsis], Union[float, ellipsis]] ) -> Type[float]: ... def __getitem__(self, args: Tuple[Any, ...]) -> Any: a, b = [None if x is Ellipsis else x for x in args] return Range.create(min_value=a, max_value=b) class Range(type, metaclass=RangeMeta): """Type depicting a limited range of allowed values. See :ref:`param_ranges` for more information. .. versionadded:: 2.4 """ min_value: Optional[float] max_value: Optional[float] @overload @classmethod def create( cls, min_value: int = None, max_value: int = None, *, le: int = None, lt: int = None, ge: int = None, gt: int = None, ) -> Type[int]: ... @overload @classmethod def create( cls, min_value: float = None, max_value: float = None, *, le: float = None, lt: float = None, ge: float = None, gt: float = None, ) -> Type[float]: ... @classmethod def create( cls, min_value: float = None, max_value: float = None, *, le: float = None, lt: float = None, ge: float = None, gt: float = None, ) -> Any: """Construct a new range with any possible constraints""" self = cls(cls.__name__, (), {}) self.min_value = min_value if min_value is not None else _xt_to_xe(le, lt, -1) self.max_value = max_value if max_value is not None else _xt_to_xe(ge, gt, 1) return self @property def underlying_type(self) -> Union[Type[int], Type[float]]: if isinstance(self.min_value, float) or isinstance(self.max_value, float): return float return int def __repr__(self) -> str: a = "..." if self.min_value is None else self.min_value b = "..." if self.max_value is None else self.max_value return f"{type(self).__name__}[{a}, {b}]" class LargeInt(int): """Type for large integers in slash commands.""" # option types that require additional handling in verify_type _VERIFY_TYPES: Final[FrozenSet[OptionType]] = frozenset((OptionType.user, OptionType.mentionable)) class ParamInfo: """A class that basically connects function params with slash command options. The instances of this class are not created manually, but via the functional interface instead. See :func:`Param`. Parameters ---------- default: Any The actual default value for the corresponding function param. name: Optional[Union[:class:`str`, :class:`.Localized`]] The name of this slash command option. .. versionchanged:: 2.5 Added support for localizations. description: Optional[Union[:class:`str`, :class:`.Localized`]] The description of this slash command option. .. versionchanged:: 2.5 Added support for localizations. choices: Union[List[:class:`.OptionChoice`], List[Union[:class:`str`, :class:`int`]], Dict[:class:`str`, Union[:class:`str`, :class:`int`]]] The list of choices of this slash command option. ge: :class:`float` The lowest allowed value for this option. le: :class:`float` The greatest allowed value for this option. type: Any The type of the parameter. channel_types: List[:class:`.ChannelType`] The list of channel types supported by this slash command option. autocomplete: Callable[[:class:`.ApplicationCommandInteraction`, :class:`str`], Any] The function that will suggest possible autocomplete options while typing. converter: Callable[[:class:`.ApplicationCommandInteraction`, Any], Any] The function that will convert the original input to a desired format. """ TYPES: ClassVar[Dict[type, int]] = { # fmt: off str: OptionType.string.value, int: OptionType.integer.value, bool: OptionType.boolean.value, disnake.abc.User: OptionType.user.value, disnake.User: OptionType.user.value, disnake.Member: OptionType.user.value, Union[disnake.User, disnake.Member]: OptionType.user.value, # channels handled separately disnake.abc.GuildChannel: OptionType.channel.value, disnake.Role: OptionType.role.value, Union[disnake.Member, disnake.Role]: OptionType.mentionable.value, disnake.abc.Snowflake: OptionType.mentionable.value, float: OptionType.number.value, disnake.Attachment: OptionType.attachment.value, # fmt: on } _registered_converters: ClassVar[Dict[type, Callable]] = {} def __init__( self, default: Any = ..., *, name: LocalizedOptional = None, description: LocalizedOptional = None, converter: Callable[[CommandInteraction, Any], Any] = None, convert_default: bool = False, autcomplete: Callable[[CommandInteraction, str], Any] = None, choices: Choices = None, type: type = None, channel_types: List[ChannelType] = None, lt: float = None, le: float = None, gt: float = None, ge: float = None, large: bool = False, ) -> None: name_loc = Localized._cast(name, False) self.name: str = name_loc.string or "" self.name_localizations: LocalizationValue = name_loc.localizations desc_loc = Localized._cast(description, False) self.description: Optional[str] = desc_loc.string self.description_localizations: LocalizationValue = desc_loc.localizations self.default = default self.param_name: str = self.name self.converter = converter self.convert_default = convert_default self.autocomplete = autcomplete self.choices = choices or [] self.type = type or str self.channel_types = channel_types or [] self.max_value = _xt_to_xe(le, lt, -1) self.min_value = _xt_to_xe(ge, gt, 1) self.large = large @property def required(self) -> bool: return self.default is Ellipsis @property def discord_type(self) -> OptionType: return OptionType(self.TYPES.get(self.type, OptionType.string.value)) @discord_type.setter def discord_type(self, discord_type: OptionType) -> None: value = try_enum_to_int(discord_type) for t, v in self.TYPES.items(): if value == v: self.type = t return raise TypeError(f"Type {discord_type} is not a valid Param type") @classmethod def from_param( cls, param: inspect.Parameter, type_hints: Dict[str, Any], parsed_docstring: Dict[str, disnake.utils._DocstringParam] = None, ) -> ParamInfo: # hopefully repeated parsing won't cause any problems parsed_docstring = parsed_docstring or {} if isinstance(param.default, cls): self = param.default else: default = param.default if param.default is not inspect.Parameter.empty else ... self = cls(default) self.parse_parameter(param) doc = parsed_docstring.get(param.name) if doc: self.parse_doc(doc) self.parse_annotation(type_hints.get(param.name, param.annotation)) return self @classmethod def register_converter(cls, annotation: Any, converter: CallableT) -> CallableT: cls._registered_converters[annotation] = converter return converter def __repr__(self) -> str: args = ", ".join(f"{k}={'...' if v is ... else repr(v)}" for k, v in vars(self).items()) return f"{type(self).__name__}({args})" async def get_default(self, inter: CommandInteraction) -> Any: """Gets the default for an interaction""" default = self.default if callable(self.default): default = self.default(inter) if inspect.isawaitable(default): default = await default if self.convert_default: default = await self.convert_argument(inter, default) return default async def verify_type(self, inter: CommandInteraction, argument: Any) -> Any: """Check if the type of an argument is correct and possibly raise if it's not.""" if self.discord_type not in _VERIFY_TYPES: return argument # The API may return a `User` for options annotated with `Member`, # including `Member` (user option), `Union[User, Member]` (user option) and # `Union[Member, Role]` (mentionable option). # If we received a `User` but didn't expect one, raise. if ( isinstance(argument, disnake.User) and issubclass_(self.type, disnake.Member) and not issubclass_(self.type, disnake.User) ): raise errors.MemberNotFound(str(argument.id)) return argument async def convert_argument(self, inter: CommandInteraction, argument: Any) -> Any: """Convert a value if a converter is given""" if self.large: try: argument = int(argument) except ValueError as e: raise errors.LargeIntConversionFailure(argument) from None if self.converter is None: # TODO: Custom validators return await self.verify_type(inter, argument) try: argument = self.converter(inter, argument) if inspect.isawaitable(argument): return await argument return argument except errors.CommandError: raise except Exception as e: raise errors.ConversionError(self.converter, e) from e def _parse_enum(self, annotation: Any) -> None: if isinstance(annotation, (EnumMeta, disnake.enums.EnumMeta)): self.choices = [OptionChoice(name, value.value) for name, value in annotation.__members__.items()] # type: ignore else: self.choices = [OptionChoice(str(i), i) for i in annotation.__args__] self.type = type(self.choices[0].value) def _parse_guild_channel( self, *channels: Union[Type[disnake.abc.GuildChannel], Type[disnake.Thread]] ) -> None: # this variable continues to be GuildChannel because the type is still # determined from the TYPE mapping in the class definition self.type = disnake.abc.GuildChannel if not self.channel_types: channel_types = set() for channel in channels: channel_types.update(_channel_type_factory(channel)) self.channel_types = list(channel_types) def parse_annotation(self, annotation: Any, converter_mode: bool = False) -> bool: """Parse an annotation""" annotation = remove_optionals(annotation) if not converter_mode: self.converter = ( self.converter or getattr(annotation, "__discord_converter__", None) or self._registered_converters.get(annotation) ) if self.converter: self.parse_converter_annotation(self.converter, annotation) return True # short circuit if user forgot to provide annotations if annotation is inspect.Parameter.empty or annotation is Any: return False # resolve type aliases if isinstance(annotation, Range): self.min_value = annotation.min_value self.max_value = annotation.max_value annotation = annotation.underlying_type if issubclass_(annotation, LargeInt): self.large = True annotation = int if self.large: self.type = str if annotation is not int: raise TypeError("Large integers must be annotated with int or LargeInt") elif annotation in self.TYPES: self.type = annotation elif ( isinstance(annotation, (EnumMeta, disnake.enums.EnumMeta)) or get_origin(annotation) is Literal ): self._parse_enum(annotation) elif get_origin(annotation) in (Union, UnionType): args = annotation.__args__ if all( issubclass_(channel, (disnake.abc.GuildChannel, disnake.Thread)) for channel in args ): self._parse_guild_channel(*args) else: raise TypeError( "Unions for anything else other than channels or a mentionable are not supported" ) elif issubclass_(annotation, (disnake.abc.GuildChannel, disnake.Thread)): self._parse_guild_channel(annotation) elif issubclass_(get_origin(annotation), collections.abc.Sequence): raise TypeError( f"List arguments have not been implemented yet and therefore {annotation!r} is invalid" ) elif annotation in CONVERTER_MAPPING: if converter_mode: raise TypeError( f"{annotation!r} implies the usage of a converter but those cannot be nested" ) self.converter = CONVERTER_MAPPING[annotation]().convert elif converter_mode: raise TypeError(f"{annotation!r} is not a valid converter annotation") else: raise TypeError(f"{annotation!r} is not a valid parameter annotation") return True def parse_converter_annotation(self, converter: Callable, fallback_annotation: Any) -> None: _, parameters = isolate_self(converter) if len(parameters) != 1: raise TypeError( "Converters must take precisely two arguments: the interaction and the argument" ) _, parameter = parameters.popitem() annotation = parameter.annotation if parameter.default is not inspect.Parameter.empty and self.required: self.default = parameter.default self.convert_default = True success = self.parse_annotation(annotation, converter_mode=True) if success: return success = self.parse_annotation(fallback_annotation, converter_mode=True) if success: return raise TypeError( f"Both the converter annotation {annotation!r} and the option annotation {fallback_annotation!r} are invalid" ) def parse_parameter(self, param: inspect.Parameter) -> None: self.name = self.name or param.name self.param_name = param.name def parse_doc(self, doc: disnake.utils._DocstringParam) -> None: if self.type == str and doc["type"] is not None: self.parse_annotation(doc["type"]) self.description = self.description or doc["description"] self.name_localizations._upgrade(doc["localization_key_name"]) self.description_localizations._upgrade(doc["localization_key_desc"]) def to_option(self) -> Option: if not self.name: raise TypeError("Param must be parsed first") name = Localized(self.name, data=self.name_localizations) desc = Localized(self.description, data=self.description_localizations) return Option( name=name, description=desc, type=self.discord_type, required=self.required, choices=self.choices or None, channel_types=self.channel_types, autocomplete=self.autocomplete is not None, min_value=self.min_value, max_value=self.max_value, ) def safe_call(function: Callable[..., T], /, *possible_args: Any, **possible_kwargs: Any) -> T: """Calls a function without providing any extra unexpected arguments""" MISSING: Any = object() sig = signature(function) kinds = {p.kind for p in sig.parameters.values()} arb = {inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD} if arb.issubset(kinds): raise TypeError( "Cannot safely call a function with both *args and **kwargs. " "If this is a wrapper please use functools.wraps to keep the signature correct" ) parsed_pos = False args: List[Any] = [] kwargs: Dict[str, Any] = {} for index, parameter, posarg in itertools.zip_longest( itertools.count(), sig.parameters.values(), possible_args, fillvalue=MISSING, ): if parameter is MISSING: break if posarg is MISSING: parsed_pos = True if parameter.kind is inspect.Parameter.VAR_POSITIONAL: args = list(possible_args) parsed_pos = True elif parameter.kind is inspect.Parameter.VAR_KEYWORD: kwargs = possible_kwargs break elif parameter.kind is inspect.Parameter.KEYWORD_ONLY: parsed_pos = True if not parsed_pos: args.append(possible_args[index]) elif parameter.kind is inspect.Parameter.POSITIONAL_ONLY: break # guaranteed error since not enough positional arguments elif parameter.name in possible_kwargs: kwargs[parameter.name] = possible_kwargs[parameter.name] return function(*args, **kwargs) def isolate_self( function: Callable, ) -> Tuple[Tuple[Optional[inspect.Parameter], ...], Dict[str, inspect.Parameter]]: """Create parameters without self and the first interaction""" sig = signature(function) parameters = dict(sig.parameters) parametersl = list(sig.parameters.values()) if not parameters: return (None, None), {} cog_param: Optional[inspect.Parameter] = None inter_param: Optional[inspect.Parameter] = None if parametersl[0].name == "self": cog_param = parameters.pop(parametersl[0].name) parametersl.pop(0) if parametersl: annot = parametersl[0].annotation if issubclass_(annot, CommandInteraction) or annot is inspect.Parameter.empty: inter_param = parameters.pop(parametersl[0].name) return (cog_param, inter_param), parameters def collect_params( function: Callable, ) -> Tuple[Optional[str], Optional[str], List[ParamInfo], Dict[str, Injection]]: """Collect all parameters in a function Returns: (`cog parameter`, `interaction parameter`, `param infos`, `injections`) """ (cog_param, inter_param), parameters = isolate_self(function) doc = disnake.utils.parse_docstring(function)["params"] paraminfos: List[ParamInfo] = [] injections: Dict[str, Injection] = {} for parameter in parameters.values(): if parameter.kind in [parameter.VAR_POSITIONAL, parameter.VAR_KEYWORD]: continue if parameter.kind is parameter.POSITIONAL_ONLY: raise TypeError("Positional-only parameters cannot be used in commands") default = parameter.default if isinstance(default, Injection): injections[parameter.name] = default elif parameter.annotation in Injection._registered: injections[parameter.name] = Injection._registered[parameter.annotation] elif issubclass_(parameter.annotation, CommandInteraction): if inter_param is None: inter_param = parameter else: raise TypeError( f"Found two candidates for the interaction parameter in {function!r}: {inter_param.name} and {parameter.name}" ) elif issubclass_(parameter.annotation, commands.Cog): if cog_param is None: cog_param = parameter else: raise TypeError( f"Found two candidates for the cog parameter in {function!r}: {cog_param.name} and {parameter.name}" ) else: paraminfo = ParamInfo.from_param(parameter, {}, doc) paraminfos.append(paraminfo) return ( cog_param.name if cog_param else None, inter_param.name if inter_param else None, paraminfos, injections, ) def collect_nested_params(function: Callable) -> List[ParamInfo]: """Collect all options from a function""" # TODO: Have these be actually sorted properly and not have injections always at the end _, _, paraminfos, injections = collect_params(function) for injection in injections.values(): paraminfos += collect_nested_params(injection.function) return sorted(paraminfos, key=lambda param: not param.required) def format_kwargs( interaction: CommandInteraction, cog_param: str = None, inter_param: str = None, /, *args: Any, **kwargs: Any, ) -> Dict[str, Any]: """Create kwargs from appropriate information""" first = args[0] if args else None if len(args) > 1: raise TypeError( "When calling a slash command only self and the interaction should be positional" ) elif first and not isinstance(first, commands.Cog): raise TypeError("Method slash commands may be created only in cog subclasses") cog: Optional[commands.Cog] = first if cog_param: kwargs[cog_param] = cog if inter_param: kwargs[inter_param] = interaction return kwargs async def run_injections( injections: Dict[str, Injection], interaction: CommandInteraction, /, *args: Any, **kwargs: Any ) -> Dict[str, Any]: """Run and resolve a list of injections""" async def _helper(name: str, injection: Injection) -> Tuple[str, Any]: return name, await call_param_func(injection.function, interaction, *args, **kwargs) resolved = await asyncio.gather(*(_helper(name, i) for name, i in injections.items())) return dict(resolved) async def call_param_func( function: Callable, interaction: CommandInteraction, /, *args: Any, **kwargs: Any ) -> Any: """Call a function utilizing ParamInfo""" cog_param, inter_param, paraminfos, injections = collect_params(function) formatted_kwargs = format_kwargs(interaction, cog_param, inter_param, *args, **kwargs) formatted_kwargs.update(await run_injections(injections, interaction, *args, **kwargs)) kwargs = formatted_kwargs for param in paraminfos: if param.param_name in kwargs: kwargs[param.param_name] = await param.convert_argument( interaction, kwargs[param.param_name] ) elif param.default is not ...: kwargs[param.param_name] = await param.get_default(interaction) return await maybe_coroutine(safe_call, function, **kwargs) def expand_params(command: AnySlashCommand) -> List[Option]: """Update an option with its params *in-place* Returns the created options """ sig = signature(command.callback) _, inter_param, params, injections = collect_params(command.callback) if inter_param is None: raise TypeError(f"Couldn't find an interaction parameter in {command.callback}") for injection in injections.values(): params += collect_nested_params(injection.function) params = sorted(params, key=lambda param: not param.required) # update connectors and autocompleters for param in params: if param.name != param.param_name: command.connectors[param.name] = param.param_name if param.autocomplete: command.autocompleters[param.name] = param.autocomplete if issubclass_(sig.parameters[inter_param].annotation, disnake.GuildCommandInteraction): command._guild_only = True return [param.to_option() for param in params] def Param( default: Any = ..., *, name: LocalizedOptional = None, description: LocalizedOptional = None, choices: Choices = None, converter: Callable[[CommandInteraction, Any], Any] = None, convert_defaults: bool = False, autocomplete: Callable[[CommandInteraction, str], Any] = None, channel_types: List[ChannelType] = None, lt: float = None, le: float = None, gt: float = None, ge: float = None, large: bool = False, **kwargs: Any, ) -> Any: """A special function that creates an instance of :class:`ParamInfo` that contains some information about a slash command option. This instance should be assigned to a parameter of a function representing your slash command. See :ref:`param_syntax` for more info. Parameters ---------- default: Any The actual default value of the function parameter that should be passed instead of the :class:`ParamInfo` instance. name: Optional[Union[:class:`str`, :class:`.Localized`]] The name of the option. By default, the option name is the parameter name. .. versionchanged:: 2.5 Added support for localizations. description: Optional[Union[:class:`str`, :class:`.Localized`]] The description of the option. You can skip this kwarg and use docstrings. See :ref:`param_syntax`. Kwarg aliases: ``desc``. .. versionchanged:: 2.5 Added support for localizations. choices: Union[List[:class:`.OptionChoice`], List[Union[:class:`str`, :class:`int`]], Dict[:class:`str`, Union[:class:`str`, :class:`int`]]] A list of choices for this option. converter: Callable[[:class:`.ApplicationCommandInteraction`, Any], Any] A function that will convert the original input to a desired format. Kwarg aliases: ``conv``. convert_defaults: :class:`bool` Whether to also apply the converter to the provided default value. Defaults to ``False``. .. versionadded:: 2.3 autocomplete: Callable[[:class:`.ApplicationCommandInteraction`, :class:`str`], Any] A function that will suggest possible autocomplete options while typing. See :ref:`param_syntax`. Kwarg aliases: ``autocomp``. channel_types: Iterable[:class:`.ChannelType`] A list of channel types that should be allowed. By default these are discerned from the annotation. lt: :class:`float` The (exclusive) upper bound of values for this option (less-than). le: :class:`float` The (inclusive) upper bound of values for this option (less-than-or-equal). Kwarg aliases: ``max_value``. gt: :class:`float` The (exclusive) lower bound of values for this option (greater-than). ge: :class:`float` The (inclusive) lower bound of values for this option (greater-than-or-equal). Kwarg aliases: ``min_value``. large: :class:`bool` Whether to accept large :class:`int` values (if this is ``False``, only values in the range ``(-2^53, 2^53)`` would be accepted due to an API limitation). .. versionadded:: 2.3 Raises ------ TypeError Unexpected keyword arguments were provided. Returns ------- :class:`ParamInfo` An instance with the option info. """ description = kwargs.pop("desc", description) converter = kwargs.pop("conv", converter) autocomplete = kwargs.pop("autocomp", autocomplete) le = kwargs.pop("max_value", le) ge = kwargs.pop("min_value", ge) if kwargs: a = ", ".join(map(repr, kwargs)) raise TypeError(f"Param() got unexpected keyword arguments: {a}") return ParamInfo( default, name=name, description=description, choices=choices, converter=converter, convert_default=convert_defaults, autcomplete=autocomplete, channel_types=channel_types, lt=lt, le=le, gt=gt, ge=ge, large=large, ) param = Param def inject(function: Callable[..., Any]) -> Any: """A special function to use the provided function for injections. This should be assigned to a parameter of a function representing your slash command. .. versionadded:: 2.3 """ return Injection(function) def option_enum( choices: Union[Dict[str, TChoice], List[TChoice]], **kwargs: TChoice ) -> Type[TChoice]: """ A utility function to create an enum type. Returns a new :class:`~enum.Enum` based on the provided parameters. .. versionadded:: 2.1 Parameters ---------- choices: Union[Dict[:class:`str`, :class:`Any`], List[:class:`Any`]] A name/value mapping of choices, or a list of values whose stringified representations will be used as the names. **kwargs Name/value pairs to use instead of the ``choices`` parameter. """ if isinstance(choices, list): choices = {str(i): i for i in choices} choices = choices or kwargs first, *_ = choices.values() return Enum("", choices, type=type(first)) class ConverterMethod(classmethod): """A class to help register a method as a converter method.""" def __set_name__(self, owner: Any, name: str): # this feels wrong function = self.__get__(None, owner) ParamInfo._registered_converters[owner] = function owner.__discord_converter__ = function # due to a bug in pylance classmethod subclasses do not actually work properly if TYPE_CHECKING: converter_method = classmethod else: def converter_method(function: Any) -> ConverterMethod: """A decorator to register a method as the converter method. .. versionadded:: 2.3 """ return ConverterMethod(function) def register_injection(function: CallableT) -> CallableT: """A decorator to register a global injection. .. versionadded:: 2.3 """ sig = signature(function) tp = sig.return_annotation if tp is inspect.Parameter.empty: raise TypeError("Injection must have a return annotation") if tp in ParamInfo.TYPES: raise TypeError("Injection cannot overwrite builtin types") Injection.register(function, sig.return_annotation) return function