diff --git a/discord/commands.py b/discord/commands.py index 02f0b7bf4..0809d8a06 100644 --- a/discord/commands.py +++ b/discord/commands.py @@ -24,12 +24,11 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from asyncio import TimeoutError from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union +from typing import Any, Dict, List, Optional, Protocol, Tuple, runtime_checkable, TYPE_CHECKING, Union from .enums import CommandType, ChannelType, OptionType, try_enum -from .errors import InvalidData +from .errors import InvalidData, InvalidArgument from .utils import time_snowflake if TYPE_CHECKING: @@ -39,29 +38,37 @@ if TYPE_CHECKING: from .state import ConnectionState -class ApplicationCommand: - def __init__( - self, data: Dict[str, Any] - ) -> None: - self.name: str = data['name'] - self.description: str = data['description'] +@runtime_checkable +class ApplicationCommand(Protocol): + + __slots__ = () + + if TYPE_CHECKING: + _state: ConnectionState + _application_id: int + name: str + description: str + version: int + type: CommandType + target_channel: Optional[Messageable] + default_permission: bool async def __call__(self, data, channel: Optional[Messageable] = None) -> Interaction: - channel = channel or self.target_channel # type: ignore + channel = channel or self.target_channel if channel is None: raise TypeError('__call__() missing 1 required keyword-only argument: \'channel\'') - state = self._state # type: ignore + state = self._state channel = await channel._get_channel() payload = { - 'application_id': str(self._application_id), # type: ignore + 'application_id': str(self._application_id), 'channel_id': str(channel.id), 'data': data, 'nonce': str(time_snowflake(datetime.utcnow())), 'type': 2, # Should be an enum but eh } - if getattr(channel, 'guild', None): - payload['guild_id'] = str(channel.guild.id) # type: ignore + if getattr(channel, 'guild', None) is not None: + payload['guild_id'] = str(channel.guild.id) state._interactions[payload['nonce']] = 2 await state.http.interact(payload, form_data=True) @@ -76,17 +83,33 @@ class ApplicationCommand: return i -class _BaseCommand(ApplicationCommand): +class BaseCommand(ApplicationCommand): + + __slots__ = ( + 'name', + 'description', + 'id', + 'version', + 'type', + 'default_permission', + '_dm_permission', + '_default_member_permissions', + '_state', + '_channel', + '_application_id' + ) + def __init__( self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None ) -> None: - super().__init__(data) + self.name = data['name'] + self.description = data['description'] self._state = state self._channel = channel self._application_id: int = int(data['application_id']) self.id: int = int(data['id']) - self.version: int = int(data['version']) - self.type: CommandType = try_enum(CommandType, data['type']) + self.version = int(data['version']) + self.type = try_enum(CommandType, data['type']) self.default_permission: bool = data['default_permission'] self._dm_permission = data['dm_permission'] self._default_member_permissions = data['default_member_permissions'] @@ -128,19 +151,23 @@ class _BaseCommand(ApplicationCommand): self._channel = value -class _SlashMixin: +class SlashMixin(ApplicationCommand): + if TYPE_CHECKING: + _parent: SlashCommand + options: List[Option] + children: List[SubCommand] + async def __call__(self, options, channel=None): - # This will always be used in a context where all these attributes are set - obj = getattr(self, '_parent', self) + obj = self._parent data = { 'attachments': [], - 'id': str(obj.id), # type: ignore - 'name': obj.name, # type: ignore + 'id': str(obj.id), + 'name': obj.name, 'options': options, - 'type': obj.type.value, # type: ignore - 'version': str(obj.version), # type: ignore + 'type': obj.type.value, + 'version': str(obj.version), } - return await super().__call__(data, channel) # type: ignore + return await super().__call__(data, channel) def _parse_kwargs(self, kwargs: Dict[str, Any]) -> List[Dict[str, Any]]: possible_options = {o.name: o for o in self.options} @@ -190,11 +217,11 @@ class _SlashMixin: for child in children: setattr(self, child.name, child) - self.options: List[Option] = options - self.children: List[SubCommand] = children + self.options = options + self.children = children -class UserCommand(_BaseCommand): +class UserCommand(BaseCommand): """Represents a user command. Attributes @@ -210,6 +237,9 @@ class UserCommand(_BaseCommand): default_permission: :class:`bool` Whether the command is enabled in guilds by default. """ + + __slots__ = ('_user',) + def __init__(self, *, user: Optional[Snowflake] = None, **kwargs): super().__init__(**kwargs) self._user = user @@ -259,7 +289,7 @@ class UserCommand(_BaseCommand): self._user = value -class MessageCommand(_BaseCommand): +class MessageCommand(BaseCommand): """Represents a message command. Attributes @@ -275,6 +305,9 @@ class MessageCommand(_BaseCommand): default_permission: :class:`bool` Whether the command is enabled in guilds by default. """ + + __slots__ = ('_message',) + def __init__(self, *, message: Optional[Message] = None, **kwargs): super().__init__(**kwargs) self._message = message @@ -324,7 +357,7 @@ class MessageCommand(_BaseCommand): self._message = value -class SlashCommand(_SlashMixin, _BaseCommand): +class SlashCommand(BaseCommand, SlashMixin): """Represents a slash command. Attributes @@ -346,13 +379,16 @@ class SlashCommand(_SlashMixin, _BaseCommand): You can access (and use) subcommands directly as attributes of the class. """ + __slots__ = ('_parent', 'options', 'children') + def __init__( self, *, data: Dict[str, Any], **kwargs ) -> None: super().__init__(data=data, **kwargs) + self._parent = self self._unwrap_options(data.get('options', [])) - async def __call__(self, channel, /, **kwargs): + async def __call__(self, channel: Optional[Messageable] = None, /, **kwargs): r"""Use the slash command. Parameters @@ -363,9 +399,14 @@ class SlashCommand(_SlashMixin, _BaseCommand): \*\*kwargs: Any The options to use. These will be casted to the correct type. If an option has choices, they are automatically converted from name to value for you. + + Raises + ------ + InvalidArgument + Attempted to use a group. """ if self.is_group(): - raise TypeError('Cannot use a group') + raise InvalidArgument('Cannot use a group') return await super().__call__(self._parse_kwargs(kwargs), channel) @@ -388,7 +429,7 @@ class SlashCommand(_SlashMixin, _BaseCommand): return bool(self.children) -class SubCommand(_SlashMixin, ApplicationCommand): +class SubCommand(SlashMixin): """Represents a slash command child. This could be a subcommand, or a subgroup. @@ -405,8 +446,20 @@ class SubCommand(_SlashMixin, ApplicationCommand): The type of application command. Always :class:`CommandType.chat_input`. """ + __slots__ = ( + '_parent', + '_state', + '_type', + 'parent', + 'options', + 'children', + 'type', + ) + def __init__(self, *, parent, data): - super().__init__(data) + self.name = data['name'] + self.description = data.get('description') + self._state = parent._state self.parent: Union[SlashCommand, SubCommand] = parent self._parent: SlashCommand = getattr(parent, 'parent', parent) # type: ignore self.type = CommandType.chat_input # Avoid confusion I guess @@ -433,9 +486,14 @@ class SubCommand(_SlashMixin, ApplicationCommand): \*\*kwargs: Any The options to use. These will be casted to the correct type. If an option has choices, they are automatically converted from name to value for you. + + Raises + ------ + InvalidArgument + Attempted to use a group. """ if self.is_group(): - raise TypeError('Cannot use a group') + raise InvalidArgument('Cannot use a group') options = [{ 'type': self._type.value, @@ -489,7 +547,7 @@ class SubCommand(_SlashMixin, ApplicationCommand): return self._parent.application @property - def target_channel(self): + def target_channel(self) -> Optional[Messageable]: """Optional[:class:`abc.Messageable`]: The channel this command will be used on. You can set this in order to use this command on a different channel without re-fetching it. @@ -526,6 +584,19 @@ class Option: autocomplete: :class:`bool` Whether the option autocompletes. Always ``False`` if :attr:`choices` are present. """ + + __slots__ = ( + 'name', + 'description', + 'type', + 'required', + 'min_value', + 'max_value', + 'choices', + 'channel_types', + 'autocomplete', + ) + def __init__(self, data): self.name: str = data['name'] self.description: str = data['description'] @@ -557,6 +628,9 @@ class OptionChoice: value: Any The choice's value. The type of this depends on the option's type. """ + + __slots__ = ('name', 'value') + def __init__(self, data: Dict[str, str], type: OptionType): self.name: str = data['name'] if type is OptionType.string: @@ -575,7 +649,7 @@ class OptionChoice: return value -def _command_factory(command_type: int) -> Tuple[CommandType, _BaseCommand]: +def _command_factory(command_type: int) -> Tuple[CommandType, BaseCommand]: value = try_enum(CommandType, command_type) if value is CommandType.chat_input: return value, SlashCommand @@ -584,4 +658,4 @@ def _command_factory(command_type: int) -> Tuple[CommandType, _BaseCommand]: elif value is CommandType.message: return value, MessageCommand else: - return value, _BaseCommand # IDK about this \ No newline at end of file + return value, BaseCommand # IDK about this \ No newline at end of file