Browse Source

Add support for annotation transformers

This facilitates the "converter-like" API of the app_commands
submodule. As a consequence of this refactor, more types are supported
like channels and attachment.
pull/7492/head
Rapptz 3 years ago
parent
commit
3cf3065c02
  1. 2
      discord/app_commands/__init__.py
  2. 206
      discord/app_commands/commands.py
  3. 50
      discord/app_commands/errors.py
  4. 496
      discord/app_commands/transformers.py

2
discord/app_commands/__init__.py

@ -14,3 +14,5 @@ from .enums import *
from .errors import *
from .models import *
from .tree import *
from .namespace import Namespace
from .transformers import *

206
discord/app_commands/commands.py

@ -41,7 +41,6 @@ from typing import (
TypeVar,
Union,
)
from dataclasses import dataclass
from textwrap import TextWrapper
import sys
@ -51,6 +50,7 @@ from .enums import AppCommandOptionType, AppCommandType
from ..interactions import Interaction
from ..enums import ChannelType, try_enum
from .models import AppCommandChannel, AppCommandThread, Choice
from .transformers import annotation_to_parameter, CommandParameter, NoneType
from .errors import AppCommandError, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
from ..utils import resolve_annotation, MISSING, is_inside_class
from ..user import User
@ -72,7 +72,6 @@ if TYPE_CHECKING:
from .namespace import Namespace
__all__ = (
'CommandParameter',
'Command',
'ContextMenu',
'Group',
@ -130,158 +129,6 @@ def _to_kebab_case(text: str) -> str:
return CAMEL_CASE_REGEX.sub('-', text).lower()
@dataclass
class CommandParameter:
"""Represents a application command parameter.
Attributes
-----------
name: :class:`str`
The name of the parameter.
description: :class:`str`
The description of the parameter
required: :class:`bool`
Whether the parameter is required
choices: List[:class:`~discord.app_commands.Choice`]
A list of choices this parameter takes
type: :class:`~discord.app_commands.AppCommandOptionType`
The underlying type of this parameter.
channel_types: List[:class:`~discord.ChannelType`]
The channel types that are allowed for this parameter.
min_value: Optional[:class:`int`]
The minimum supported value for this parameter.
max_value: Optional[:class:`int`]
The maximum supported value for this parameter.
autocomplete: :class:`bool`
Whether this parameter enables autocomplete.
"""
name: str = MISSING
description: str = MISSING
required: bool = MISSING
default: Any = MISSING
choices: List[Choice] = MISSING
type: AppCommandOptionType = MISSING
channel_types: List[ChannelType] = MISSING
min_value: Optional[int] = None
max_value: Optional[int] = None
autocomplete: bool = MISSING
_annotation: Any = MISSING
def to_dict(self) -> Dict[str, Any]:
base = {
'type': self.type.value,
'name': self.name,
'description': self.description,
'required': self.required,
}
if self.choices:
base['choices'] = [choice.to_dict() for choice in self.choices]
if self.channel_types:
base['channel_types'] = [t.value for t in self.channel_types]
if self.autocomplete:
base['autocomplete'] = True
if self.min_value is not None:
base['min_value'] = self.min_value
if self.max_value is not None:
base['max_value'] = self.max_value
return base
annotation_to_option_type: Dict[Any, AppCommandOptionType] = {
str: AppCommandOptionType.string,
int: AppCommandOptionType.integer,
float: AppCommandOptionType.number,
bool: AppCommandOptionType.boolean,
User: AppCommandOptionType.user,
Member: AppCommandOptionType.user,
Role: AppCommandOptionType.role,
AppCommandChannel: AppCommandOptionType.channel,
AppCommandThread: AppCommandOptionType.channel,
# StageChannel: AppCommandOptionType.channel,
# StoreChannel: AppCommandOptionType.channel,
# VoiceChannel: AppCommandOptionType.channel,
# TextChannel: AppCommandOptionType.channel,
}
NoneType = type(None)
allowed_default_types: Dict[AppCommandOptionType, Tuple[Type[Any], ...]] = {
AppCommandOptionType.string: (str, NoneType),
AppCommandOptionType.integer: (int, NoneType),
AppCommandOptionType.boolean: (bool, NoneType),
}
# Some sanity checks:
# str => string
# int => int
# User => user
# etc ...
# Optional[str] => string, required: false, default: None
# Optional[int] => integer, required: false, default: None
# Optional[Model] = None => resolved, required: false, default: None
# Optional[Model] can only have (CommandParameter, None) as default
# Optional[int | str | bool] can have (CommandParameter, None, int | str | bool) as a default
# Union[str, Member] => disallowed
# Union[int, str] => disallowed
# Union[Member, User] => user
# Optional[Union[Member, User]] => user, required: false, default: None
# Union[Member, User, Object] => mentionable
# Union[Models] => mentionable
# Optional[Union[Models]] => mentionable, required: false, default: None
def _annotation_to_type(
annotation: Any,
*,
mapping=annotation_to_option_type,
_none=NoneType,
) -> Tuple[AppCommandOptionType, Any]:
# Straight simple case, a regular ol' parameter
try:
option_type = mapping[annotation]
except KeyError:
pass
else:
return (option_type, MISSING)
# Check if there's an origin
origin = getattr(annotation, '__origin__', None)
if origin is not Union:
# Only Union/Optional is supported so bail early
raise TypeError(f'unsupported type annotation {annotation!r}')
default = MISSING
if annotation.__args__[-1] is _none:
if len(annotation.__args__) == 2:
underlying = annotation.__args__[0]
option_type = mapping.get(underlying)
if option_type is None:
raise TypeError(f'unsupported inner optional type {underlying!r}')
return (option_type, None)
else:
args = annotation.__args__[:-1]
default = None
else:
args = annotation.__args__
# At this point only models are allowed
# Since Optional[int | bool | str] will be taken care of above
# The only valid transformations here are:
# [Member, User] => user
# [Member, User, Role] => mentionable
# [Member | User, Role] => mentionable
supported_types: Set[Any] = {Role, Member, User}
if not all(arg in supported_types for arg in args):
raise TypeError(f'unsupported types given inside {annotation!r}')
if args == (User, Member) or args == (Member, User):
return (AppCommandOptionType.user, default)
return (AppCommandOptionType.mentionable, default)
def _context_menu_annotation(annotation: Any, *, _none=NoneType) -> AppCommandType:
if annotation is Message:
return AppCommandType.message
@ -324,33 +171,6 @@ def _populate_descriptions(params: Dict[str, CommandParameter], descriptions: Di
raise TypeError(f'unknown parameter given: {first}')
def _get_parameter(annotation: Any, parameter: inspect.Parameter) -> CommandParameter:
(type, default) = _annotation_to_type(annotation)
if default is MISSING:
default = parameter.default
if default is parameter.empty:
default = MISSING
result = CommandParameter(
type=type,
default=default,
required=default is MISSING,
name=parameter.name,
)
if parameter.kind in (parameter.POSITIONAL_ONLY, parameter.VAR_KEYWORD, parameter.VAR_POSITIONAL):
raise TypeError(f'unsupported parameter kind in callback: {parameter.kind!s}')
# Verify validity of the default parameter
if result.default is not MISSING:
valid_types: Tuple[Any, ...] = allowed_default_types.get(result.type, (NoneType,))
if not isinstance(result.default, valid_types):
raise TypeError(f'invalid default parameter type given ({result.default.__class__}), expected {valid_types}')
result._annotation = annotation
return result
def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[str, Any]) -> Dict[str, CommandParameter]:
params = inspect.signature(func).parameters
cache = {}
@ -368,7 +188,7 @@ def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[s
raise TypeError(f'annotation for {parameter.name} must be given')
resolved = resolve_annotation(parameter.annotation, globalns, globalns, cache)
param = _get_parameter(resolved, parameter)
param = annotation_to_parameter(resolved, parameter)
parameters.append(param)
values = sorted(parameters, key=lambda a: a.required, reverse=True)
@ -377,7 +197,9 @@ def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[s
try:
descriptions = func.__discord_app_commands_param_description__
except AttributeError:
pass
for param in values:
if param.description is MISSING:
param.description = '...'
else:
_populate_descriptions(result, descriptions)
@ -489,14 +311,24 @@ class Command(Generic[GroupT, P, T]):
await parent.parent.on_error(interaction, self, error)
async def _invoke_with_namespace(self, interaction: Interaction, namespace: Namespace) -> T:
defaults = ((name, param.default) for name, param in self._params.items() if not param.required)
namespace._update_with_defaults(defaults)
values = namespace.__dict__
for name, param in self._params.items():
if not param.required:
values.setdefault(name, param.default)
else:
try:
value = values[name]
except KeyError:
raise CommandSignatureMismatch(self) from None
else:
values[name] = await param.transform(interaction, value)
# These type ignores are because the type checker doesn't quite understand the narrowing here
# Likewise, it thinks we're missing positional arguments when there aren't any.
try:
if self.binding is not None:
return await self._callback(self.binding, interaction, **namespace.__dict__) # type: ignore
return await self._callback(interaction, **namespace.__dict__) # type: ignore
return await self._callback(self.binding, interaction, **values) # type: ignore
return await self._callback(interaction, **values) # type: ignore
except TypeError as e:
# In order to detect mismatch from the provided signature and the Discord data,
# there are many ways it can go wrong yet all of them eventually lead to a TypeError

50
discord/app_commands/errors.py

@ -24,14 +24,16 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional, Union
from typing import Any, TYPE_CHECKING, List, Optional, Type, Union
from .enums import AppCommandType
from .enums import AppCommandOptionType, AppCommandType
from ..errors import DiscordException
__all__ = (
'AppCommandError',
'CommandInvokeError',
'TransformerError',
'CommandAlreadyRegistered',
'CommandSignatureMismatch',
'CommandNotFound',
@ -39,6 +41,7 @@ __all__ = (
if TYPE_CHECKING:
from .commands import Command, Group, ContextMenu
from .transformers import Transformer
class AppCommandError(DiscordException):
@ -82,6 +85,49 @@ class CommandInvokeError(AppCommandError):
super().__init__(f'Command {command.name!r} raised an exception: {e.__class__.__name__}: {e}')
class TransformerError(AppCommandError):
"""An exception raised when a :class:`Transformer` or type annotation fails to
convert to its target type.
This inherits from :exc:`~discord.app_commands.AppCommandError`.
.. note::
If the transformer raises a custom :exc:`AppCommandError` then it will
be propagated rather than wrapped into this exception.
.. versionadded:: 2.0
Attributes
-----------
value: Any
The value that failed to convert.
type: :class:`AppCommandOptionType`
The type of argument that failed to convert.
transformer: Type[:class:`Transformer`]
The transformer that failed the conversion.
"""
def __init__(self, value: Any, opt_type: AppCommandOptionType, transformer: Type[Transformer]):
self.value: Any = value
self.type: AppCommandOptionType = opt_type
self.transformer: Type[Transformer] = transformer
try:
result_type = transformer.transform.__annotations__['return']
except KeyError:
name = transformer.__name__
if name.endswith('Transformer'):
result_type = name[:-11]
else:
result_type = name
else:
if isinstance(result_type, type):
result_type = result_type.__name__
super().__init__(f'Failed to convert {value} to {result_type!s}')
class CommandAlreadyRegistered(AppCommandError):
"""An exception raised when a command is already registered.

496
discord/app_commands/transformers.py

@ -0,0 +1,496 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
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
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
from .enums import AppCommandOptionType
from .errors import TransformerError
from .models import AppCommandChannel, AppCommandThread, Choice
from ..channel import StageChannel, StoreChannel, VoiceChannel, TextChannel, CategoryChannel
from ..enums import ChannelType
from ..utils import MISSING
from ..user import User
from ..role import Role
from ..member import Member
from ..message import Attachment
__all__ = (
'Transformer',
'Transform',
)
T = TypeVar('T')
NoneType = type(None)
if TYPE_CHECKING:
from ..interactions import Interaction
@dataclass
class CommandParameter:
"""Represents a application command parameter.
Attributes
-----------
name: :class:`str`
The name of the parameter.
description: :class:`str`
The description of the parameter
required: :class:`bool`
Whether the parameter is required
choices: List[:class:`~discord.app_commands.Choice`]
A list of choices this parameter takes
type: :class:`~discord.app_commands.AppCommandOptionType`
The underlying type of this parameter.
channel_types: List[:class:`~discord.ChannelType`]
The channel types that are allowed for this parameter.
min_value: Optional[Union[:class:`int`, :class:`float`]]
The minimum supported value for this parameter.
max_value: Optional[Union[:class:`int`, :class:`float`]]
The maximum supported value for this parameter.
autocomplete: :class:`bool`
Whether this parameter enables autocomplete.
"""
name: str = MISSING
description: str = MISSING
required: bool = MISSING
default: Any = MISSING
choices: List[Choice] = MISSING
type: AppCommandOptionType = MISSING
channel_types: List[ChannelType] = MISSING
min_value: Optional[Union[int, float]] = None
max_value: Optional[Union[int, float]] = None
autocomplete: bool = MISSING
_annotation: Any = MISSING
def to_dict(self) -> Dict[str, Any]:
base = {
'type': self.type.value,
'name': self.name,
'description': self.description,
'required': self.required,
}
if self.choices:
base['choices'] = [choice.to_dict() for choice in self.choices]
if self.channel_types:
base['channel_types'] = [t.value for t in self.channel_types]
if self.autocomplete:
base['autocomplete'] = True
if self.min_value is not None:
base['min_value'] = self.min_value
if self.max_value is not None:
base['max_value'] = self.max_value
return base
async def transform(self, interaction: Interaction, value: Any) -> Any:
if hasattr(self._annotation, '__discord_app_commands_transformer__'):
return await self._annotation.transform(interaction, value)
return value
class Transformer:
"""The base class that allows a type annotation in an application command parameter
to map into a :class:`AppCommandOptionType` and transform the raw value into one from
this type.
This class is customisable through the overriding of :obj:`classmethod`s in the class
and by using it as the second type parameter of the :class:`~discord.app_commands.Transform`
class. For example, to convert a string into a custom pair type:
.. code-block:: python3
class Point(typing.NamedTuple):
x: int
y: int
class PointTransformer(app_commands.Transformer):
@classmethod
async def transform(cls, interaction: discord.Interaction, value: str) -> Point:
(x, _, y) = value.partition(',')
return Point(x=int(x.strip()), y=int(y.strip()))
@app_commands.command()
async def graph(
interaction: discord.Interaction,
point: app_commands.Transform[Point, PointTransformer],
):
await interaction.response.send_message(str(point))
.. versionadded:: 2.0
"""
__discord_app_commands_transformer__: ClassVar[bool] = True
@classmethod
def type(cls) -> AppCommandOptionType:
""":class:`AppCommandOptionType`: The option type associated with this transformer.
This must be a :obj:`classmethod`.
Defaults to :attr:`AppCommandOptionType.string`.
"""
return AppCommandOptionType.string
@classmethod
def channel_types(cls) -> List[ChannelType]:
"""List[:class:`~discord.ChannelType`]: A list of channel types that are allowed to this parameter.
Only valid if the :meth:`type` returns :attr:`AppCommandOptionType.channel`.
Defaults to an empty list.
"""
return []
@classmethod
def min_value(cls) -> Optional[Union[int, float]]:
"""Optional[:class:`int`]: The minimum supported value for this parameter.
Only valid if the :meth:`type` returns :attr:`AppCommandOptionType.number` or
:attr:`AppCommandOptionType.integer`.
Defaults to ``None``.
"""
return None
@classmethod
def max_value(cls) -> Optional[Union[int, float]]:
"""Optional[:class:`int`]: The maximum supported value for this parameter.
Only valid if the :meth:`type` returns :attr:`AppCommandOptionType.number` or
:attr:`AppCommandOptionType.integer`.
Defaults to ``None``.
"""
return None
@classmethod
async def transform(cls, interaction: Interaction, value: Any) -> Any:
"""|coro|
Transforms the converted option value into another value.
The value passed into this transform function is the same as the
one in the :class:`conversion table <discord.app_commands.Namespace>`.
Parameters
-----------
interaction: :class:`~discord.Interaction`
The interaction being handled.
value: Any
The value of the given argument after being resolved.
See the :class:`conversion table <discord.app_commands.Namespace>`
for how certain option types correspond to certain values.
"""
raise NotImplementedError('Derived classes need to implement this.')
class _TransformMetadata:
__discord_app_commands_transform__: ClassVar[bool] = True
__slots__ = ('metadata',)
def __init__(self, metadata: Type[Transformer]):
self.metadata: Type[Transformer] = metadata
def _dynamic_transformer(
opt_type: AppCommandOptionType,
*,
channel_types: List[ChannelType] = MISSING,
min: Optional[Union[int, float]] = None,
max: Optional[Union[int, float]] = None,
) -> Type[Transformer]:
types = channel_types or []
async def transform(cls, interaction: Interaction, value: Any) -> Any:
return value
ns = {
'type': classmethod(lambda _: opt_type),
'channel_types': classmethod(lambda _: types),
'min_value': classmethod(lambda _: min),
'max_value': classmethod(lambda _: max),
'transform': classmethod(transform),
}
return type('_DynamicTransformer', (Transformer,), ns)
if TYPE_CHECKING:
from typing_extensions import Annotated as Transform
else:
class Transform:
"""A type annotation that can be applied to a parameter to customise the behaviour of
an option type by transforming with the given :class:`Transformer`. This requires
the usage of two generic parameters, the first one is the type you're converting to and the second
one is the type of the :class:`Transformer` actually doing the transformation.
During type checking time this is equivalent to :obj:`py:Annotated` so type checkers understand
the intent of the code.
For example usage, check :class:`Transformer`.
.. versionadded:: 2.0
"""
def __class_getitem__(cls, items) -> _TransformMetadata:
if not isinstance(items, tuple):
raise TypeError(f'expected tuple for arguments, received {items.__class__!r} instead')
if len(items) != 2:
raise TypeError(f'Transform only accepts exactly two arguments')
_, transformer = items
is_valid = inspect.isclass(transformer) and issubclass(transformer, Transformer)
if not is_valid:
raise TypeError(f'second argument of Transform must be a Transformer class not {transformer!r}')
return _TransformMetadata(transformer)
def passthrough_transformer(opt_type: AppCommandOptionType) -> Type[Transformer]:
class _Generated(Transformer):
@classmethod
def type(cls) -> AppCommandOptionType:
return opt_type
@classmethod
async def transform(cls, interaction: Interaction, value: Any) -> Any:
return value
return _Generated
class MemberTransformer(Transformer):
@classmethod
def type(cls) -> AppCommandOptionType:
return AppCommandOptionType.user
@classmethod
async def transform(cls, interaction: Interaction, value: Any) -> Member:
if not isinstance(value, Member):
raise TransformerError(value, cls.type(), cls)
return value
def channel_transformer(*channel_types: Type[Any], raw: Optional[bool] = False) -> Type[Transformer]:
if raw:
async def transform(cls, interaction: Interaction, value: Any):
if not isinstance(value, channel_types):
raise TransformerError(value, AppCommandOptionType.channel, cls)
return value
elif raw is False:
async def transform(cls, interaction: Interaction, value: Any):
resolved = value.resolve()
if resolved is None or not isinstance(resolved, channel_types):
raise TransformerError(value, AppCommandOptionType.channel, cls)
return resolved
else:
async def transform(cls, interaction: Interaction, value: Any):
if isinstance(value, channel_types):
return value
resolved = value.resolve()
if resolved is None or not isinstance(resolved, channel_types):
raise TransformerError(value, AppCommandOptionType.channel, cls)
return resolved
if len(channel_types) == 1:
name = channel_types[0].__name__
types = CHANNEL_TO_TYPES[channel_types[0]]
else:
name = 'MultiChannel'
types = []
for t in channel_types:
try:
types.extend(CHANNEL_TO_TYPES[t])
except KeyError:
raise TypeError(f'Union type of channels must be entirely made up of channels') from None
return type(
f'{name}Transformer',
(Transformer,),
{
'type': classmethod(lambda cls: AppCommandOptionType.channel),
'transform': classmethod(transform),
'channel_types': classmethod(lambda cls: types),
},
)
CHANNEL_TO_TYPES: Dict[Any, List[ChannelType]] = {
AppCommandChannel: [
ChannelType.stage_voice,
ChannelType.store,
ChannelType.voice,
ChannelType.text,
ChannelType.category,
],
AppCommandThread: [ChannelType.news_thread, ChannelType.private_thread, ChannelType.public_thread],
StageChannel: [ChannelType.stage_voice],
StoreChannel: [ChannelType.store],
VoiceChannel: [ChannelType.voice],
TextChannel: [ChannelType.text],
CategoryChannel: [ChannelType.category],
}
BUILT_IN_TRANSFORMERS: Dict[Any, Type[Transformer]] = {
str: passthrough_transformer(AppCommandOptionType.string),
int: passthrough_transformer(AppCommandOptionType.integer),
float: passthrough_transformer(AppCommandOptionType.number),
bool: passthrough_transformer(AppCommandOptionType.boolean),
User: passthrough_transformer(AppCommandOptionType.user),
Member: MemberTransformer,
Role: passthrough_transformer(AppCommandOptionType.role),
AppCommandChannel: channel_transformer(AppCommandChannel, raw=True),
AppCommandThread: channel_transformer(AppCommandThread, raw=True),
StageChannel: channel_transformer(StageChannel),
StoreChannel: channel_transformer(StoreChannel),
VoiceChannel: channel_transformer(VoiceChannel),
TextChannel: channel_transformer(TextChannel),
CategoryChannel: channel_transformer(CategoryChannel),
Attachment: passthrough_transformer(AppCommandOptionType.attachment),
}
ALLOWED_DEFAULTS: Dict[AppCommandOptionType, Tuple[Type[Any], ...]] = {
AppCommandOptionType.string: (str, NoneType),
AppCommandOptionType.integer: (int, NoneType),
AppCommandOptionType.boolean: (bool, NoneType),
}
def get_supported_annotation(
annotation: Any,
*,
_none=NoneType,
_mapping: Dict[Any, Type[Transformer]] = BUILT_IN_TRANSFORMERS,
) -> Tuple[Any, Any]:
"""Returns an appropriate, yet supported, annotation along with an optional default value.
This differs from the built in mapping by supporting a few more things.
Likewise, this returns a "transformed" annotation that is ready to use with CommandParameter.transform.
"""
try:
return (_mapping[annotation], MISSING)
except KeyError:
pass
if hasattr(annotation, '__discord_app_commands_transform__'):
return (annotation.metadata, MISSING)
if inspect.isclass(annotation) and issubclass(annotation, Transformer):
return (annotation, MISSING)
# Check if there's an origin
origin = getattr(annotation, '__origin__', None)
if origin is not Union:
# Only Union/Optional is supported right now so bail early
raise TypeError(f'unsupported type annotation {annotation!r}')
default = MISSING
args = annotation.__args__ # type: ignore
if args[-1] is _none:
if len(args) == 2:
underlying = args[0]
inner, _ = get_supported_annotation(underlying)
if inner is None:
raise TypeError(f'unsupported inner optional type {underlying!r}')
return (inner, None)
else:
args = args[:-1]
default = None
# Check for channel union types
if any(arg in CHANNEL_TO_TYPES for arg in args):
# If any channel type is given, then *all* must be channel types
return (channel_transformer(*args, raw=None), default)
# The only valid transformations here are:
# [Member, User] => user
# [Member, User, Role] => mentionable
# [Member | User, Role] => mentionable
supported_types: Set[Any] = {Role, Member, User}
if not all(arg in supported_types for arg in args):
raise TypeError(f'unsupported types given inside {annotation!r}')
if args == (User, Member) or args == (Member, User):
return (passthrough_transformer(AppCommandOptionType.user), default)
return (passthrough_transformer(AppCommandOptionType.mentionable), default)
def annotation_to_parameter(annotation: Any, parameter: inspect.Parameter) -> CommandParameter:
"""Returns the appropriate :class:`CommandParameter` for the given annotation.
The resulting ``_annotation`` attribute might not match the one given here and might
be transformed in order to be easier to call from the ``transform`` asynchronous function
of a command parameter.
"""
(inner, default) = get_supported_annotation(annotation)
type = inner.type()
if default is MISSING:
default = parameter.default
if default is parameter.empty:
default = MISSING
# Verify validity of the default parameter
if default is not MISSING:
valid_types: Tuple[Any, ...] = ALLOWED_DEFAULTS.get(type, (NoneType,))
if not isinstance(default, valid_types):
raise TypeError(f'invalid default parameter type given ({default.__class__}), expected {valid_types}')
result = CommandParameter(
type=type,
_annotation=inner,
default=default,
required=default is MISSING,
name=parameter.name,
)
# These methods should be duck typed
if type in (AppCommandOptionType.number, AppCommandOptionType.integer):
result.min_value = inner.min_value()
result.max_value = inner.max_value()
if type is AppCommandOptionType.channel:
result.channel_types = inner.channel_types()
if parameter.kind in (parameter.POSITIONAL_ONLY, parameter.VAR_KEYWORD, parameter.VAR_POSITIONAL):
raise TypeError(f'unsupported parameter kind in callback: {parameter.kind!s}')
return result
Loading…
Cancel
Save