From 3c371795516ebb5c9fac67ed037b8741bdcf0844 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sun, 17 Sep 2023 00:23:38 +0300 Subject: [PATCH 01/67] A draft of the app commands storage refactor --- disnake/ext/commands/cog.py | 23 +- disnake/ext/commands/errors.py | 37 ++ disnake/ext/commands/interaction_bot_base.py | 346 +++++++++++++------ disnake/interactions/application_command.py | 10 +- 4 files changed, 294 insertions(+), 122 deletions(-) diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index a0305ccea7..6c40a4bd6e 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -754,21 +754,11 @@ def _inject(self, bot: AnyBot) -> Self: for index, command in enumerate(self.__cog_app_commands__): command.cog = self try: - if isinstance(command, InvokableSlashCommand): - bot.add_slash_command(command) - elif isinstance(command, InvokableUserCommand): - bot.add_user_command(command) - elif isinstance(command, InvokableMessageCommand): - bot.add_message_command(command) + bot.add_app_command(command) except Exception: # undo our additions for to_undo in self.__cog_app_commands__[:index]: - if isinstance(to_undo, InvokableSlashCommand): - bot.remove_slash_command(to_undo.name) - elif isinstance(to_undo, InvokableUserCommand): - bot.remove_user_command(to_undo.name) - elif isinstance(to_undo, InvokableMessageCommand): - bot.remove_message_command(to_undo.name) + bot.remove_app_command(to_undo.body.type, to_undo.name, to_undo.guild_ids) raise if not hasattr(self.cog_load.__func__, "__cog_special_method__"): @@ -841,12 +831,9 @@ def _eject(self, bot: AnyBot) -> None: bot.remove_command(command.name) # type: ignore for app_command in self.__cog_app_commands__: - if isinstance(app_command, InvokableSlashCommand): - bot.remove_slash_command(app_command.name) - elif isinstance(app_command, InvokableUserCommand): - bot.remove_user_command(app_command.name) - elif isinstance(app_command, InvokableMessageCommand): - bot.remove_message_command(app_command.name) + bot.remove_app_command( + app_command.body.type, app_command.name, app_command.guild_ids + ) for name, method_name in self.__cog_listeners__: bot.remove_listener(getattr(self, method_name), name) diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 25329d9305..5b65e40f48 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Type, Union +from disnake.enums import ApplicationCommandType from disnake.errors import ClientException, DiscordException from disnake.utils import humanize_list @@ -74,6 +75,7 @@ "ExtensionFailed", "ExtensionNotFound", "CommandRegistrationError", + "AppCommandRegistrationError", "FlagError", "BadFlagArgument", "MissingFlagArgument", @@ -1021,6 +1023,41 @@ def __init__(self, name: str, *, alias_conflict: bool = False) -> None: type_ = "alias" if alias_conflict else "command" super().__init__(f"The {type_} {name} is already an existing command or alias.") +# we inherit CommandRegistrationError for backwards compatibility, +# because this error replaced CommandRegistrationError in several places +class AppCommandRegistrationError(CommandRegistrationError): + """An exception raised when the app command can't be added + because a command with the same key already exists. + A key is determined by command type, name, and guild_id. + + This inherits from :exc:`disnake.CommandRegistrationError` + + .. versionadded:: 2.10 + + Attributes + ---------- + cmd_type: :class:`ApplicationCommandType` + The command type. + name: :class:`str` + The command name. + guild_id: Optional[:class:`int`] + The ID of the guild where the command was supposed to be registered + or ``None`` if it was a global command. + """ + + def __init__(self, cmd_type: ApplicationCommandType, name: str, guild_id: Optional[int]) -> None: + self.cmd_type: ApplicationCommandType = cmd_type + self.name: str = name + self.guild_id: Optional[int] = guild_id + # fixed API naming here because no one calls slash commands "chat input" + type_ = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name + if guild_id is None: + msg = f"Global {type_} command {name} was specified earlier in your code." + else: + msg = f"Local {type_} command {name} with guild ID {guild_id} was specified earlier in your code." + # this bypasses CommandRegistrationError.__init__ + super(CommandRegistrationError, self).__init__(msg) + class FlagError(BadArgument): """The base exception type for all flag parsing related errors. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 25308c3649..5ea5b291a5 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -7,7 +7,6 @@ import sys import traceback import warnings -from itertools import chain from typing import ( TYPE_CHECKING, Any, @@ -15,6 +14,7 @@ Dict, Iterable, List, + NamedTuple, Optional, Sequence, Set, @@ -22,6 +22,7 @@ TypedDict, TypeVar, Union, + cast, ) import disnake @@ -39,7 +40,7 @@ message_command, user_command, ) -from .errors import CommandRegistrationError +from .errors import AppCommandRegistrationError from .flags import CommandSyncFlags from .slash_core import InvokableSlashCommand, SubCommand, SubCommandGroup, slash_command @@ -79,6 +80,12 @@ class _Diff(TypedDict): delete_ignored: NotRequired[List[ApplicationCommand]] +class AppCmdIndex(NamedTuple): + type: ApplicationCommandType + name: str + guild_id: Optional[int] + + def _get_to_send_from_diff(diff: _Diff): return diff["no_changes"] + diff["upsert"] + diff["edit"] + diff.get("delete_ignored", []) @@ -139,6 +146,21 @@ def _format_diff(diff: _Diff) -> str: return "\n".join(f"| {line}" for line in lines) +def _match_subcommand_chain( + command: InvokableSlashCommand, chain: List[str] +) -> Union[InvokableSlashCommand, SubCommand, SubCommandGroup, None]: + if command.name != chain[0]: + return None + if len(chain) == 1: + return command + if len(chain) == 2: + return command.children.get(chain[1]) + if len(chain) == 3: + group = command.children.get(chain[1]) + if isinstance(group, SubCommandGroup): + return group.children.get(chain[2]) + + class InteractionBotBase(CommonBotBase): def __init__( self, @@ -213,9 +235,8 @@ def __init__( self._before_message_command_invoke = None self._after_message_command_invoke = None - self.all_slash_commands: Dict[str, InvokableSlashCommand] = {} - self.all_user_commands: Dict[str, InvokableUserCommand] = {} - self.all_message_commands: Dict[str, InvokableMessageCommand] = {} + self.all_app_commands: Dict[AppCmdIndex, InvokableApplicationCommand] = {} + # TODO: add .all_xxx_commands properties with deprecation warnings @disnake.utils.copy_doc(disnake.Client.login) async def login(self, token: str) -> None: @@ -232,31 +253,82 @@ def command_sync_flags(self) -> CommandSyncFlags: return CommandSyncFlags._from_value(self._command_sync_flags.value) def application_commands_iterator(self) -> Iterable[InvokableApplicationCommand]: - return chain( - self.all_slash_commands.values(), - self.all_user_commands.values(), - self.all_message_commands.values(), + warn_deprecated( + "application_commands_iterator is deprecated and will be removed in a future version. " + "Use all_app_commands.values() instead.", + stacklevel=3, ) + return self.all_app_commands.values() @property def application_commands(self) -> Set[InvokableApplicationCommand]: """Set[:class:`InvokableApplicationCommand`]: A set of all application commands the bot has.""" - return set(self.application_commands_iterator()) + return set(self.all_app_commands.values()) @property def slash_commands(self) -> Set[InvokableSlashCommand]: """Set[:class:`InvokableSlashCommand`]: A set of all slash commands the bot has.""" - return set(self.all_slash_commands.values()) + return set( + cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableSlashCommand) + ) @property def user_commands(self) -> Set[InvokableUserCommand]: """Set[:class:`InvokableUserCommand`]: A set of all user commands the bot has.""" - return set(self.all_user_commands.values()) + return set( + cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableUserCommand) + ) @property def message_commands(self) -> Set[InvokableMessageCommand]: """Set[:class:`InvokableMessageCommand`]: A set of all message commands the bot has.""" - return set(self.all_message_commands.values()) + return set( + cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableMessageCommand) + ) + + def add_app_command(self, app_command: InvokableApplicationCommand) -> None: + """Adds an :class:`InvokableApplicationCommand` into the internal list of app commands. + + This is usually not called, instead shortcut decorators are used. + + .. versionadded:: 2.10 + + Parameters + ---------- + app_command: :class:`InvokableApplicationCommand` + The app command to add. + + Raises + ------ + AppCommandRegistrationError + The app command is already registered. + TypeError + The app command passed is not an instance of :class:`InvokableApplicationCommand`. + """ + + if not isinstance(self, disnake.Client): + raise NotImplementedError("This method is only usable in disnake.Client subclasses") + + if not isinstance(slash_command, InvokableSlashCommand): + raise TypeError("The slash_command passed must be an instance of InvokableSlashCommand") + + if app_command.guild_ids is None: + # if test_guilds are specified then we add the same command for each test guild + guild_ids = (None,) if self._test_guilds is None else self._test_guilds + else: + guild_ids = app_command.guild_ids + + for guild_id in guild_ids: + cmd_index = AppCmdIndex( + type=app_command.body.type, name=app_command.name, guild_id=guild_id + ) + if cmd_index in self.all_app_commands: + raise AppCommandRegistrationError(cmd_index.type, cmd_index.name, cmd_index.guild_id) + + # localization may be called multiple times for the same command but it's harmless + app_command.body.localize(self.i18n) + + self.all_app_commands[cmd_index] = app_command def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: """Adds an :class:`InvokableSlashCommand` into the internal list of slash commands. @@ -276,17 +348,12 @@ def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: TypeError The slash command passed is not an instance of :class:`InvokableSlashCommand`. """ - if not isinstance(self, disnake.Client): - raise NotImplementedError("This method is only usable in disnake.Client subclasses") - - if not isinstance(slash_command, InvokableSlashCommand): - raise TypeError("The slash_command passed must be an instance of InvokableSlashCommand") - - if slash_command.name in self.all_slash_commands: - raise CommandRegistrationError(slash_command.name) - - slash_command.body.localize(self.i18n) - self.all_slash_commands[slash_command.name] = slash_command + warn_deprecated( + "add_slash_command is deprecated and will be removed in a future version. " + "Use add_app_command instead.", + stacklevel=3, + ) + self.add_app_command(slash_command) def add_user_command(self, user_command: InvokableUserCommand) -> None: """Adds an :class:`InvokableUserCommand` into the internal list of user commands. @@ -306,17 +373,12 @@ def add_user_command(self, user_command: InvokableUserCommand) -> None: TypeError The user command passed is not an instance of :class:`InvokableUserCommand`. """ - if not isinstance(self, disnake.Client): - raise NotImplementedError("This method is only usable in disnake.Client subclasses") - - if not isinstance(user_command, InvokableUserCommand): - raise TypeError("The user_command passed must be an instance of InvokableUserCommand") - - if user_command.name in self.all_user_commands: - raise CommandRegistrationError(user_command.name) - - user_command.body.localize(self.i18n) - self.all_user_commands[user_command.name] = user_command + warn_deprecated( + "add_user_command is deprecated and will be removed in a future version. " + "Use add_app_command instead.", + stacklevel=3, + ) + self.add_app_command(user_command) def add_message_command(self, message_command: InvokableMessageCommand) -> None: """Adds an :class:`InvokableMessageCommand` into the internal list of message commands. @@ -336,19 +398,71 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: TypeError The message command passed is not an instance of :class:`InvokableMessageCommand`. """ - if not isinstance(self, disnake.Client): - raise NotImplementedError("This method is only usable in disnake.Client subclasses") + warn_deprecated( + "add_message_command is deprecated and will be removed in a future version. " + "Use add_app_command instead.", + stacklevel=3, + ) + self.add_app_command(message_command) - if not isinstance(message_command, InvokableMessageCommand): - raise TypeError( - "The message_command passed must be an instance of InvokableMessageCommand" - ) + def remove_app_command( + self, cmd_type: ApplicationCommandType, name: str, guild_ids: Optional[Tuple[int, ...]] + ) -> Optional[InvokableApplicationCommand]: + """Removes an :class:`InvokableApplicationCommand` from the internal list of app commands. + + .. versionadded:: 2.10 + + Parameters + ---------- + cmd_type: :class:`ApplicationCommandType` + The type of the app command to remove. + name: :class:`str` + The name of the app command to remove. + guild_ids: Optional[Tuple[:class:`str`, ...]] + The IDs of the guilds from which the command should be removed. + + Returns + ------- + Optional[:class:`InvokableApplicationCommand`] + The app command that was removed. If the key data was not valid then ``None`` is returned instead. + """ + + if guild_ids is None: + extended_guild_ids = (None,) + # a global command may end up being a local command if test_guilds were specified + # so we should remove this "global" command from each test guild + if self._test_guilds is not None: + extended_guild_ids += self._test_guilds + else: + extended_guild_ids = guild_ids + + result = None + for guild_id in extended_guild_ids: + cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) + cmd = self.all_app_commands.pop(cmd_index, None) + if result is None: + result = cmd + return result + + def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name: str) -> Any: + type_info = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name + warn_deprecated( + f"remove_{type_info}_command is deprecated and will be removed in a future version. " + "Use remove_app_command instead.", + stacklevel=3, + ) + bad_keys = [] + for key in self.all_app_commands.keys(): + if key.type is cmd_type and key.name == name: + bad_keys.append(key) - if message_command.name in self.all_message_commands: - raise CommandRegistrationError(message_command.name) + result = None + for key in bad_keys: + cmd = self.all_app_commands.pop(key, None) + if result is None: + result = cmd - message_command.body.localize(self.i18n) - self.all_message_commands[message_command.name] = message_command + return result def remove_slash_command(self, name: str) -> Optional[InvokableSlashCommand]: """Removes an :class:`InvokableSlashCommand` from the internal list @@ -364,10 +478,7 @@ def remove_slash_command(self, name: str) -> Optional[InvokableSlashCommand]: Optional[:class:`InvokableSlashCommand`] The slash command that was removed. If the name is not valid then ``None`` is returned instead. """ - command = self.all_slash_commands.pop(name, None) - if command is None: - return None - return command + self._emulate_old_app_command_remove(ApplicationCommandType.chat_input, name) def remove_user_command(self, name: str) -> Optional[InvokableUserCommand]: """Removes an :class:`InvokableUserCommand` from the internal list @@ -383,10 +494,7 @@ def remove_user_command(self, name: str) -> Optional[InvokableUserCommand]: Optional[:class:`InvokableUserCommand`] The user command that was removed. If the name is not valid then ``None`` is returned instead. """ - command = self.all_user_commands.pop(name, None) - if command is None: - return None - return command + self._emulate_old_app_command_remove(ApplicationCommandType.user, name) def remove_message_command(self, name: str) -> Optional[InvokableMessageCommand]: """Removes an :class:`InvokableMessageCommand` from the internal list @@ -402,13 +510,10 @@ def remove_message_command(self, name: str) -> Optional[InvokableMessageCommand] Optional[:class:`InvokableMessageCommand`] The message command that was removed. If the name is not valid then ``None`` is returned instead. """ - command = self.all_message_commands.pop(name, None) - if command is None: - return None - return command + self._emulate_old_app_command_remove(ApplicationCommandType.message, name) def get_slash_command( - self, name: str + self, name: str, guild_id: Optional[int] = MISSING ) -> Optional[Union[InvokableSlashCommand, SubCommandGroup, SubCommand]]: """Works like ``Bot.get_command``, but for slash commands. @@ -421,6 +526,9 @@ def get_slash_command( ---------- name: :class:`str` The name of the slash command to get. + guild_id: Optional[:class:`int`] + The guild ID corresponding to the slash command or ``None`` if it's a global command. + If this is not specified then the first match is returned instead. Raises ------ @@ -436,20 +544,25 @@ def get_slash_command( raise TypeError(f"Expected name to be str, not {name.__class__}") chain = name.split() - slash = self.all_slash_commands.get(chain[0]) - if slash is None: - return None - - if len(chain) == 1: - return slash - elif len(chain) == 2: - return slash.children.get(chain[1]) - elif len(chain) == 3: - group = slash.children.get(chain[1]) - if isinstance(group, SubCommandGroup): - return group.children.get(chain[2]) - - def get_user_command(self, name: str) -> Optional[InvokableUserCommand]: + if guild_id is not MISSING: + cmd_index = AppCmdIndex( + type=ApplicationCommandType.chat_input, name=chain[0], guild_id=guild_id + ) + command = self.all_app_commands.get(cmd_index) + if command is None: + return None + return _match_subcommand_chain(command, chain) # type: ignore + + for command in self.all_app_commands.values(): + if not isinstance(command, InvokableSlashCommand): + continue + result = _match_subcommand_chain(command, chain) + if result is not None: + return result + + def get_user_command( + self, name: str, guild_id: Optional[int] = MISSING + ) -> Optional[InvokableUserCommand]: """Gets an :class:`InvokableUserCommand` from the internal list of user commands. @@ -457,15 +570,31 @@ def get_user_command(self, name: str) -> Optional[InvokableUserCommand]: ---------- name: :class:`str` The name of the user command to get. + guild_id: Optional[:class:`int`] + The guild ID corresponding to the user command or ``None`` if it's a global command. + If this is not specified then the first match is returned instead. Returns ------- Optional[:class:`InvokableUserCommand`] The user command that was requested. If not found, returns ``None``. """ - return self.all_user_commands.get(name) - - def get_message_command(self, name: str) -> Optional[InvokableMessageCommand]: + if guild_id is not MISSING: + cmd_index = AppCmdIndex( + type=ApplicationCommandType.user, name=name, guild_id=guild_id + ) + command = self.all_app_commands.get(cmd_index) + if command is None: + return None + return command # type: ignore + + for command in self.all_app_commands.values(): + if isinstance(command, InvokableUserCommand) and command.name == name: + return command + + def get_message_command( + self, name: str, guild_id: Optional[int] = MISSING + ) -> Optional[InvokableMessageCommand]: """Gets an :class:`InvokableMessageCommand` from the internal list of message commands. @@ -473,13 +602,27 @@ def get_message_command(self, name: str) -> Optional[InvokableMessageCommand]: ---------- name: :class:`str` The name of the message command to get. + guild_id: Optional[:class:`int`] + The guild ID corresponding to the message command or ``None`` if it's a global command. + If this is not specified then the first match is returned instead. Returns ------- Optional[:class:`InvokableMessageCommand`] The message command that was requested. If not found, returns ``None``. """ - return self.all_message_commands.get(name) + if guild_id is not MISSING: + cmd_index = AppCmdIndex( + type=ApplicationCommandType.message, name=name, guild_id=guild_id + ) + command = self.all_app_commands.get(cmd_index) + if command is None: + return None + return command # type: ignore + + for command in self.all_app_commands.values(): + if isinstance(command, InvokableMessageCommand) and command.name == name: + return command def slash_command( self, @@ -570,7 +713,7 @@ def decorator(func: CommandCallback) -> InvokableSlashCommand: extras=extras, **kwargs, )(func) - self.add_slash_command(result) + self.add_app_command(result) return result return decorator @@ -647,7 +790,7 @@ def decorator( extras=extras, **kwargs, )(func) - self.add_user_command(result) + self.add_app_command(result) return result return decorator @@ -724,7 +867,7 @@ def decorator( extras=extras, **kwargs, )(func) - self.add_message_command(result) + self.add_app_command(result) return result return decorator @@ -734,24 +877,22 @@ def decorator( def _ordered_unsynced_commands( self, test_guilds: Optional[Sequence[int]] = None ) -> Tuple[List[ApplicationCommand], Dict[int, List[ApplicationCommand]]]: + # TODO: deprecation warning for the test_guilds arg global_cmds = [] guilds = {} - for cmd in self.application_commands_iterator(): + for key, cmd in self.all_app_commands.items(): if not cmd.auto_sync: cmd.body._always_synced = True - guild_ids = cmd.guild_ids or test_guilds + guild_id = key.guild_id - if guild_ids is None: + if guild_id is None: global_cmds.append(cmd.body) - continue - - for guild_id in guild_ids: - if guild_id not in guilds: - guilds[guild_id] = [cmd.body] - else: - guilds[guild_id].append(cmd.body) + elif guild_id not in guilds: + guilds[guild_id] = [cmd.body] + else: + guilds[guild_id].append(cmd.body) return global_cmds, guilds @@ -759,7 +900,7 @@ async def _cache_application_commands(self) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") - _, guilds = self._ordered_unsynced_commands(self._test_guilds) + _, guilds = self._ordered_unsynced_commands() # Here we only cache global commands and commands from guilds that are specified in the code. # They're collected from the "test_guilds" kwarg of commands.InteractionBotBase @@ -794,8 +935,8 @@ async def _sync_application_commands(self) -> None: return # We assume that all commands are already cached. - # Sort all invokable commands between guild IDs: - global_cmds, guild_cmds = self._ordered_unsynced_commands(self._test_guilds) + # Group all invokable commands by guild IDs: + global_cmds, guild_cmds = self._ordered_unsynced_commands() if self._command_sync_flags.sync_global_commands: # Update global commands first @@ -1280,11 +1421,17 @@ async def process_app_command_autocompletion( inter: :class:`disnake.ApplicationCommandInteraction` The interaction to process. """ - slash_command = self.all_slash_commands.get(inter.data.name) + cmd_index = AppCmdIndex( + type=inter.data.type, name=inter.data.name, guild_id=inter.data.guild_id + ) + # this happens to always be a slash command + slash_command = self.all_app_commands.get(cmd_index) if slash_command is None: return + slash_command = cast(InvokableSlashCommand, slash_command) + inter.application_command = slash_command if slash_command.guild_ids is None or inter.guild_id in slash_command.guild_ids: await slash_command._call_relevant_autocompleter(inter) @@ -1316,7 +1463,7 @@ async def process_application_commands( # and we're instructed to sync guild commands and self._command_sync_flags.sync_guild_commands # and the current command was registered to a guild - and interaction.data.get("guild_id") + and interaction.data.guild_id # and we don't know the command and not self.get_guild_command(interaction.guild_id, interaction.data.id) # type: ignore ): @@ -1351,20 +1498,17 @@ async def process_application_commands( return command_type = interaction.data.type - command_name = interaction.data.name - app_command = None event_name = None + cmd_index = AppCmdIndex( + type=command_type, name=interaction.data.name, guild_id=interaction.data.guild_id + ) + app_command = self.all_app_commands.get(cmd_index) if command_type is ApplicationCommandType.chat_input: - app_command = self.all_slash_commands.get(command_name) event_name = "slash_command" - elif command_type is ApplicationCommandType.user: - app_command = self.all_user_commands.get(command_name) event_name = "user_command" - elif command_type is ApplicationCommandType.message: - app_command = self.all_message_commands.get(command_name) event_name = "message_command" if event_name is None or app_command is None: diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 13c96c02de..8fb465d7c6 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -174,16 +174,19 @@ class ApplicationCommandInteractionData(Dict[str, Any]): All resolved objects related to this interaction. options: List[:class:`ApplicationCommandInteractionDataOption`] A list of options from the API. + guild_id: Optional[:class:`int`] + ID of the guild the command is registered to. target_id: :class:`int` - ID of the user or message targetted by a user or message command + ID of the user or message targetted by a user or message command. target: Union[:class:`User`, :class:`Member`, :class:`Message`] - The user or message targetted by a user or message command + The user or message targetted by a user or message command. """ __slots__ = ( "id", "name", "type", + "guild_id", "target_id", "target", "resolved", @@ -195,7 +198,7 @@ def __init__( *, data: ApplicationCommandInteractionDataPayload, state: ConnectionState, - guild_id: Optional[int], + guild_id: Optional[int], # the ID of the guild where this command has been invoked ) -> None: super().__init__(data) self.id: int = int(data["id"]) @@ -205,6 +208,7 @@ def __init__( self.resolved = InteractionDataResolved( data=data.get("resolved", {}), state=state, guild_id=guild_id ) + self.guild_id: Optional[int] = utils._get_as_snowflake(data, "guild_id") self.target_id: Optional[int] = utils._get_as_snowflake(data, "target_id") target = self.resolved.get_by_id(self.target_id) self.target: Optional[Union[User, Member, Message]] = target # type: ignore From b6500097608e15168f74662ef734b4eddbc6a27d Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:02:53 +0300 Subject: [PATCH 02/67] Add missing deprecation warnings and properties --- disnake/ext/commands/interaction_bot_base.py | 69 +++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 5ea5b291a5..6ff5f490ad 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -236,7 +236,6 @@ def __init__( self._after_message_command_invoke = None self.all_app_commands: Dict[AppCmdIndex, InvokableApplicationCommand] = {} - # TODO: add .all_xxx_commands properties with deprecation warnings @disnake.utils.copy_doc(disnake.Client.login) async def login(self, token: str) -> None: @@ -286,6 +285,48 @@ def message_commands(self) -> Set[InvokableMessageCommand]: cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableMessageCommand) ) + @property + def all_slash_commands(self) -> Dict[str, InvokableSlashCommand]: + # no docstring because it was an attribute and now it's deprecated + warn_deprecated( + "all_slash_commands is deprecated and will be removed in a future version. " + "Use all_app_commands as a replacement.", + stacklevel=3, + ) + return { + cmd.name: cmd + for cmd in self.all_app_commands.values() + if isinstance(cmd, InvokableSlashCommand) + } + + @property + def all_user_commands(self) -> Dict[str, InvokableUserCommand]: + # no docstring because it was an attribute and now it's deprecated + warn_deprecated( + "all_user_commands is deprecated and will be removed in a future version. " + "Use all_app_commands as a replacement.", + stacklevel=3, + ) + return { + cmd.name: cmd + for cmd in self.all_app_commands.values() + if isinstance(cmd, InvokableUserCommand) + } + + @property + def all_message_commands(self) -> Dict[str, InvokableMessageCommand]: + # no docstring because it was an attribute and now it's deprecated + warn_deprecated( + "all_message_commands is deprecated and will be removed in a future version. " + "Use all_app_commands as a replacement.", + stacklevel=3, + ) + return { + cmd.name: cmd + for cmd in self.all_app_commands.values() + if isinstance(cmd, InvokableMessageCommand) + } + def add_app_command(self, app_command: InvokableApplicationCommand) -> None: """Adds an :class:`InvokableApplicationCommand` into the internal list of app commands. @@ -327,7 +368,9 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: # localization may be called multiple times for the same command but it's harmless app_command.body.localize(self.i18n) - + # note that we're adding the same command object for each guild_id + # this ensures that any changes that happen to app_command after add_app_command + # (such as hook attachments or permission modifications) apply properly self.all_app_commands[cmd_index] = app_command def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: @@ -428,11 +471,12 @@ def remove_app_command( """ if guild_ids is None: - extended_guild_ids = (None,) # a global command may end up being a local command if test_guilds were specified # so we should remove this "global" command from each test guild if self._test_guilds is not None: - extended_guild_ids += self._test_guilds + extended_guild_ids = self._test_guilds + else: + extended_guild_ids = (None,) else: extended_guild_ids = guild_ids @@ -442,6 +486,7 @@ def remove_app_command( cmd = self.all_app_commands.pop(cmd_index, None) if result is None: result = cmd + return result def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name: str) -> Any: @@ -552,7 +597,7 @@ def get_slash_command( if command is None: return None return _match_subcommand_chain(command, chain) # type: ignore - + # this is mostly for backwards compatibility, as previously guild_id arg didn't exist for command in self.all_app_commands.values(): if not isinstance(command, InvokableSlashCommand): continue @@ -587,7 +632,7 @@ def get_user_command( if command is None: return None return command # type: ignore - + # this is mostly for backwards compatibility, as previously guild_id arg didn't exist for command in self.all_app_commands.values(): if isinstance(command, InvokableUserCommand) and command.name == name: return command @@ -619,7 +664,7 @@ def get_message_command( if command is None: return None return command # type: ignore - + # this is mostly for backwards compatibility, as previously guild_id arg didn't exist for command in self.all_app_commands.values(): if isinstance(command, InvokableMessageCommand) and command.name == name: return command @@ -875,9 +920,15 @@ def decorator( # command synchronisation def _ordered_unsynced_commands( - self, test_guilds: Optional[Sequence[int]] = None + self, test_guilds: Optional[Sequence[int]] = MISSING ) -> Tuple[List[ApplicationCommand], Dict[int, List[ApplicationCommand]]]: - # TODO: deprecation warning for the test_guilds arg + if test_guilds is not MISSING: + warn_deprecated( + "Argument test_guilds of _ordered_unsynced_commands is deprecated " + "and will be removed in a future version.", + stacklevel=3, + ) + global_cmds = [] guilds = {} From ec3896d0b6f42d99ed9f9d329a61351d53b35680 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 18 Sep 2023 15:09:37 +0300 Subject: [PATCH 03/67] Fix a bad check --- disnake/ext/commands/interaction_bot_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 6ff5f490ad..44a70793f7 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -350,8 +350,8 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") - if not isinstance(slash_command, InvokableSlashCommand): - raise TypeError("The slash_command passed must be an instance of InvokableSlashCommand") + if not isinstance(app_command, InvokableApplicationCommand): + raise TypeError("The app_command passed must be an instance of InvokableApplicationCommand") if app_command.guild_ids is None: # if test_guilds are specified then we add the same command for each test guild From ea1ad6586061cb773a7f59c80c7bf69ca8e7963a Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 18 Sep 2023 15:38:16 +0300 Subject: [PATCH 04/67] Fix linting issues --- disnake/ext/commands/errors.py | 4 +++- disnake/ext/commands/interaction_bot_base.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 5b65e40f48..3e9a9360b6 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1045,7 +1045,9 @@ class AppCommandRegistrationError(CommandRegistrationError): or ``None`` if it was a global command. """ - def __init__(self, cmd_type: ApplicationCommandType, name: str, guild_id: Optional[int]) -> None: + def __init__( + self, cmd_type: ApplicationCommandType, name: str, guild_id: Optional[int] + ) -> None: self.cmd_type: ApplicationCommandType = cmd_type self.name: str = name self.guild_id: Optional[int] = guild_id diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 44a70793f7..7df3cbe0b7 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -282,7 +282,9 @@ def user_commands(self) -> Set[InvokableUserCommand]: def message_commands(self) -> Set[InvokableMessageCommand]: """Set[:class:`InvokableMessageCommand`]: A set of all message commands the bot has.""" return set( - cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableMessageCommand) + cmd + for cmd in self.all_app_commands.values() + if isinstance(cmd, InvokableMessageCommand) ) @property @@ -351,7 +353,9 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: raise NotImplementedError("This method is only usable in disnake.Client subclasses") if not isinstance(app_command, InvokableApplicationCommand): - raise TypeError("The app_command passed must be an instance of InvokableApplicationCommand") + raise TypeError( + "The app_command passed must be an instance of InvokableApplicationCommand" + ) if app_command.guild_ids is None: # if test_guilds are specified then we add the same command for each test guild @@ -364,7 +368,9 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: type=app_command.body.type, name=app_command.name, guild_id=guild_id ) if cmd_index in self.all_app_commands: - raise AppCommandRegistrationError(cmd_index.type, cmd_index.name, cmd_index.guild_id) + raise AppCommandRegistrationError( + cmd_index.type, cmd_index.name, cmd_index.guild_id + ) # localization may be called multiple times for the same command but it's harmless app_command.body.localize(self.i18n) @@ -625,9 +631,7 @@ def get_user_command( The user command that was requested. If not found, returns ``None``. """ if guild_id is not MISSING: - cmd_index = AppCmdIndex( - type=ApplicationCommandType.user, name=name, guild_id=guild_id - ) + cmd_index = AppCmdIndex(type=ApplicationCommandType.user, name=name, guild_id=guild_id) command = self.all_app_commands.get(cmd_index) if command is None: return None From 424ed03e8790b8cde17e00bc0b39c279401520ea Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:10:20 +0300 Subject: [PATCH 05/67] Update docs and add changelog --- changelog/260.bugfix.rst | 1 + changelog/260.deprecate.0.rst | 1 + changelog/260.deprecate.1.rst | 1 + changelog/260.deprecate.2.rst | 1 + changelog/260.deprecate.3.rst | 1 + changelog/260.deprecate.4.rst | 1 + changelog/260.deprecate.5.rst | 1 + changelog/260.deprecate.6.rst | 1 + changelog/260.deprecate.7.rst | 1 + changelog/260.deprecate.8.rst | 1 + changelog/260.feature.0.rst | 1 + changelog/260.feature.1.rst | 1 + changelog/260.feature.2.rst | 1 + changelog/260.feature.3.rst | 1 + changelog/260.feature.4.rst | 1 + changelog/260.feature.5.rst | 1 + disnake/ext/commands/errors.py | 1 + docs/ext/commands/api/exceptions.rst | 3 +++ 18 files changed, 20 insertions(+) create mode 100644 changelog/260.bugfix.rst create mode 100644 changelog/260.deprecate.0.rst create mode 100644 changelog/260.deprecate.1.rst create mode 100644 changelog/260.deprecate.2.rst create mode 100644 changelog/260.deprecate.3.rst create mode 100644 changelog/260.deprecate.4.rst create mode 100644 changelog/260.deprecate.5.rst create mode 100644 changelog/260.deprecate.6.rst create mode 100644 changelog/260.deprecate.7.rst create mode 100644 changelog/260.deprecate.8.rst create mode 100644 changelog/260.feature.0.rst create mode 100644 changelog/260.feature.1.rst create mode 100644 changelog/260.feature.2.rst create mode 100644 changelog/260.feature.3.rst create mode 100644 changelog/260.feature.4.rst create mode 100644 changelog/260.feature.5.rst diff --git a/changelog/260.bugfix.rst b/changelog/260.bugfix.rst new file mode 100644 index 0000000000..3a77eb513c --- /dev/null +++ b/changelog/260.bugfix.rst @@ -0,0 +1 @@ +|commands| Allow registering 2 commands with the same type and name in different guilds. diff --git a/changelog/260.deprecate.0.rst b/changelog/260.deprecate.0.rst new file mode 100644 index 0000000000..5f001a5a2f --- /dev/null +++ b/changelog/260.deprecate.0.rst @@ -0,0 +1 @@ +|commands| :attr:`InteractionBotBase.all_slash_commands` is deprecated. diff --git a/changelog/260.deprecate.1.rst b/changelog/260.deprecate.1.rst new file mode 100644 index 0000000000..b8f41dbdff --- /dev/null +++ b/changelog/260.deprecate.1.rst @@ -0,0 +1 @@ +|commands| :attr:`InteractionBotBase.all_user_commands` is deprecated. diff --git a/changelog/260.deprecate.2.rst b/changelog/260.deprecate.2.rst new file mode 100644 index 0000000000..7eb5aad372 --- /dev/null +++ b/changelog/260.deprecate.2.rst @@ -0,0 +1 @@ +|commands| :attr:`InteractionBotBase.all_message_commands` is deprecated. diff --git a/changelog/260.deprecate.3.rst b/changelog/260.deprecate.3.rst new file mode 100644 index 0000000000..bd6cfc053d --- /dev/null +++ b/changelog/260.deprecate.3.rst @@ -0,0 +1 @@ +|commands| :meth:`InteractionBotBase.add_slash_command` is deprecated. diff --git a/changelog/260.deprecate.4.rst b/changelog/260.deprecate.4.rst new file mode 100644 index 0000000000..371f949640 --- /dev/null +++ b/changelog/260.deprecate.4.rst @@ -0,0 +1 @@ +|commands| :meth:`InteractionBotBase.add_user_command` is deprecated. diff --git a/changelog/260.deprecate.5.rst b/changelog/260.deprecate.5.rst new file mode 100644 index 0000000000..ccd4e28be5 --- /dev/null +++ b/changelog/260.deprecate.5.rst @@ -0,0 +1 @@ +|commands| :meth:`InteractionBotBase.add_message_command` is deprecated. diff --git a/changelog/260.deprecate.6.rst b/changelog/260.deprecate.6.rst new file mode 100644 index 0000000000..117b504a0a --- /dev/null +++ b/changelog/260.deprecate.6.rst @@ -0,0 +1 @@ +|commands| :meth:`InteractionBotBase.remove_slash_command` is deprecated. diff --git a/changelog/260.deprecate.7.rst b/changelog/260.deprecate.7.rst new file mode 100644 index 0000000000..c1930a4bc2 --- /dev/null +++ b/changelog/260.deprecate.7.rst @@ -0,0 +1 @@ +|commands| :meth:`InteractionBotBase.remove_user_command` is deprecated. diff --git a/changelog/260.deprecate.8.rst b/changelog/260.deprecate.8.rst new file mode 100644 index 0000000000..c45e1eee27 --- /dev/null +++ b/changelog/260.deprecate.8.rst @@ -0,0 +1 @@ +|commands| :meth:`InteractionBotBase.remove_message_command` is deprecated. diff --git a/changelog/260.feature.0.rst b/changelog/260.feature.0.rst new file mode 100644 index 0000000000..b10c4b3332 --- /dev/null +++ b/changelog/260.feature.0.rst @@ -0,0 +1 @@ +|commands| Add :meth:`InteractionBotBase.add_app_command`. diff --git a/changelog/260.feature.1.rst b/changelog/260.feature.1.rst new file mode 100644 index 0000000000..e6a715b02b --- /dev/null +++ b/changelog/260.feature.1.rst @@ -0,0 +1 @@ +|commands| Add :meth:`InteractionBotBase.remove_app_command`. diff --git a/changelog/260.feature.2.rst b/changelog/260.feature.2.rst new file mode 100644 index 0000000000..aa06ab03e5 --- /dev/null +++ b/changelog/260.feature.2.rst @@ -0,0 +1 @@ +|commands| Add ``guild_id`` parameter to :meth:`InteractionBotBase.get_slash_command`. diff --git a/changelog/260.feature.3.rst b/changelog/260.feature.3.rst new file mode 100644 index 0000000000..da14bd48b8 --- /dev/null +++ b/changelog/260.feature.3.rst @@ -0,0 +1 @@ +|commands| Add ``guild_id`` parameter to :meth:`InteractionBotBase.get_user_command`. diff --git a/changelog/260.feature.4.rst b/changelog/260.feature.4.rst new file mode 100644 index 0000000000..6523484883 --- /dev/null +++ b/changelog/260.feature.4.rst @@ -0,0 +1 @@ +|commands| Add ``guild_id`` parameter to :meth:`InteractionBotBase.get_message_command`. diff --git a/changelog/260.feature.5.rst b/changelog/260.feature.5.rst new file mode 100644 index 0000000000..fb1c42fdee --- /dev/null +++ b/changelog/260.feature.5.rst @@ -0,0 +1 @@ +|commands| Add :class:`AppCommandRegistrationError`. diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 3e9a9360b6..f2fdb2893f 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1023,6 +1023,7 @@ def __init__(self, name: str, *, alias_conflict: bool = False) -> None: type_ = "alias" if alias_conflict else "command" super().__init__(f"The {type_} {name} is already an existing command or alias.") + # we inherit CommandRegistrationError for backwards compatibility, # because this error replaced CommandRegistrationError in several places class AppCommandRegistrationError(CommandRegistrationError): diff --git a/docs/ext/commands/api/exceptions.rst b/docs/ext/commands/api/exceptions.rst index bde1d7d446..4dd874f51e 100644 --- a/docs/ext/commands/api/exceptions.rst +++ b/docs/ext/commands/api/exceptions.rst @@ -186,6 +186,9 @@ Exceptions .. autoexception:: CommandRegistrationError :members: +.. autoexception:: AppCommandRegistrationError + :members: + Exception Hierarchy ~~~~~~~~~~~~~~~~~~~ From a2b067727ea91a4992175e1c37110db36d757259 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:31:50 +0300 Subject: [PATCH 06/67] Avoid unnecessary generators --- disnake/ext/commands/interaction_bot_base.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 7df3cbe0b7..83f7b8ddb7 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -267,25 +267,25 @@ def application_commands(self) -> Set[InvokableApplicationCommand]: @property def slash_commands(self) -> Set[InvokableSlashCommand]: """Set[:class:`InvokableSlashCommand`]: A set of all slash commands the bot has.""" - return set( + return { cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableSlashCommand) - ) + } @property def user_commands(self) -> Set[InvokableUserCommand]: """Set[:class:`InvokableUserCommand`]: A set of all user commands the bot has.""" - return set( + return { cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableUserCommand) - ) + } @property def message_commands(self) -> Set[InvokableMessageCommand]: """Set[:class:`InvokableMessageCommand`]: A set of all message commands the bot has.""" - return set( + return { cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableMessageCommand) - ) + } @property def all_slash_commands(self) -> Dict[str, InvokableSlashCommand]: @@ -348,7 +348,6 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: TypeError The app command passed is not an instance of :class:`InvokableApplicationCommand`. """ - if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") @@ -475,7 +474,6 @@ def remove_app_command( Optional[:class:`InvokableApplicationCommand`] The app command that was removed. If the key data was not valid then ``None`` is returned instead. """ - if guild_ids is None: # a global command may end up being a local command if test_guilds were specified # so we should remove this "global" command from each test guild From dbd7920210bead4d60d855cd45382798c967c583 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 18 Sep 2023 17:07:38 +0300 Subject: [PATCH 07/67] Fix docs references --- disnake/ext/commands/errors.py | 4 ++-- disnake/ext/commands/interaction_bot_base.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index f2fdb2893f..e7ecd8cf28 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1031,13 +1031,13 @@ class AppCommandRegistrationError(CommandRegistrationError): because a command with the same key already exists. A key is determined by command type, name, and guild_id. - This inherits from :exc:`disnake.CommandRegistrationError` + This inherits from :exc:`CommandRegistrationError` .. versionadded:: 2.10 Attributes ---------- - cmd_type: :class:`ApplicationCommandType` + cmd_type: :class:`disnake.ApplicationCommandType` The command type. name: :class:`str` The command name. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 83f7b8ddb7..df7b00ecfa 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -462,7 +462,7 @@ def remove_app_command( Parameters ---------- - cmd_type: :class:`ApplicationCommandType` + cmd_type: :class:`disnake.ApplicationCommandType` The type of the app command to remove. name: :class:`str` The name of the app command to remove. From e86e5cb646a2c3ba99c2bb0318acd3f2fec51572 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:05:20 +0300 Subject: [PATCH 08/67] Add errors for ambiguous app command searches --- disnake/ext/commands/interaction_bot_base.py | 72 +++++++++++++++++--- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index df7b00ecfa..eac94aec4e 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -149,6 +149,9 @@ def _format_diff(diff: _Diff) -> str: def _match_subcommand_chain( command: InvokableSlashCommand, chain: List[str] ) -> Union[InvokableSlashCommand, SubCommand, SubCommandGroup, None]: + """This is an internal function that returns a subcommand with a route matching the chain. + If there's no match then ``None`` is returned. + """ if command.name != chain[0]: return None if len(chain) == 1: @@ -332,7 +335,8 @@ def all_message_commands(self) -> Dict[str, InvokableMessageCommand]: def add_app_command(self, app_command: InvokableApplicationCommand) -> None: """Adds an :class:`InvokableApplicationCommand` into the internal list of app commands. - This is usually not called, instead shortcut decorators are used. + This is usually not called, instead shortcut decorators are used, such as + :meth:`.slash_command`, :meth:`.user_command` or :meth:`.message_command`. .. versionadded:: 2.10 @@ -583,6 +587,9 @@ def get_slash_command( ------ TypeError The name is not a string. + ValueError + Parameter ``guild_id`` was not provided in a case where different slash commands + have the same name but different guild_ids. Returns ------- @@ -601,13 +608,28 @@ def get_slash_command( if command is None: return None return _match_subcommand_chain(command, chain) # type: ignore + # this is mostly for backwards compatibility, as previously guild_id arg didn't exist + result = None for command in self.all_app_commands.values(): if not isinstance(command, InvokableSlashCommand): continue - result = _match_subcommand_chain(command, chain) - if result is not None: - return result + chain_match = _match_subcommand_chain(command, chain) + if chain_match is None: + continue + if result is None: + result = chain_match + continue + # we should check whether there's an ambiguity in command search + result_guild_ids = (result.root_parent or result).guild_ids + match_guild_ids = (chain_match.root_parent or chain_match).guild_ids + if result_guild_ids != match_guild_ids: + raise ValueError( + "Argument guild_id must be provided if there're different slash commands " + "with the same name but different guilds or one of them is global." + ) + + return result def get_user_command( self, name: str, guild_id: Optional[int] = MISSING @@ -623,6 +645,12 @@ def get_user_command( The guild ID corresponding to the user command or ``None`` if it's a global command. If this is not specified then the first match is returned instead. + Raises + ------ + ValueError + Parameter ``guild_id`` was not provided in a case where different user commands + have the same name but different guild_ids. + Returns ------- Optional[:class:`InvokableUserCommand`] @@ -635,9 +663,20 @@ def get_user_command( return None return command # type: ignore # this is mostly for backwards compatibility, as previously guild_id arg didn't exist + result = None for command in self.all_app_commands.values(): - if isinstance(command, InvokableUserCommand) and command.name == name: - return command + if not isinstance(command, InvokableUserCommand) or command.name != name: + continue + if result is None: + result = command + # we should check whether there's an ambiguity in command search + elif result.guild_ids != command.guild_ids: + raise ValueError( + "Argument guild_id must be provided if there're different user commands " + "with the same name but different guilds or one of them is global." + ) + + return result def get_message_command( self, name: str, guild_id: Optional[int] = MISSING @@ -653,6 +692,12 @@ def get_message_command( The guild ID corresponding to the message command or ``None`` if it's a global command. If this is not specified then the first match is returned instead. + Raises + ------ + ValueError + Parameter ``guild_id`` was not provided in a case where different message commands + have the same name but different guild_ids. + Returns ------- Optional[:class:`InvokableMessageCommand`] @@ -667,9 +712,20 @@ def get_message_command( return None return command # type: ignore # this is mostly for backwards compatibility, as previously guild_id arg didn't exist + result = None for command in self.all_app_commands.values(): - if isinstance(command, InvokableMessageCommand) and command.name == name: - return command + if not isinstance(command, InvokableMessageCommand) or command.name != name: + continue + if result is None: + result = command + # we should check whether there's an ambiguity in command search + elif result.guild_ids != command.guild_ids: + raise ValueError( + "Argument guild_id must be provided if there're different message commands " + "with the same name but different guilds or one of them is global." + ) + + return result def slash_command( self, From f80dbf69c52a9f07c5c398381e10a83128241058 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:07:21 +0300 Subject: [PATCH 09/67] First word of the docstring should not be "This" --- disnake/ext/commands/interaction_bot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index eac94aec4e..576f047674 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -149,7 +149,7 @@ def _format_diff(diff: _Diff) -> str: def _match_subcommand_chain( command: InvokableSlashCommand, chain: List[str] ) -> Union[InvokableSlashCommand, SubCommand, SubCommandGroup, None]: - """This is an internal function that returns a subcommand with a route matching the chain. + """An internal function that returns a subcommand with a route matching the chain. If there's no match then ``None`` is returned. """ if command.name != chain[0]: From 4ed930d4b2ee55ec19506601c26279a5182e5a3a Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 22 Sep 2023 23:08:32 +0300 Subject: [PATCH 10/67] docs: group some changelog entries --- changelog/260.deprecate.0.rst | 4 +++- changelog/260.deprecate.1.rst | 4 +++- changelog/260.deprecate.2.rst | 4 +++- changelog/260.deprecate.3.rst | 1 - changelog/260.deprecate.4.rst | 1 - changelog/260.deprecate.5.rst | 1 - changelog/260.deprecate.6.rst | 1 - changelog/260.deprecate.7.rst | 1 - changelog/260.deprecate.8.rst | 1 - changelog/260.feature.0.rst | 2 +- changelog/260.feature.1.rst | 3 ++- changelog/260.feature.2.rst | 2 +- changelog/260.feature.3.rst | 1 - changelog/260.feature.4.rst | 1 - changelog/260.feature.5.rst | 1 - 15 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 changelog/260.deprecate.3.rst delete mode 100644 changelog/260.deprecate.4.rst delete mode 100644 changelog/260.deprecate.5.rst delete mode 100644 changelog/260.deprecate.6.rst delete mode 100644 changelog/260.deprecate.7.rst delete mode 100644 changelog/260.deprecate.8.rst delete mode 100644 changelog/260.feature.3.rst delete mode 100644 changelog/260.feature.4.rst delete mode 100644 changelog/260.feature.5.rst diff --git a/changelog/260.deprecate.0.rst b/changelog/260.deprecate.0.rst index 5f001a5a2f..6253334c55 100644 --- a/changelog/260.deprecate.0.rst +++ b/changelog/260.deprecate.0.rst @@ -1 +1,3 @@ -|commands| :attr:`InteractionBotBase.all_slash_commands` is deprecated. +|commands| :attr:`InteractionBot.all_slash_commands`, +:attr:`InteractionBot.all_user_commands`, +:attr:`InteractionBot.all_message_commands` are deprecated. diff --git a/changelog/260.deprecate.1.rst b/changelog/260.deprecate.1.rst index b8f41dbdff..4267953adb 100644 --- a/changelog/260.deprecate.1.rst +++ b/changelog/260.deprecate.1.rst @@ -1 +1,3 @@ -|commands| :attr:`InteractionBotBase.all_user_commands` is deprecated. +|commands| :meth:`InteractionBot.add_slash_command`, +:meth:`InteractionBot.add_user_command`, +:meth:`InteractionBot.add_message_command` are deprecated. diff --git a/changelog/260.deprecate.2.rst b/changelog/260.deprecate.2.rst index 7eb5aad372..35848c65b7 100644 --- a/changelog/260.deprecate.2.rst +++ b/changelog/260.deprecate.2.rst @@ -1 +1,3 @@ -|commands| :attr:`InteractionBotBase.all_message_commands` is deprecated. +|commands| :meth:`InteractionBot.remove_slash_command`, +:meth:`InteractionBot.remove_user_command`, +:meth:`InteractionBot.remove_message_command` are deprecated. diff --git a/changelog/260.deprecate.3.rst b/changelog/260.deprecate.3.rst deleted file mode 100644 index bd6cfc053d..0000000000 --- a/changelog/260.deprecate.3.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`InteractionBotBase.add_slash_command` is deprecated. diff --git a/changelog/260.deprecate.4.rst b/changelog/260.deprecate.4.rst deleted file mode 100644 index 371f949640..0000000000 --- a/changelog/260.deprecate.4.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`InteractionBotBase.add_user_command` is deprecated. diff --git a/changelog/260.deprecate.5.rst b/changelog/260.deprecate.5.rst deleted file mode 100644 index ccd4e28be5..0000000000 --- a/changelog/260.deprecate.5.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`InteractionBotBase.add_message_command` is deprecated. diff --git a/changelog/260.deprecate.6.rst b/changelog/260.deprecate.6.rst deleted file mode 100644 index 117b504a0a..0000000000 --- a/changelog/260.deprecate.6.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`InteractionBotBase.remove_slash_command` is deprecated. diff --git a/changelog/260.deprecate.7.rst b/changelog/260.deprecate.7.rst deleted file mode 100644 index c1930a4bc2..0000000000 --- a/changelog/260.deprecate.7.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`InteractionBotBase.remove_user_command` is deprecated. diff --git a/changelog/260.deprecate.8.rst b/changelog/260.deprecate.8.rst deleted file mode 100644 index c45e1eee27..0000000000 --- a/changelog/260.deprecate.8.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`InteractionBotBase.remove_message_command` is deprecated. diff --git a/changelog/260.feature.0.rst b/changelog/260.feature.0.rst index b10c4b3332..a9d250f21d 100644 --- a/changelog/260.feature.0.rst +++ b/changelog/260.feature.0.rst @@ -1 +1 @@ -|commands| Add :meth:`InteractionBotBase.add_app_command`. +|commands| Add :meth:`InteractionBot.add_app_command` and :meth:`InteractionBot.remove_app_command`. diff --git a/changelog/260.feature.1.rst b/changelog/260.feature.1.rst index e6a715b02b..494986d99b 100644 --- a/changelog/260.feature.1.rst +++ b/changelog/260.feature.1.rst @@ -1 +1,2 @@ -|commands| Add :meth:`InteractionBotBase.remove_app_command`. +|commands| Add ``guild_id`` parameter to :meth:`InteractionBot.get_slash_command`, +:meth:`InteractionBot.get_user_command`, and :meth:`InteractionBot.get_message_command`. diff --git a/changelog/260.feature.2.rst b/changelog/260.feature.2.rst index aa06ab03e5..fb1c42fdee 100644 --- a/changelog/260.feature.2.rst +++ b/changelog/260.feature.2.rst @@ -1 +1 @@ -|commands| Add ``guild_id`` parameter to :meth:`InteractionBotBase.get_slash_command`. +|commands| Add :class:`AppCommandRegistrationError`. diff --git a/changelog/260.feature.3.rst b/changelog/260.feature.3.rst deleted file mode 100644 index da14bd48b8..0000000000 --- a/changelog/260.feature.3.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| Add ``guild_id`` parameter to :meth:`InteractionBotBase.get_user_command`. diff --git a/changelog/260.feature.4.rst b/changelog/260.feature.4.rst deleted file mode 100644 index 6523484883..0000000000 --- a/changelog/260.feature.4.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| Add ``guild_id`` parameter to :meth:`InteractionBotBase.get_message_command`. diff --git a/changelog/260.feature.5.rst b/changelog/260.feature.5.rst deleted file mode 100644 index fb1c42fdee..0000000000 --- a/changelog/260.feature.5.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| Add :class:`AppCommandRegistrationError`. From 192d2b93f1c6ed5969635c9db7f13d29f880badc Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 22 Sep 2023 23:14:32 +0300 Subject: [PATCH 11/67] chore: rename `AppCommandRegistrationError` Also adds `alias_conflict` attribute for backwards compatibility --- changelog/260.feature.2.rst | 2 +- disnake/ext/commands/errors.py | 6 ++++-- disnake/ext/commands/interaction_bot_base.py | 6 +++--- docs/ext/commands/api/exceptions.rst | 3 ++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/changelog/260.feature.2.rst b/changelog/260.feature.2.rst index fb1c42fdee..7abeb8d277 100644 --- a/changelog/260.feature.2.rst +++ b/changelog/260.feature.2.rst @@ -1 +1 @@ -|commands| Add :class:`AppCommandRegistrationError`. +|commands| Add :class:`ApplicationCommandRegistrationError`. diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index e7ecd8cf28..515951ffda 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -75,7 +75,7 @@ "ExtensionFailed", "ExtensionNotFound", "CommandRegistrationError", - "AppCommandRegistrationError", + "ApplicationCommandRegistrationError", "FlagError", "BadFlagArgument", "MissingFlagArgument", @@ -1026,7 +1026,7 @@ def __init__(self, name: str, *, alias_conflict: bool = False) -> None: # we inherit CommandRegistrationError for backwards compatibility, # because this error replaced CommandRegistrationError in several places -class AppCommandRegistrationError(CommandRegistrationError): +class ApplicationCommandRegistrationError(CommandRegistrationError): """An exception raised when the app command can't be added because a command with the same key already exists. A key is determined by command type, name, and guild_id. @@ -1052,6 +1052,8 @@ def __init__( self.cmd_type: ApplicationCommandType = cmd_type self.name: str = name self.guild_id: Optional[int] = guild_id + # backwards compatibility + self.alias_conflict: bool = False # fixed API naming here because no one calls slash commands "chat input" type_ = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name if guild_id is None: diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 576f047674..4c69f06335 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -40,7 +40,7 @@ message_command, user_command, ) -from .errors import AppCommandRegistrationError +from .errors import ApplicationCommandRegistrationError from .flags import CommandSyncFlags from .slash_core import InvokableSlashCommand, SubCommand, SubCommandGroup, slash_command @@ -347,7 +347,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: Raises ------ - AppCommandRegistrationError + ApplicationCommandRegistrationError The app command is already registered. TypeError The app command passed is not an instance of :class:`InvokableApplicationCommand`. @@ -371,7 +371,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: type=app_command.body.type, name=app_command.name, guild_id=guild_id ) if cmd_index in self.all_app_commands: - raise AppCommandRegistrationError( + raise ApplicationCommandRegistrationError( cmd_index.type, cmd_index.name, cmd_index.guild_id ) diff --git a/docs/ext/commands/api/exceptions.rst b/docs/ext/commands/api/exceptions.rst index 4dd874f51e..094ac5dbbc 100644 --- a/docs/ext/commands/api/exceptions.rst +++ b/docs/ext/commands/api/exceptions.rst @@ -186,7 +186,7 @@ Exceptions .. autoexception:: CommandRegistrationError :members: -.. autoexception:: AppCommandRegistrationError +.. autoexception:: ApplicationCommandRegistrationError :members: @@ -255,6 +255,7 @@ Exception Hierarchy - :exc:`ExtensionNotFound` - :exc:`~.ClientException` - :exc:`CommandRegistrationError` + - :exc:`ApplicationCommandRegistrationError` Warnings -------- From 686bd488c6cd3c5ce853b77ac4bc73d23fab7966 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sat, 23 Sep 2023 02:32:02 +0300 Subject: [PATCH 12/67] docs: fix references in changelog entries --- changelog/260.deprecate.0.rst | 4 +--- changelog/260.deprecate.1.rst | 4 +--- changelog/260.deprecate.2.rst | 4 +--- changelog/260.feature.0.rst | 2 +- changelog/260.feature.1.rst | 3 +-- changelog/260.feature.2.rst | 2 +- 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/changelog/260.deprecate.0.rst b/changelog/260.deprecate.0.rst index 6253334c55..51658a46cd 100644 --- a/changelog/260.deprecate.0.rst +++ b/changelog/260.deprecate.0.rst @@ -1,3 +1 @@ -|commands| :attr:`InteractionBot.all_slash_commands`, -:attr:`InteractionBot.all_user_commands`, -:attr:`InteractionBot.all_message_commands` are deprecated. +|commands| :attr:`.ext.commands.InteractionBot.all_slash_commands`, :attr:`.ext.commands.InteractionBot.all_user_commands`, :attr:`.ext.commands.InteractionBot.all_message_commands` are deprecated. diff --git a/changelog/260.deprecate.1.rst b/changelog/260.deprecate.1.rst index 4267953adb..d383c8394b 100644 --- a/changelog/260.deprecate.1.rst +++ b/changelog/260.deprecate.1.rst @@ -1,3 +1 @@ -|commands| :meth:`InteractionBot.add_slash_command`, -:meth:`InteractionBot.add_user_command`, -:meth:`InteractionBot.add_message_command` are deprecated. +|commands| :meth:`.ext.commands.InteractionBot.add_slash_command`, :meth:`.ext.commands.InteractionBot.add_user_command`, :meth:`.ext.commands.InteractionBot.add_message_command` are deprecated. diff --git a/changelog/260.deprecate.2.rst b/changelog/260.deprecate.2.rst index 35848c65b7..bf20c12a10 100644 --- a/changelog/260.deprecate.2.rst +++ b/changelog/260.deprecate.2.rst @@ -1,3 +1 @@ -|commands| :meth:`InteractionBot.remove_slash_command`, -:meth:`InteractionBot.remove_user_command`, -:meth:`InteractionBot.remove_message_command` are deprecated. +|commands| :meth:`.ext.commands.InteractionBot.remove_slash_command`, :meth:`.ext.commands.InteractionBot.remove_user_command`, :meth:`.ext.commands.InteractionBot.remove_message_command` are deprecated. diff --git a/changelog/260.feature.0.rst b/changelog/260.feature.0.rst index a9d250f21d..2c5ae38652 100644 --- a/changelog/260.feature.0.rst +++ b/changelog/260.feature.0.rst @@ -1 +1 @@ -|commands| Add :meth:`InteractionBot.add_app_command` and :meth:`InteractionBot.remove_app_command`. +|commands| Add :meth:`.ext.commands.InteractionBot.add_app_command` and :meth:`.ext.commands.InteractionBot.remove_app_command`. diff --git a/changelog/260.feature.1.rst b/changelog/260.feature.1.rst index 494986d99b..e48cb5a22e 100644 --- a/changelog/260.feature.1.rst +++ b/changelog/260.feature.1.rst @@ -1,2 +1 @@ -|commands| Add ``guild_id`` parameter to :meth:`InteractionBot.get_slash_command`, -:meth:`InteractionBot.get_user_command`, and :meth:`InteractionBot.get_message_command`. +|commands| Add ``guild_id`` parameter to :meth:`.ext.commands.InteractionBot.get_slash_command`, :meth:`.ext.commands.InteractionBot.get_user_command`, and :meth:`.ext.commands.InteractionBot.get_message_command`. diff --git a/changelog/260.feature.2.rst b/changelog/260.feature.2.rst index 7abeb8d277..191f973b7e 100644 --- a/changelog/260.feature.2.rst +++ b/changelog/260.feature.2.rst @@ -1 +1 @@ -|commands| Add :class:`ApplicationCommandRegistrationError`. +|commands| Add :class:`.ext.commands.ApplicationCommandRegistrationError`. From 8b5916c266581c3e877651299b648b5a9afe44f8 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sat, 23 Sep 2023 02:41:41 +0300 Subject: [PATCH 13/67] docs: hide the `ext.commands` prefix in changelog entries --- changelog/260.deprecate.0.rst | 2 +- changelog/260.deprecate.1.rst | 2 +- changelog/260.deprecate.2.rst | 2 +- changelog/260.feature.0.rst | 2 +- changelog/260.feature.1.rst | 2 +- changelog/260.feature.2.rst | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/changelog/260.deprecate.0.rst b/changelog/260.deprecate.0.rst index 51658a46cd..d268a8b7c0 100644 --- a/changelog/260.deprecate.0.rst +++ b/changelog/260.deprecate.0.rst @@ -1 +1 @@ -|commands| :attr:`.ext.commands.InteractionBot.all_slash_commands`, :attr:`.ext.commands.InteractionBot.all_user_commands`, :attr:`.ext.commands.InteractionBot.all_message_commands` are deprecated. +|commands| :attr:`~disnake.ext.commands.InteractionBot.all_slash_commands`, :attr:`~disnake.ext.commands.InteractionBot.all_user_commands`, :attr:`~disnake.ext.commands.InteractionBot.all_message_commands` are deprecated. diff --git a/changelog/260.deprecate.1.rst b/changelog/260.deprecate.1.rst index d383c8394b..016ce80e2a 100644 --- a/changelog/260.deprecate.1.rst +++ b/changelog/260.deprecate.1.rst @@ -1 +1 @@ -|commands| :meth:`.ext.commands.InteractionBot.add_slash_command`, :meth:`.ext.commands.InteractionBot.add_user_command`, :meth:`.ext.commands.InteractionBot.add_message_command` are deprecated. +|commands| :meth:`~disnake.ext.commands.InteractionBot.add_slash_command`, :meth:`~disnake.ext.commands.InteractionBot.add_user_command`, :meth:`~disnake.ext.commands.InteractionBot.add_message_command` are deprecated. diff --git a/changelog/260.deprecate.2.rst b/changelog/260.deprecate.2.rst index bf20c12a10..d59cb2a5da 100644 --- a/changelog/260.deprecate.2.rst +++ b/changelog/260.deprecate.2.rst @@ -1 +1 @@ -|commands| :meth:`.ext.commands.InteractionBot.remove_slash_command`, :meth:`.ext.commands.InteractionBot.remove_user_command`, :meth:`.ext.commands.InteractionBot.remove_message_command` are deprecated. +|commands| :meth:`~disnake.ext.commands.InteractionBot.remove_slash_command`, :meth:`~disnake.ext.commands.InteractionBot.remove_user_command`, :meth:`~disnake.ext.commands.InteractionBot.remove_message_command` are deprecated. diff --git a/changelog/260.feature.0.rst b/changelog/260.feature.0.rst index 2c5ae38652..2d7b1e472c 100644 --- a/changelog/260.feature.0.rst +++ b/changelog/260.feature.0.rst @@ -1 +1 @@ -|commands| Add :meth:`.ext.commands.InteractionBot.add_app_command` and :meth:`.ext.commands.InteractionBot.remove_app_command`. +|commands| Add :meth:`~disnake.ext.commands.InteractionBot.add_app_command` and :meth:`~disnake.ext.commands.InteractionBot.remove_app_command`. diff --git a/changelog/260.feature.1.rst b/changelog/260.feature.1.rst index e48cb5a22e..6961119fd3 100644 --- a/changelog/260.feature.1.rst +++ b/changelog/260.feature.1.rst @@ -1 +1 @@ -|commands| Add ``guild_id`` parameter to :meth:`.ext.commands.InteractionBot.get_slash_command`, :meth:`.ext.commands.InteractionBot.get_user_command`, and :meth:`.ext.commands.InteractionBot.get_message_command`. +|commands| Add ``guild_id`` parameter to :meth:`~disnake.ext.commands.InteractionBot.get_slash_command`, :meth:`~disnake.ext.commands.InteractionBot.get_user_command`, and :meth:`~disnake.ext.commands.InteractionBot.get_message_command`. diff --git a/changelog/260.feature.2.rst b/changelog/260.feature.2.rst index 191f973b7e..b04d1d380d 100644 --- a/changelog/260.feature.2.rst +++ b/changelog/260.feature.2.rst @@ -1 +1 @@ -|commands| Add :class:`.ext.commands.ApplicationCommandRegistrationError`. +|commands| Add :class:`~disnake.ext.commands.ApplicationCommandRegistrationError`. From 88584c71087b971ddacb291f32fa49fee4955697 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sat, 23 Sep 2023 10:58:03 +0300 Subject: [PATCH 14/67] docs: revert the removal of `ext.commands` prefix --- changelog/260.deprecate.0.rst | 2 +- changelog/260.deprecate.1.rst | 2 +- changelog/260.deprecate.2.rst | 2 +- changelog/260.feature.0.rst | 2 +- changelog/260.feature.1.rst | 2 +- changelog/260.feature.2.rst | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/changelog/260.deprecate.0.rst b/changelog/260.deprecate.0.rst index d268a8b7c0..51658a46cd 100644 --- a/changelog/260.deprecate.0.rst +++ b/changelog/260.deprecate.0.rst @@ -1 +1 @@ -|commands| :attr:`~disnake.ext.commands.InteractionBot.all_slash_commands`, :attr:`~disnake.ext.commands.InteractionBot.all_user_commands`, :attr:`~disnake.ext.commands.InteractionBot.all_message_commands` are deprecated. +|commands| :attr:`.ext.commands.InteractionBot.all_slash_commands`, :attr:`.ext.commands.InteractionBot.all_user_commands`, :attr:`.ext.commands.InteractionBot.all_message_commands` are deprecated. diff --git a/changelog/260.deprecate.1.rst b/changelog/260.deprecate.1.rst index 016ce80e2a..d383c8394b 100644 --- a/changelog/260.deprecate.1.rst +++ b/changelog/260.deprecate.1.rst @@ -1 +1 @@ -|commands| :meth:`~disnake.ext.commands.InteractionBot.add_slash_command`, :meth:`~disnake.ext.commands.InteractionBot.add_user_command`, :meth:`~disnake.ext.commands.InteractionBot.add_message_command` are deprecated. +|commands| :meth:`.ext.commands.InteractionBot.add_slash_command`, :meth:`.ext.commands.InteractionBot.add_user_command`, :meth:`.ext.commands.InteractionBot.add_message_command` are deprecated. diff --git a/changelog/260.deprecate.2.rst b/changelog/260.deprecate.2.rst index d59cb2a5da..bf20c12a10 100644 --- a/changelog/260.deprecate.2.rst +++ b/changelog/260.deprecate.2.rst @@ -1 +1 @@ -|commands| :meth:`~disnake.ext.commands.InteractionBot.remove_slash_command`, :meth:`~disnake.ext.commands.InteractionBot.remove_user_command`, :meth:`~disnake.ext.commands.InteractionBot.remove_message_command` are deprecated. +|commands| :meth:`.ext.commands.InteractionBot.remove_slash_command`, :meth:`.ext.commands.InteractionBot.remove_user_command`, :meth:`.ext.commands.InteractionBot.remove_message_command` are deprecated. diff --git a/changelog/260.feature.0.rst b/changelog/260.feature.0.rst index 2d7b1e472c..2c5ae38652 100644 --- a/changelog/260.feature.0.rst +++ b/changelog/260.feature.0.rst @@ -1 +1 @@ -|commands| Add :meth:`~disnake.ext.commands.InteractionBot.add_app_command` and :meth:`~disnake.ext.commands.InteractionBot.remove_app_command`. +|commands| Add :meth:`.ext.commands.InteractionBot.add_app_command` and :meth:`.ext.commands.InteractionBot.remove_app_command`. diff --git a/changelog/260.feature.1.rst b/changelog/260.feature.1.rst index 6961119fd3..e48cb5a22e 100644 --- a/changelog/260.feature.1.rst +++ b/changelog/260.feature.1.rst @@ -1 +1 @@ -|commands| Add ``guild_id`` parameter to :meth:`~disnake.ext.commands.InteractionBot.get_slash_command`, :meth:`~disnake.ext.commands.InteractionBot.get_user_command`, and :meth:`~disnake.ext.commands.InteractionBot.get_message_command`. +|commands| Add ``guild_id`` parameter to :meth:`.ext.commands.InteractionBot.get_slash_command`, :meth:`.ext.commands.InteractionBot.get_user_command`, and :meth:`.ext.commands.InteractionBot.get_message_command`. diff --git a/changelog/260.feature.2.rst b/changelog/260.feature.2.rst index b04d1d380d..191f973b7e 100644 --- a/changelog/260.feature.2.rst +++ b/changelog/260.feature.2.rst @@ -1 +1 @@ -|commands| Add :class:`~disnake.ext.commands.ApplicationCommandRegistrationError`. +|commands| Add :class:`.ext.commands.ApplicationCommandRegistrationError`. From 1f10a8df8f67b47d2ebb5257b82a2bd346227421 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sun, 24 Sep 2023 01:39:14 +0300 Subject: [PATCH 15/67] fix: subcommands trying to be top level commands --- disnake/ext/commands/cog.py | 4 +++- disnake/ext/commands/interaction_bot_base.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index 6c40a4bd6e..b21a1375bd 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -27,7 +27,7 @@ from ._types import _BaseCommand from .base_core import InvokableApplicationCommand from .ctx_menus_core import InvokableMessageCommand, InvokableUserCommand -from .slash_core import InvokableSlashCommand +from .slash_core import InvokableSlashCommand, SubCommand, SubCommandGroup if TYPE_CHECKING: from typing_extensions import Self @@ -753,6 +753,8 @@ def _inject(self, bot: AnyBot) -> Self: for index, command in enumerate(self.__cog_app_commands__): command.cog = self + if isinstance(command, (SubCommand, SubCommandGroup)): + continue try: bot.add_app_command(command) except Exception: diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 4c69f06335..35792f9941 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -359,6 +359,11 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: raise TypeError( "The app_command passed must be an instance of InvokableApplicationCommand" ) + if isinstance(app_command, (SubCommand, SubCommandGroup)): + raise TypeError( + "The app_command passed must be a top level command, " + "not an instance of SubCommand or SubCommandGroup" + ) if app_command.guild_ids is None: # if test_guilds are specified then we add the same command for each test guild From f5d5d5777a9abb4ca7f6122c28293076b1329d7c Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sun, 24 Sep 2023 01:46:41 +0300 Subject: [PATCH 16/67] chore: remove deprecated undocumented things --- disnake/ext/commands/interaction_bot_base.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 35792f9941..4aaf725ab1 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -254,14 +254,6 @@ def command_sync_flags(self) -> CommandSyncFlags: """ return CommandSyncFlags._from_value(self._command_sync_flags.value) - def application_commands_iterator(self) -> Iterable[InvokableApplicationCommand]: - warn_deprecated( - "application_commands_iterator is deprecated and will be removed in a future version. " - "Use all_app_commands.values() instead.", - stacklevel=3, - ) - return self.all_app_commands.values() - @property def application_commands(self) -> Set[InvokableApplicationCommand]: """Set[:class:`InvokableApplicationCommand`]: A set of all application commands the bot has.""" @@ -983,15 +975,8 @@ def decorator( # command synchronisation def _ordered_unsynced_commands( - self, test_guilds: Optional[Sequence[int]] = MISSING + self ) -> Tuple[List[ApplicationCommand], Dict[int, List[ApplicationCommand]]]: - if test_guilds is not MISSING: - warn_deprecated( - "Argument test_guilds of _ordered_unsynced_commands is deprecated " - "and will be removed in a future version.", - stacklevel=3, - ) - global_cmds = [] guilds = {} From 164df505900bc1ec5d4436a87a69d7658a191236 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sun, 24 Sep 2023 01:48:39 +0300 Subject: [PATCH 17/67] chore: fix a small linting issue --- disnake/ext/commands/interaction_bot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 4aaf725ab1..fb1e910f14 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -975,7 +975,7 @@ def decorator( # command synchronisation def _ordered_unsynced_commands( - self + self, ) -> Tuple[List[ApplicationCommand], Dict[int, List[ApplicationCommand]]]: global_cmds = [] guilds = {} From 0064e9a0f037381cef3d249e0fc41b2470eadbe1 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Sun, 24 Sep 2023 23:44:49 +0300 Subject: [PATCH 18/67] chore: replace `warn_deprecated` with `deprecated ` --- disnake/ext/commands/interaction_bot_base.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index fb1e910f14..cfd9613619 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -29,7 +29,7 @@ from disnake.app_commands import ApplicationCommand, Option from disnake.custom_warnings import SyncWarning from disnake.enums import ApplicationCommandType -from disnake.utils import warn_deprecated +from disnake.utils import deprecated, warn_deprecated from . import errors from .base_core import InvokableApplicationCommand @@ -283,13 +283,9 @@ def message_commands(self) -> Set[InvokableMessageCommand]: } @property + @deprecated("slash_commands") def all_slash_commands(self) -> Dict[str, InvokableSlashCommand]: # no docstring because it was an attribute and now it's deprecated - warn_deprecated( - "all_slash_commands is deprecated and will be removed in a future version. " - "Use all_app_commands as a replacement.", - stacklevel=3, - ) return { cmd.name: cmd for cmd in self.all_app_commands.values() @@ -297,13 +293,9 @@ def all_slash_commands(self) -> Dict[str, InvokableSlashCommand]: } @property + @deprecated("user_commands") def all_user_commands(self) -> Dict[str, InvokableUserCommand]: # no docstring because it was an attribute and now it's deprecated - warn_deprecated( - "all_user_commands is deprecated and will be removed in a future version. " - "Use all_app_commands as a replacement.", - stacklevel=3, - ) return { cmd.name: cmd for cmd in self.all_app_commands.values() @@ -311,13 +303,9 @@ def all_user_commands(self) -> Dict[str, InvokableUserCommand]: } @property + @deprecated("message_commands") def all_message_commands(self) -> Dict[str, InvokableMessageCommand]: # no docstring because it was an attribute and now it's deprecated - warn_deprecated( - "all_message_commands is deprecated and will be removed in a future version. " - "Use all_app_commands as a replacement.", - stacklevel=3, - ) return { cmd.name: cmd for cmd in self.all_app_commands.values() From 4736dad01e6f79345cca53816fe5aa8cda0828c6 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:31:32 +0300 Subject: [PATCH 19/67] chore: resolve `remove_app_command` nits --- disnake/ext/commands/cog.py | 6 ++++-- disnake/ext/commands/interaction_bot_base.py | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index b21a1375bd..53a2799bf4 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -760,7 +760,9 @@ def _inject(self, bot: AnyBot) -> Self: except Exception: # undo our additions for to_undo in self.__cog_app_commands__[:index]: - bot.remove_app_command(to_undo.body.type, to_undo.name, to_undo.guild_ids) + bot.remove_app_command( + to_undo.body.type, to_undo.name, guild_ids=to_undo.guild_ids + ) raise if not hasattr(self.cog_load.__func__, "__cog_special_method__"): @@ -834,7 +836,7 @@ def _eject(self, bot: AnyBot) -> None: for app_command in self.__cog_app_commands__: bot.remove_app_command( - app_command.body.type, app_command.name, app_command.guild_ids + app_command.body.type, app_command.name, guild_ids=app_command.guild_ids ) for name, method_name in self.__cog_listeners__: diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index cfd9613619..83cc6e73ce 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -443,7 +443,7 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: self.add_app_command(message_command) def remove_app_command( - self, cmd_type: ApplicationCommandType, name: str, guild_ids: Optional[Tuple[int, ...]] + self, cmd_type: ApplicationCommandType, name: str, *, guild_ids: Optional[Sequence[int]] ) -> Optional[InvokableApplicationCommand]: """Removes an :class:`InvokableApplicationCommand` from the internal list of app commands. @@ -455,13 +455,14 @@ def remove_app_command( The type of the app command to remove. name: :class:`str` The name of the app command to remove. - guild_ids: Optional[Tuple[:class:`str`, ...]] - The IDs of the guilds from which the command should be removed. + guild_ids: Optional[Sequence[:class:`int`]] + The IDs of the guilds from which the command should be removed, + or ``None`` if global. Defaults to ``None``. Returns ------- Optional[:class:`InvokableApplicationCommand`] - The app command that was removed. If the key data was not valid then ``None`` is returned instead. + The app command that was removed. If no matching command was found, then ``None`` is returned instead. """ if guild_ids is None: # a global command may end up being a local command if test_guilds were specified From 8c6bb90659ee0f8a54722938cff65773420d0fb0 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:42:07 +0300 Subject: [PATCH 20/67] chore: add suggested typings --- disnake/ext/commands/interaction_bot_base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 83cc6e73ce..e1c5835774 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -490,12 +490,12 @@ def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name "Use remove_app_command instead.", stacklevel=3, ) - bad_keys = [] + bad_keys: List[AppCmdIndex] = [] for key in self.all_app_commands.keys(): if key.type is cmd_type and key.name == name: bad_keys.append(key) - result = None + result: Optional[InvokableApplicationCommand] = None for key in bad_keys: cmd = self.all_app_commands.pop(key, None) if result is None: @@ -517,7 +517,7 @@ def remove_slash_command(self, name: str) -> Optional[InvokableSlashCommand]: Optional[:class:`InvokableSlashCommand`] The slash command that was removed. If the name is not valid then ``None`` is returned instead. """ - self._emulate_old_app_command_remove(ApplicationCommandType.chat_input, name) + return self._emulate_old_app_command_remove(ApplicationCommandType.chat_input, name) def remove_user_command(self, name: str) -> Optional[InvokableUserCommand]: """Removes an :class:`InvokableUserCommand` from the internal list @@ -533,7 +533,7 @@ def remove_user_command(self, name: str) -> Optional[InvokableUserCommand]: Optional[:class:`InvokableUserCommand`] The user command that was removed. If the name is not valid then ``None`` is returned instead. """ - self._emulate_old_app_command_remove(ApplicationCommandType.user, name) + return self._emulate_old_app_command_remove(ApplicationCommandType.user, name) def remove_message_command(self, name: str) -> Optional[InvokableMessageCommand]: """Removes an :class:`InvokableMessageCommand` from the internal list @@ -549,7 +549,7 @@ def remove_message_command(self, name: str) -> Optional[InvokableMessageCommand] Optional[:class:`InvokableMessageCommand`] The message command that was removed. If the name is not valid then ``None`` is returned instead. """ - self._emulate_old_app_command_remove(ApplicationCommandType.message, name) + return self._emulate_old_app_command_remove(ApplicationCommandType.message, name) def get_slash_command( self, name: str, guild_id: Optional[int] = MISSING @@ -966,8 +966,8 @@ def decorator( def _ordered_unsynced_commands( self, ) -> Tuple[List[ApplicationCommand], Dict[int, List[ApplicationCommand]]]: - global_cmds = [] - guilds = {} + global_cmds: List[ApplicationCommand] = [] + guilds: Dict[int, List[ApplicationCommand]] = {} for key, cmd in self.all_app_commands.items(): if not cmd.auto_sync: From c44806b42cbd6a815d2343cbc41313231c57a887 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:49:20 +0300 Subject: [PATCH 21/67] docs: fix typos and improve comments --- disnake/ext/commands/interaction_bot_base.py | 4 ++++ disnake/interactions/application_command.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index e1c5835774..9f122e2eba 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -1509,6 +1509,8 @@ async def process_app_command_autocompletion( inter: :class:`disnake.ApplicationCommandInteraction` The interaction to process. """ + # `inter.data.guild_id` is the guild ID the command is registered to, + # so this is correct even when a global command is called from a guild cmd_index = AppCmdIndex( type=inter.data.type, name=inter.data.name, guild_id=inter.data.guild_id ) @@ -1587,6 +1589,8 @@ async def process_application_commands( command_type = interaction.data.type event_name = None + # `inter.data.guild_id` is the guild ID the command is registered to, + # so this is correct even when a global command is called from a guild cmd_index = AppCmdIndex( type=command_type, name=interaction.data.name, guild_id=interaction.data.guild_id ) diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 8fb465d7c6..d5248176fd 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -177,9 +177,9 @@ class ApplicationCommandInteractionData(Dict[str, Any]): guild_id: Optional[:class:`int`] ID of the guild the command is registered to. target_id: :class:`int` - ID of the user or message targetted by a user or message command. + ID of the user or message targeted by a user or message command. target: Union[:class:`User`, :class:`Member`, :class:`Message`] - The user or message targetted by a user or message command. + The user or message targeted by a user or message command. """ __slots__ = ( From ef591e697034296f44319adcc3abbe2255a6017b Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:58:23 +0300 Subject: [PATCH 22/67] chore: simplify ambiguity checks --- disnake/ext/commands/interaction_bot_base.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 9f122e2eba..e3a1200d9d 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -605,11 +605,8 @@ def get_slash_command( continue if result is None: result = chain_match - continue # we should check whether there's an ambiguity in command search - result_guild_ids = (result.root_parent or result).guild_ids - match_guild_ids = (chain_match.root_parent or chain_match).guild_ids - if result_guild_ids != match_guild_ids: + elif chain_match is not result: raise ValueError( "Argument guild_id must be provided if there're different slash commands " "with the same name but different guilds or one of them is global." @@ -656,7 +653,7 @@ def get_user_command( if result is None: result = command # we should check whether there's an ambiguity in command search - elif result.guild_ids != command.guild_ids: + elif command is not result: raise ValueError( "Argument guild_id must be provided if there're different user commands " "with the same name but different guilds or one of them is global." @@ -705,7 +702,7 @@ def get_message_command( if result is None: result = command # we should check whether there's an ambiguity in command search - elif result.guild_ids != command.guild_ids: + elif command is not result: raise ValueError( "Argument guild_id must be provided if there're different message commands " "with the same name but different guilds or one of them is global." From 42c7e6f0e09911c4786e581653b885c2023b4bc4 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Tue, 26 Sep 2023 00:24:22 +0300 Subject: [PATCH 23/67] docs: explain command location priorities --- disnake/ext/commands/ctx_menus_core.py | 16 +++++++---- disnake/ext/commands/interaction_bot_base.py | 29 ++++++++++++++------ disnake/ext/commands/slash_core.py | 8 ++++-- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 835bd674e6..cee4073d23 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -266,9 +266,11 @@ def user_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: Sequence[:class:`int`] - If specified, the client will register the command in these guilds. - Otherwise, this command will be registered globally. + guild_ids: Optional[Sequence[:class:`int`]] + If specified, the client will register the command to these guilds. + Otherwise the command will be registered globally, unless + parameter ``test_guilds`` is specified in the bot constructor, in which case + this command will be registered to those guilds. extras: Dict[:class:`str`, Any] A dict of user provided extras to attach to the command. @@ -348,9 +350,11 @@ def message_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: Sequence[:class:`int`] - If specified, the client will register the command in these guilds. - Otherwise, this command will be registered globally. + guild_ids: Optional[Sequence[:class:`int`]] + If specified, the client will register the command to these guilds. + Otherwise the command will be registered globally, unless + parameter ``test_guilds`` is specified in the bot constructor, in which case + this command will be registered to those guilds. extras: Dict[:class:`str`, Any] A dict of user provided extras to attach to the command. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index e3a1200d9d..7700e0b7f2 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -318,6 +318,11 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: This is usually not called, instead shortcut decorators are used, such as :meth:`.slash_command`, :meth:`.user_command` or :meth:`.message_command`. + The app command is registered to guilds specified in the ``guild_ids`` attribute. + If this attribute is ``None`` then the command is registered globally, unless + parameter ``test_guilds`` is specified in the bot constructor, in which case + this command is registered to those guilds. + .. versionadded:: 2.10 Parameters @@ -762,9 +767,11 @@ def slash_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` - guild_ids: Sequence[:class:`int`] - If specified, the client will register the command in these guilds. - Otherwise, this command will be registered globally. + guild_ids: Optional[Sequence[:class:`int`]] + If specified, the client will register the command to these guilds. + Otherwise the command will be registered globally, unless + parameter ``test_guilds`` is specified in the bot constructor, in which case + this command will be registered to those guilds. connectors: Dict[:class:`str`, :class:`str`] Binds function names to option names. If the name of an option already matches the corresponding function param, @@ -846,9 +853,11 @@ def user_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: Sequence[:class:`int`] - If specified, the client will register the command in these guilds. - Otherwise, this command will be registered globally. + guild_ids: Optional[Sequence[:class:`int`]] + If specified, the client will register the command to these guilds. + Otherwise the command will be registered globally, unless + parameter ``test_guilds`` is specified in the bot constructor, in which case + this command will be registered to those guilds. extras: Dict[:class:`str`, Any] A dict of user provided extras to attach to the command. @@ -923,9 +932,11 @@ def message_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` - guild_ids: Sequence[:class:`int`] - If specified, the client will register the command in these guilds. - Otherwise, this command will be registered globally. + guild_ids: Optional[Sequence[:class:`int`]] + If specified, the client will register the command to these guilds. + Otherwise the command will be registered globally, unless + parameter ``test_guilds`` is specified in the bot constructor, in which case + this command will be registered to those guilds. extras: Dict[:class:`str`, Any] A dict of user provided extras to attach to the command. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 1b318a21d0..7895851717 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -792,9 +792,11 @@ def slash_command( .. versionadded:: 2.5 - guild_ids: List[:class:`int`] - If specified, the client will register the command in these guilds. - Otherwise, this command will be registered globally. + guild_ids: Optional[Sequence[:class:`int`]] + If specified, the client will register the command to these guilds. + Otherwise the command will be registered globally, unless + parameter ``test_guilds`` is specified in the bot constructor, in which case + this command will be registered to those guilds. connectors: Dict[:class:`str`, :class:`str`] Binds function names to option names. If the name of an option already matches the corresponding function param, From 9a4d1aa5bb59efba893aba68a01c65dbc8079d65 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:31:47 +0300 Subject: [PATCH 24/67] Make `bot.all_app_commands` a mapping proxy --- disnake/ext/commands/interaction_bot_base.py | 51 +++++++++++--------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 7700e0b7f2..5f4bb7e5e3 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -7,6 +7,7 @@ import sys import traceback import warnings +from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, @@ -238,7 +239,7 @@ def __init__( self._before_message_command_invoke = None self._after_message_command_invoke = None - self.all_app_commands: Dict[AppCmdIndex, InvokableApplicationCommand] = {} + self._all_app_commands: Dict[AppCmdIndex, InvokableApplicationCommand] = {} @disnake.utils.copy_doc(disnake.Client.login) async def login(self, token: str) -> None: @@ -254,23 +255,29 @@ def command_sync_flags(self) -> CommandSyncFlags: """ return CommandSyncFlags._from_value(self._command_sync_flags.value) + @property + def all_app_commands(self) -> MappingProxyType[AppCmdIndex, InvokableApplicationCommand]: + """MappingProxyType[:class:`AppCmdIndex`, :class:`InvokableApplicationCommand`]: + A mapping proxy with all application commands the bot has.""" + return MappingProxyType(self._all_app_commands) + @property def application_commands(self) -> Set[InvokableApplicationCommand]: """Set[:class:`InvokableApplicationCommand`]: A set of all application commands the bot has.""" - return set(self.all_app_commands.values()) + return set(self._all_app_commands.values()) @property def slash_commands(self) -> Set[InvokableSlashCommand]: """Set[:class:`InvokableSlashCommand`]: A set of all slash commands the bot has.""" return { - cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableSlashCommand) + cmd for cmd in self._all_app_commands.values() if isinstance(cmd, InvokableSlashCommand) } @property def user_commands(self) -> Set[InvokableUserCommand]: """Set[:class:`InvokableUserCommand`]: A set of all user commands the bot has.""" return { - cmd for cmd in self.all_app_commands.values() if isinstance(cmd, InvokableUserCommand) + cmd for cmd in self._all_app_commands.values() if isinstance(cmd, InvokableUserCommand) } @property @@ -278,7 +285,7 @@ def message_commands(self) -> Set[InvokableMessageCommand]: """Set[:class:`InvokableMessageCommand`]: A set of all message commands the bot has.""" return { cmd - for cmd in self.all_app_commands.values() + for cmd in self._all_app_commands.values() if isinstance(cmd, InvokableMessageCommand) } @@ -288,7 +295,7 @@ def all_slash_commands(self) -> Dict[str, InvokableSlashCommand]: # no docstring because it was an attribute and now it's deprecated return { cmd.name: cmd - for cmd in self.all_app_commands.values() + for cmd in self._all_app_commands.values() if isinstance(cmd, InvokableSlashCommand) } @@ -298,7 +305,7 @@ def all_user_commands(self) -> Dict[str, InvokableUserCommand]: # no docstring because it was an attribute and now it's deprecated return { cmd.name: cmd - for cmd in self.all_app_commands.values() + for cmd in self._all_app_commands.values() if isinstance(cmd, InvokableUserCommand) } @@ -308,7 +315,7 @@ def all_message_commands(self) -> Dict[str, InvokableMessageCommand]: # no docstring because it was an attribute and now it's deprecated return { cmd.name: cmd - for cmd in self.all_app_commands.values() + for cmd in self._all_app_commands.values() if isinstance(cmd, InvokableMessageCommand) } @@ -360,7 +367,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: cmd_index = AppCmdIndex( type=app_command.body.type, name=app_command.name, guild_id=guild_id ) - if cmd_index in self.all_app_commands: + if cmd_index in self._all_app_commands: raise ApplicationCommandRegistrationError( cmd_index.type, cmd_index.name, cmd_index.guild_id ) @@ -370,7 +377,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: # note that we're adding the same command object for each guild_id # this ensures that any changes that happen to app_command after add_app_command # (such as hook attachments or permission modifications) apply properly - self.all_app_commands[cmd_index] = app_command + self._all_app_commands[cmd_index] = app_command def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: """Adds an :class:`InvokableSlashCommand` into the internal list of slash commands. @@ -482,7 +489,7 @@ def remove_app_command( result = None for guild_id in extended_guild_ids: cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) - cmd = self.all_app_commands.pop(cmd_index, None) + cmd = self._all_app_commands.pop(cmd_index, None) if result is None: result = cmd @@ -496,13 +503,13 @@ def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name stacklevel=3, ) bad_keys: List[AppCmdIndex] = [] - for key in self.all_app_commands.keys(): + for key in self._all_app_commands.keys(): if key.type is cmd_type and key.name == name: bad_keys.append(key) result: Optional[InvokableApplicationCommand] = None for key in bad_keys: - cmd = self.all_app_commands.pop(key, None) + cmd = self._all_app_commands.pop(key, None) if result is None: result = cmd @@ -595,14 +602,14 @@ def get_slash_command( cmd_index = AppCmdIndex( type=ApplicationCommandType.chat_input, name=chain[0], guild_id=guild_id ) - command = self.all_app_commands.get(cmd_index) + command = self._all_app_commands.get(cmd_index) if command is None: return None return _match_subcommand_chain(command, chain) # type: ignore # this is mostly for backwards compatibility, as previously guild_id arg didn't exist result = None - for command in self.all_app_commands.values(): + for command in self._all_app_commands.values(): if not isinstance(command, InvokableSlashCommand): continue chain_match = _match_subcommand_chain(command, chain) @@ -646,13 +653,13 @@ def get_user_command( """ if guild_id is not MISSING: cmd_index = AppCmdIndex(type=ApplicationCommandType.user, name=name, guild_id=guild_id) - command = self.all_app_commands.get(cmd_index) + command = self._all_app_commands.get(cmd_index) if command is None: return None return command # type: ignore # this is mostly for backwards compatibility, as previously guild_id arg didn't exist result = None - for command in self.all_app_commands.values(): + for command in self._all_app_commands.values(): if not isinstance(command, InvokableUserCommand) or command.name != name: continue if result is None: @@ -695,13 +702,13 @@ def get_message_command( cmd_index = AppCmdIndex( type=ApplicationCommandType.message, name=name, guild_id=guild_id ) - command = self.all_app_commands.get(cmd_index) + command = self._all_app_commands.get(cmd_index) if command is None: return None return command # type: ignore # this is mostly for backwards compatibility, as previously guild_id arg didn't exist result = None - for command in self.all_app_commands.values(): + for command in self._all_app_commands.values(): if not isinstance(command, InvokableMessageCommand) or command.name != name: continue if result is None: @@ -977,7 +984,7 @@ def _ordered_unsynced_commands( global_cmds: List[ApplicationCommand] = [] guilds: Dict[int, List[ApplicationCommand]] = {} - for key, cmd in self.all_app_commands.items(): + for key, cmd in self._all_app_commands.items(): if not cmd.auto_sync: cmd.body._always_synced = True @@ -1523,7 +1530,7 @@ async def process_app_command_autocompletion( type=inter.data.type, name=inter.data.name, guild_id=inter.data.guild_id ) # this happens to always be a slash command - slash_command = self.all_app_commands.get(cmd_index) + slash_command = self._all_app_commands.get(cmd_index) if slash_command is None: return @@ -1602,7 +1609,7 @@ async def process_application_commands( cmd_index = AppCmdIndex( type=command_type, name=interaction.data.name, guild_id=interaction.data.guild_id ) - app_command = self.all_app_commands.get(cmd_index) + app_command = self._all_app_commands.get(cmd_index) if command_type is ApplicationCommandType.chat_input: event_name = "slash_command" From 55299b25c5a52bddbd2d2987943aa590b6400dc6 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:44:07 +0300 Subject: [PATCH 25/67] fix: unintended breaking change (handling `guild_ids=[]`) --- disnake/ext/commands/interaction_bot_base.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 5f4bb7e5e3..b2868f53dc 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -258,7 +258,8 @@ def command_sync_flags(self) -> CommandSyncFlags: @property def all_app_commands(self) -> MappingProxyType[AppCmdIndex, InvokableApplicationCommand]: """MappingProxyType[:class:`AppCmdIndex`, :class:`InvokableApplicationCommand`]: - A mapping proxy with all application commands the bot has.""" + A mapping proxy with all application commands the bot has. + """ return MappingProxyType(self._all_app_commands) @property @@ -357,11 +358,8 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: "not an instance of SubCommand or SubCommandGroup" ) - if app_command.guild_ids is None: - # if test_guilds are specified then we add the same command for each test guild - guild_ids = (None,) if self._test_guilds is None else self._test_guilds - else: - guild_ids = app_command.guild_ids + test_guilds = (None,) if self._test_guilds is None else self._test_guilds + guild_ids = app_command.guild_ids or test_guilds for guild_id in guild_ids: cmd_index = AppCmdIndex( From 8dc756e68b81a99285d7d3fadc04299faf4aa327 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:55:24 +0300 Subject: [PATCH 26/67] docs: `AppCmdIndex` --- disnake/ext/commands/interaction_bot_base.py | 13 +++++++++++++ docs/ext/commands/api/app_commands.rst | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index b2868f53dc..2720602a09 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -82,6 +82,19 @@ class _Diff(TypedDict): class AppCmdIndex(NamedTuple): + """A named tuple used for indexation of :class:`InvokableApplicationCommand`s + stored in bot's cache. + + Attributes + ---------- + type: :class:`ApplicationCommandType` + The type of the application command being stored. + name: :class:`str` + The name of the application command being stored. + guild_id: :class:`int` + One of the guild IDs this command should be registered to, + or ``None`` if it's a global command. + """ type: ApplicationCommandType name: str guild_id: Optional[int] diff --git a/docs/ext/commands/api/app_commands.rst b/docs/ext/commands/api/app_commands.rst index f4d2d6f290..67a78846f4 100644 --- a/docs/ext/commands/api/app_commands.rst +++ b/docs/ext/commands/api/app_commands.rst @@ -138,6 +138,13 @@ CommandSyncFlags .. autoclass:: CommandSyncFlags() :members: +AppCmdIndex +~~~~~~~~~~~ + +.. attributetable:: AppCmdIndex + +.. autoclass:: AppCmdIndex + Injection ~~~~~~~~~ From d5197117dcb5fb10935ebcb845d4b2edfca998d5 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 3 Nov 2023 19:02:22 +0300 Subject: [PATCH 27/67] fix: docs and linting --- disnake/ext/commands/interaction_bot_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 2720602a09..bea7259e22 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -62,7 +62,7 @@ P = ParamSpec("P") -__all__ = ("InteractionBotBase",) +__all__ = ("InteractionBotBase", "AppCmdIndex") MISSING: Any = disnake.utils.MISSING @@ -95,6 +95,7 @@ class AppCmdIndex(NamedTuple): One of the guild IDs this command should be registered to, or ``None`` if it's a global command. """ + type: ApplicationCommandType name: str guild_id: Optional[int] From 413f75923956536dd8da0c87ceedd585638f90ef Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 3 Nov 2023 19:12:36 +0300 Subject: [PATCH 28/67] Make `AppCmdIndex` public --- disnake/ext/commands/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/disnake/ext/commands/__init__.py b/disnake/ext/commands/__init__.py index ea897b384e..edc00a72d1 100644 --- a/disnake/ext/commands/__init__.py +++ b/disnake/ext/commands/__init__.py @@ -22,5 +22,6 @@ from .flag_converter import * from .flags import * from .help import * +from .interaction_bot_base import * from .params import * from .slash_core import * From 304a829521ed2e3c25a25a32bf9d43796e4af45e Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 3 Nov 2023 19:34:32 +0300 Subject: [PATCH 29/67] docs: fix a reference --- disnake/ext/commands/interaction_bot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index bea7259e22..63e7f8009f 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -87,7 +87,7 @@ class AppCmdIndex(NamedTuple): Attributes ---------- - type: :class:`ApplicationCommandType` + type: :class:`disnake.ApplicationCommandType` The type of the application command being stored. name: :class:`str` The name of the application command being stored. From 63ce00788463e05abf846853600c7193935a6d98 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Fri, 3 Nov 2023 20:45:48 +0300 Subject: [PATCH 30/67] docs: improve AppCmdIndex docstring --- disnake/ext/commands/interaction_bot_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 63e7f8009f..213245a451 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -82,8 +82,8 @@ class _Diff(TypedDict): class AppCmdIndex(NamedTuple): - """A named tuple used for indexation of :class:`InvokableApplicationCommand`s - stored in bot's cache. + """A named tuple used for indexation of :class:`InvokableApplicationCommand` + objects stored in bot's cache. Attributes ---------- From a5802385de007644aacf5d5a03a3b2c685ea1b41 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:14:00 +0300 Subject: [PATCH 31/67] docs: small fixes; chore: move `AppCmdIndex` to a different file --- disnake/ext/commands/__init__.py | 1 - disnake/ext/commands/base_core.py | 23 +++++++++++++++- disnake/ext/commands/interaction_bot_base.py | 29 +++----------------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/disnake/ext/commands/__init__.py b/disnake/ext/commands/__init__.py index edc00a72d1..ea897b384e 100644 --- a/disnake/ext/commands/__init__.py +++ b/disnake/ext/commands/__init__.py @@ -22,6 +22,5 @@ from .flag_converter import * from .flags import * from .help import * -from .interaction_bot_base import * from .params import * from .slash_core import * diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index c21cc5f2d1..bfd5822efb 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -12,6 +12,7 @@ Callable, Dict, List, + NamedTuple, Optional, Tuple, TypeVar, @@ -49,7 +50,7 @@ ] -__all__ = ("InvokableApplicationCommand", "default_member_permissions") +__all__ = ("AppCmdIndex", "InvokableApplicationCommand", "default_member_permissions") T = TypeVar("T") @@ -79,6 +80,26 @@ async def wrapped(*args, **kwargs): return wrapped +class AppCmdIndex(NamedTuple): + """A named tuple used for indexation of :class:`InvokableApplicationCommand` + objects stored in bot's cache. + + Attributes + ---------- + type: :class:`disnake.ApplicationCommandType` + The type of the application command being stored. + name: :class:`str` + The name of the application command being stored. + guild_id: Optional[:class:`int`] + One of the guild IDs this command should be registered to, + or ``None`` if it's a global command. + """ + + type: ApplicationCommandType + name: str + guild_id: Optional[int] + + class InvokableApplicationCommand(ABC): """A base class that implements the protocol for a bot application command. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 213245a451..a5949d2d19 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -15,7 +15,6 @@ Dict, Iterable, List, - NamedTuple, Optional, Sequence, Set, @@ -33,7 +32,7 @@ from disnake.utils import deprecated, warn_deprecated from . import errors -from .base_core import InvokableApplicationCommand +from .base_core import AppCmdIndex, InvokableApplicationCommand from .common_bot_base import CommonBotBase from .ctx_menus_core import ( InvokableMessageCommand, @@ -62,7 +61,7 @@ P = ParamSpec("P") -__all__ = ("InteractionBotBase", "AppCmdIndex") +__all__ = ("InteractionBotBase",) MISSING: Any = disnake.utils.MISSING @@ -81,26 +80,6 @@ class _Diff(TypedDict): delete_ignored: NotRequired[List[ApplicationCommand]] -class AppCmdIndex(NamedTuple): - """A named tuple used for indexation of :class:`InvokableApplicationCommand` - objects stored in bot's cache. - - Attributes - ---------- - type: :class:`disnake.ApplicationCommandType` - The type of the application command being stored. - name: :class:`str` - The name of the application command being stored. - guild_id: :class:`int` - One of the guild IDs this command should be registered to, - or ``None`` if it's a global command. - """ - - type: ApplicationCommandType - name: str - guild_id: Optional[int] - - def _get_to_send_from_diff(diff: _Diff): return diff["no_changes"] + diff["upsert"] + diff["edit"] + diff.get("delete_ignored", []) @@ -271,8 +250,8 @@ def command_sync_flags(self) -> CommandSyncFlags: @property def all_app_commands(self) -> MappingProxyType[AppCmdIndex, InvokableApplicationCommand]: - """MappingProxyType[:class:`AppCmdIndex`, :class:`InvokableApplicationCommand`]: - A mapping proxy with all application commands the bot has. + """Mapping[:class:`AppCmdIndex`, :class:`InvokableApplicationCommand`]: + A read-only mapping with all application commands the bot has. """ return MappingProxyType(self._all_app_commands) From ebdeb33dbc1f26f8ccd74a239157f6cd3bb94c8e Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:40:53 +0300 Subject: [PATCH 32/67] fix: prevent an unintended breaking change --- disnake/ext/commands/interaction_bot_base.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 132a910cf3..5a1b5a7d93 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -467,15 +467,9 @@ def remove_app_command( Optional[:class:`InvokableApplicationCommand`] The app command that was removed. If no matching command was found, then ``None`` is returned instead. """ - if guild_ids is None: - # a global command may end up being a local command if test_guilds were specified - # so we should remove this "global" command from each test guild - if self._test_guilds is not None: - extended_guild_ids = self._test_guilds - else: - extended_guild_ids = (None,) - else: - extended_guild_ids = guild_ids + test_guilds = (None,) if self._test_guilds is None else self._test_guilds + # this is consistent with the behavior of command synchronisation + extended_guild_ids = guild_ids or test_guilds result = None for guild_id in extended_guild_ids: From 93880cffb149490166e1c3d9c3d98b77d59a2107 Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:13:06 +0300 Subject: [PATCH 33/67] refactor: `remove_app_command` now takes a single `guild_id` --- disnake/ext/commands/cog.py | 4 +-- disnake/ext/commands/interaction_bot_base.py | 27 ++++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index 452ca98bde..e353435f30 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -760,7 +760,7 @@ def _inject(self, bot: AnyBot) -> Self: except Exception: # undo our additions for to_undo in self.__cog_app_commands__[:index]: - bot.remove_app_command( + bot._remove_app_commands( to_undo.body.type, to_undo.name, guild_ids=to_undo.guild_ids ) raise @@ -835,7 +835,7 @@ def _eject(self, bot: AnyBot) -> None: bot.remove_command(command.name) # type: ignore for app_command in self.__cog_app_commands__: - bot.remove_app_command( + bot._remove_app_commands( app_command.body.type, app_command.name, guild_ids=app_command.guild_ids ) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 5a1b5a7d93..7f7e4321a4 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -446,7 +446,7 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: self.add_app_command(message_command) def remove_app_command( - self, cmd_type: ApplicationCommandType, name: str, *, guild_ids: Optional[Sequence[int]] + self, cmd_type: ApplicationCommandType, name: str, *, guild_id: Optional[int] ) -> Optional[InvokableApplicationCommand]: """Removes an :class:`InvokableApplicationCommand` from the internal list of app commands. @@ -458,21 +458,21 @@ def remove_app_command( The type of the app command to remove. name: :class:`str` The name of the app command to remove. - guild_ids: Optional[Sequence[:class:`int`]] - The IDs of the guilds from which the command should be removed, - or ``None`` if global. Defaults to ``None``. + guild_id: Optional[:class:`int`] + The ID of the guild from which this command should be removed, + or ``None`` if it's global. Returns ------- Optional[:class:`InvokableApplicationCommand`] The app command that was removed. If no matching command was found, then ``None`` is returned instead. """ - test_guilds = (None,) if self._test_guilds is None else self._test_guilds - # this is consistent with the behavior of command synchronisation - extended_guild_ids = guild_ids or test_guilds + if guild_id is not None or self._test_guilds is None: + cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) + return self._all_app_commands.pop(cmd_index, None) result = None - for guild_id in extended_guild_ids: + for guild_id in self._test_guilds: cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) cmd = self._all_app_commands.pop(cmd_index, None) if result is None: @@ -480,6 +480,17 @@ def remove_app_command( return result + def _remove_app_commands( + self, cmd_type: ApplicationCommandType, name: str, *, guild_ids: Optional[Sequence[int]] + ) -> None: + test_guilds = (None,) if self._test_guilds is None else self._test_guilds + # this is consistent with the behavior of command synchronisation + final_guild_ids = guild_ids or test_guilds + + for guild_id in final_guild_ids: + cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) + self._all_app_commands.pop(cmd_index, None) + def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name: str) -> Any: type_info = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name warn_deprecated( From 84eff4224bf1cab26b338b1c451c28c51e8bdf7a Mon Sep 17 00:00:00 2001 From: EQUENOS <50338932+EQUENOS@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:17:13 +0300 Subject: [PATCH 34/67] refactor: use `@deprecated` for `add_xxx_command` --- disnake/ext/commands/interaction_bot_base.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 7f7e4321a4..92b1805473 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -370,6 +370,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: # (such as hook attachments or permission modifications) apply properly self._all_app_commands[cmd_index] = app_command + @deprecated("add_app_command") def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: """Adds an :class:`InvokableSlashCommand` into the internal list of slash commands. @@ -388,13 +389,9 @@ def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: TypeError The slash command passed is not an instance of :class:`InvokableSlashCommand`. """ - warn_deprecated( - "add_slash_command is deprecated and will be removed in a future version. " - "Use add_app_command instead.", - stacklevel=3, - ) self.add_app_command(slash_command) + @deprecated("add_app_command") def add_user_command(self, user_command: InvokableUserCommand) -> None: """Adds an :class:`InvokableUserCommand` into the internal list of user commands. @@ -413,13 +410,9 @@ def add_user_command(self, user_command: InvokableUserCommand) -> None: TypeError The user command passed is not an instance of :class:`InvokableUserCommand`. """ - warn_deprecated( - "add_user_command is deprecated and will be removed in a future version. " - "Use add_app_command instead.", - stacklevel=3, - ) self.add_app_command(user_command) + @deprecated("add_app_command") def add_message_command(self, message_command: InvokableMessageCommand) -> None: """Adds an :class:`InvokableMessageCommand` into the internal list of message commands. @@ -438,11 +431,6 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: TypeError The message command passed is not an instance of :class:`InvokableMessageCommand`. """ - warn_deprecated( - "add_message_command is deprecated and will be removed in a future version. " - "Use add_app_command instead.", - stacklevel=3, - ) self.add_app_command(message_command) def remove_app_command( From 009c849178beeab36c9168d18abba525527d85d3 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:09:36 +0200 Subject: [PATCH 35/67] Update disnake/ext/commands/interaction_bot_base.py Co-authored-by: vi <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> --- disnake/ext/commands/interaction_bot_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 92b1805473..9ba3663745 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -448,7 +448,8 @@ def remove_app_command( The name of the app command to remove. guild_id: Optional[:class:`int`] The ID of the guild from which this command should be removed, - or ``None`` if it's global. + or ``None`` if it's global. If ``test_guilds`` is specified in the bot constructor, + passing ``None`` here will remove the command from those guilds instead. Returns ------- From 72e4839b6614ab220becb494de55de38081bb720 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:10:29 +0200 Subject: [PATCH 36/67] Update disnake/ext/commands/interaction_bot_base.py Co-authored-by: vi <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> --- disnake/ext/commands/interaction_bot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 9ba3663745..eca3993355 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -549,7 +549,7 @@ def remove_message_command(self, name: str) -> Optional[InvokableMessageCommand] return self._emulate_old_app_command_remove(ApplicationCommandType.message, name) def get_slash_command( - self, name: str, guild_id: Optional[int] = MISSING + self, name: str, *, guild_id: Optional[int] = MISSING ) -> Optional[Union[InvokableSlashCommand, SubCommandGroup, SubCommand]]: """Works like ``Bot.get_command``, but for slash commands. From d6ea6ae936efd387d665e4cf14f73c98a7a10f28 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:12:03 +0200 Subject: [PATCH 37/67] Update disnake/interactions/application_command.py Co-authored-by: vi <8530778+shiftinv@users.noreply.github.com> Signed-off-by: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> --- disnake/interactions/application_command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index d5248176fd..20945e77b9 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -176,6 +176,8 @@ class ApplicationCommandInteractionData(Dict[str, Any]): A list of options from the API. guild_id: Optional[:class:`int`] ID of the guild the command is registered to. + + .. versionadded:: 2.10 target_id: :class:`int` ID of the user or message targeted by a user or message command. target: Union[:class:`User`, :class:`Member`, :class:`Message`] From e8cea4355768556681e58ef03ac93499472a66f6 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:07:54 +0200 Subject: [PATCH 38/67] move localize call outside loop --- disnake/ext/commands/interaction_bot_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index eca3993355..aa1c06d97a 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -363,12 +363,11 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: cmd_index.type, cmd_index.name, cmd_index.guild_id ) - # localization may be called multiple times for the same command but it's harmless - app_command.body.localize(self.i18n) # note that we're adding the same command object for each guild_id # this ensures that any changes that happen to app_command after add_app_command # (such as hook attachments or permission modifications) apply properly self._all_app_commands[cmd_index] = app_command + app_command.body.localize(self.i18n) @deprecated("add_app_command") def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: From 40528cebbf6be7b05defa17a2c39a8e3fb0459c1 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:11:20 +0200 Subject: [PATCH 39/67] make guild_id kwarg only --- disnake/ext/commands/interaction_bot_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index aa1c06d97a..f2d1599b72 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -611,7 +611,7 @@ def get_slash_command( return result def get_user_command( - self, name: str, guild_id: Optional[int] = MISSING + self, name: str, *, guild_id: Optional[int] = MISSING ) -> Optional[InvokableUserCommand]: """Gets an :class:`InvokableUserCommand` from the internal list of user commands. @@ -658,7 +658,7 @@ def get_user_command( return result def get_message_command( - self, name: str, guild_id: Optional[int] = MISSING + self, name: str, *, guild_id: Optional[int] = MISSING ) -> Optional[InvokableMessageCommand]: """Gets an :class:`InvokableMessageCommand` from the internal list of message commands. From fd8676562e3e154ad281aa50eb5a99569181076c Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:12:38 +0200 Subject: [PATCH 40/67] update versionadded version --- disnake/interactions/application_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 20945e77b9..f64f780f41 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -177,7 +177,7 @@ class ApplicationCommandInteractionData(Dict[str, Any]): guild_id: Optional[:class:`int`] ID of the guild the command is registered to. - .. versionadded:: 2.10 + .. versionadded:: 2.12 target_id: :class:`int` ID of the user or message targeted by a user or message command. target: Union[:class:`User`, :class:`Member`, :class:`Message`] From 4a6cd0e3904323f0886a7878d4a91eef09528891 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:13:39 +0200 Subject: [PATCH 41/67] add versionadded to AppCmdIndex --- disnake/ext/commands/base_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index f613b73d63..2cb0eebcb2 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -84,6 +84,8 @@ class AppCmdIndex(NamedTuple): """A named tuple used for indexation of :class:`InvokableApplicationCommand` objects stored in bot's cache. + .. versionadded:: 2.12 + Attributes ---------- type: :class:`disnake.ApplicationCommandType` From a238f6643b999924cd193908394c24508f63d4fb Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:14:49 +0200 Subject: [PATCH 42/67] bump versionadded --- disnake/ext/commands/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 3d3bcb1b31..7fb6c32c1f 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1036,7 +1036,7 @@ class ApplicationCommandRegistrationError(CommandRegistrationError): This inherits from :exc:`CommandRegistrationError` - .. versionadded:: 2.10 + .. versionadded:: 2.12 Attributes ---------- From e26dcd2414088e83f465d623f64964b90153fcab Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:16:19 +0200 Subject: [PATCH 43/67] bump versionadded --- disnake/ext/commands/interaction_bot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 402a2dae20..1a069e8e97 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -330,7 +330,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: parameter ``test_guilds`` is specified in the bot constructor, in which case this command is registered to those guilds. - .. versionadded:: 2.10 + .. versionadded:: 2.12 Parameters ---------- From 0ef6837735854301f266d061437c2222653c9ffd Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:18:28 +0200 Subject: [PATCH 44/67] add note about name and guild_ids attributes --- disnake/ext/commands/interaction_bot_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 1a069e8e97..2a497fe5c6 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -332,6 +332,12 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: .. versionadded:: 2.12 + .. note:: + Any change to the ``app_command``'s ``name`` and ``guild_ids`` attributes + after the command was added to the client are not supported and may + result in undefined behaviour. + + Parameters ---------- app_command: :class:`InvokableApplicationCommand` From 8f08755554efccfb94bf93d1577db5775b2c1828 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 14:04:44 +0200 Subject: [PATCH 45/67] chore: undo broken docs annotation upgrades --- disnake/ext/commands/ctx_menus_core.py | 4 ++-- disnake/ext/commands/interaction_bot_base.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 995c265c14..91a02ccf10 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -316,7 +316,7 @@ def user_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: :class:`~collections.abc.Optional[Sequence`\\[:class:`int`]] + guild_ids: Optional[Sequence[:class:`int`]] If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case @@ -429,7 +429,7 @@ def message_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: :class:`~collections.abc.Optional[Sequence`\\[:class:`int`]] + guild_ids: Optional[Sequence[:class:`int`]] If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index f71f801fd0..925b9dbe3c 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -795,7 +795,7 @@ def slash_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` - guild_ids: :class:`~collections.abc.Optional[Sequence`\\[:class:`int`]] + guild_ids: Optional[Sequence[:class:`int`]] If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case @@ -907,7 +907,7 @@ def user_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: :class:`~collections.abc.Optional[Sequence`\\[:class:`int`]] + guild_ids: Optional[Sequence[:class:`int`]] If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case @@ -1012,7 +1012,7 @@ def message_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` - guild_ids: :class:`~collections.abc.Optional[Sequence`\\[:class:`int`]] + guild_ids: Optional[Sequence[:class:`int`]] If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case From c9ac04c0e155852dfe2ee90e837d675b5ef3e85d Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 14:08:23 +0200 Subject: [PATCH 46/67] chore: upgrade documentation annotations --- disnake/ext/commands/base_core.py | 4 +-- disnake/ext/commands/ctx_menus_core.py | 4 +-- disnake/ext/commands/errors.py | 4 +-- disnake/ext/commands/interaction_bot_base.py | 34 ++++++++++---------- disnake/interactions/application_command.py | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 416c14ad3a..83524fc91d 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -103,9 +103,9 @@ class AppCmdIndex(NamedTuple): The type of the application command being stored. name: :class:`str` The name of the application command being stored. - guild_id: Optional[:class:`int`] + guild_id: :class:`int` | :data:`None` One of the guild IDs this command should be registered to, - or ``None`` if it's a global command. + or :data:`None` if it's a global command. """ type: ApplicationCommandType diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 91a02ccf10..6ac9a22da4 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -316,7 +316,7 @@ def user_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: Optional[Sequence[:class:`int`]] + guild_ids: :class:`~collections.abc.Sequence`\\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case @@ -429,7 +429,7 @@ def message_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: Optional[Sequence[:class:`int`]] + guild_ids: :class:`~collections.abc.Sequence`\\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 1970c00397..2b077fa914 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1063,9 +1063,9 @@ class ApplicationCommandRegistrationError(CommandRegistrationError): The command type. name: :class:`str` The command name. - guild_id: Optional[:class:`int`] + guild_id: :class:`int` | :data:`None` The ID of the guild where the command was supposed to be registered - or ``None`` if it was a global command. + or :data:`None` if it was a global command. """ def __init__( diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 925b9dbe3c..37f5accc78 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -140,7 +140,7 @@ def _match_subcommand_chain( command: InvokableSlashCommand, chain: list[str] ) -> Union[InvokableSlashCommand, SubCommand, SubCommandGroup, None]: """An internal function that returns a subcommand with a route matching the chain. - If there's no match then ``None`` is returned. + If there's no match then :data:`None` is returned. """ if command.name != chain[0]: return None @@ -252,7 +252,7 @@ def command_sync_flags(self) -> CommandSyncFlags: @property def all_app_commands(self) -> MappingProxyType[AppCmdIndex, InvokableApplicationCommand]: - """Mapping[:class:`AppCmdIndex`, :class:`InvokableApplicationCommand`]: + """:class:`~collections.abc.Mapping`\\[:class:`AppCmdIndex`, :class:`InvokableApplicationCommand`]: A read-only mapping with all application commands the bot has. """ return MappingProxyType(self._all_app_commands) @@ -322,7 +322,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: :meth:`.slash_command`, :meth:`.user_command` or :meth:`.message_command`. The app command is registered to guilds specified in the ``guild_ids`` attribute. - If this attribute is ``None`` then the command is registered globally, unless + If this attribute is :data:`None` then the command is registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case this command is registered to those guilds. @@ -454,15 +454,15 @@ def remove_app_command( The type of the app command to remove. name: :class:`str` The name of the app command to remove. - guild_id: Optional[:class:`int`] + guild_id: :class:`int` | :data:`None` The ID of the guild from which this command should be removed, - or ``None`` if it's global. If ``test_guilds`` is specified in the bot constructor, - passing ``None`` here will remove the command from those guilds instead. + or :data:`None` if it's global. If ``test_guilds`` is specified in the bot constructor, + passing :data:`None` here will remove the command from those guilds instead. Returns ------- - Optional[:class:`InvokableApplicationCommand`] - The app command that was removed. If no matching command was found, then ``None`` is returned instead. + :class:`InvokableApplicationCommand` | :data:`None` + The app command that was removed. If no matching command was found, then :data:`None` is returned instead. """ if guild_id is not None or self._test_guilds is None: cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) @@ -570,8 +570,8 @@ def get_slash_command( ---------- name: :class:`str` The name of the slash command to get. - guild_id: Optional[:class:`int`] - The guild ID corresponding to the slash command or ``None`` if it's a global command. + guild_id: :class:`int` | :data:`None` + The guild ID corresponding to the slash command or :data:`None` if it's a global command. If this is not specified then the first match is returned instead. Raises @@ -631,8 +631,8 @@ def get_user_command( ---------- name: :class:`str` The name of the user command to get. - guild_id: Optional[:class:`int`] - The guild ID corresponding to the user command or ``None`` if it's a global command. + guild_id: :class:`int` | :data:`None` + The guild ID corresponding to the user command or :data:`None` if it's a global command. If this is not specified then the first match is returned instead. Raises @@ -679,8 +679,8 @@ def get_message_command( ---------- name: :class:`str` The name of the message command to get. - guild_id: Optional[:class:`int`] - The guild ID corresponding to the message command or ``None`` if it's a global command. + guild_id: :class:`int` | :data:`None` + The guild ID corresponding to the message command or :data:`None` if it's a global command. If this is not specified then the first match is returned instead. Raises @@ -795,7 +795,7 @@ def slash_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` - guild_ids: Optional[Sequence[:class:`int`]] + guild_ids: :class:`~collections.abc.Sequence`\\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case @@ -907,7 +907,7 @@ def user_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. - guild_ids: Optional[Sequence[:class:`int`]] + guild_ids: :class:`~collections.abc.Sequence`\\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case @@ -1012,7 +1012,7 @@ def message_command( auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` - guild_ids: Optional[Sequence[:class:`int`]] + guild_ids: :class:`~collections.abc.Sequence`\\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 496d1c2b05..d85bc34038 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -231,7 +231,7 @@ class ApplicationCommandInteractionData(dict[str, Any]): All resolved objects related to this interaction. options: :class:`list`\\[:class:`ApplicationCommandInteractionDataOption`] A list of options from the API. - guild_id: Optional[:class:`int`] + guild_id: :class:`int` | :data:`None` ID of the guild the command is registered to. .. versionadded:: 2.12 From be3ecea6388e936cf9238595529df551b15a391c Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 15:49:40 +0200 Subject: [PATCH 47/67] lint: use pyright ignore instead of type ignore --- disnake/ext/commands/interaction_bot_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 9d4ee0e31a..e326fab7da 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -565,7 +565,7 @@ def get_slash_command( command = self._all_app_commands.get(cmd_index) if command is None: return None - return _match_subcommand_chain(command, chain) # type: ignore[reportArgumentType] + return _match_subcommand_chain(command, chain) # pyright: ignore[reportArgumentType] # this is mostly for backwards compatibility, as previously guild_id arg didn't exist result = None @@ -617,7 +617,7 @@ def get_user_command( command = self._all_app_commands.get(cmd_index) if command is None: return None - return command # type: ignore[reportReturnType] + return command # pyright: ignore[reportReturnType] # this is mostly for backwards compatibility, as previously guild_id arg didn't exist result = None for command in self._all_app_commands.values(): @@ -667,7 +667,7 @@ def get_message_command( command = self._all_app_commands.get(cmd_index) if command is None: return None - return command # type: ignore[reportReturnType] + return command # pyright: ignore[reportReturnType] # this is mostly for backwards compatibility, as previously guild_id arg didn't exist result = None for command in self._all_app_commands.values(): From 0a6c1072b6ec94f987dac3762a1da2a0484947ee Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 15:52:10 +0200 Subject: [PATCH 48/67] fix(docs): use |vnext| --- disnake/ext/commands/base_core.py | 2 +- disnake/ext/commands/errors.py | 2 +- disnake/ext/commands/interaction_bot_base.py | 4 ++-- disnake/interactions/application_command.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index eb200e99b8..c09f2a025a 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -100,7 +100,7 @@ class AppCmdIndex(NamedTuple): """A named tuple used for indexation of :class:`InvokableApplicationCommand` objects stored in bot's cache. - .. versionadded:: 2.12 + .. versionadded:: |vnext| Attributes ---------- diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 80d04cae6d..baf0a63046 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1089,7 +1089,7 @@ class ApplicationCommandRegistrationError(CommandRegistrationError): This inherits from :exc:`CommandRegistrationError` - .. versionadded:: 2.12 + .. versionadded:: |vnext| Attributes ---------- diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index e326fab7da..47a7494831 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -292,7 +292,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: parameter ``test_guilds`` is specified in the bot constructor, in which case this command is registered to those guilds. - .. versionadded:: 2.12 + .. versionadded:: |vnext| .. note:: Any change to the ``app_command``'s ``name`` and ``guild_ids`` attributes @@ -412,7 +412,7 @@ def remove_app_command( ) -> InvokableApplicationCommand | None: """Removes an :class:`InvokableApplicationCommand` from the internal list of app commands. - .. versionadded:: 2.10 + .. versionadded:: |vnext| Parameters ---------- diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 50a0c60cc2..0479b38c6b 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -234,7 +234,7 @@ class ApplicationCommandInteractionData(dict[str, Any]): guild_id: :class:`int` | :data:`None` ID of the guild the command is registered to. - .. versionadded:: 2.12 + .. versionadded:: |vnext| target_id: :class:`int` ID of the user or message targeted by a user or message command. target: :class:`User` | :class:`Member` | :class:`Message` From 41ae57a201fae94d94ceb9e7b9fc30e522836127 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 15:54:58 +0200 Subject: [PATCH 49/67] fix: updated deprecation decorators --- disnake/ext/commands/interaction_bot_base.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 47a7494831..f4a54e6000 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -25,7 +25,6 @@ from disnake.custom_warnings import SyncWarning from disnake.enums import ApplicationCommandType from disnake.flags import ApplicationInstallTypes, InteractionContextTypes -from disnake.utils import deprecated, warn_deprecated from . import errors from .base_core import AppCmdIndex, InvokableApplicationCommand @@ -252,7 +251,7 @@ def message_commands(self) -> set[InvokableMessageCommand]: } @property - @deprecated("slash_commands") + @utils.deprecated("Use `.slash_commands` instead.") def all_slash_commands(self) -> dict[str, InvokableSlashCommand]: # no docstring because it was an attribute and now it's deprecated return { @@ -262,7 +261,7 @@ def all_slash_commands(self) -> dict[str, InvokableSlashCommand]: } @property - @deprecated("user_commands") + @utils.deprecated("Use `.user_commands` instead.") def all_user_commands(self) -> dict[str, InvokableUserCommand]: # no docstring because it was an attribute and now it's deprecated return { @@ -272,7 +271,7 @@ def all_user_commands(self) -> dict[str, InvokableUserCommand]: } @property - @deprecated("message_commands") + @utils.deprecated("Use `.message_commands` instead.") def all_message_commands(self) -> dict[str, InvokableMessageCommand]: # no docstring because it was an attribute and now it's deprecated return { @@ -344,7 +343,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: self._all_app_commands[cmd_index] = app_command app_command.body.localize(self.i18n) - @deprecated("add_app_command") + @utils.deprecated("Use `.add_app_command` instead.") def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: """Adds an :class:`InvokableSlashCommand` into the internal list of slash commands. @@ -365,7 +364,7 @@ def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: """ self.add_app_command(slash_command) - @deprecated("add_app_command") + @utils.deprecated("Use `.add_app_command` instead.") def add_user_command(self, user_command: InvokableUserCommand) -> None: """Adds an :class:`InvokableUserCommand` into the internal list of user commands. @@ -386,7 +385,7 @@ def add_user_command(self, user_command: InvokableUserCommand) -> None: """ self.add_app_command(user_command) - @deprecated("add_app_command") + @utils.deprecated("Use `.add_app_command` instead.") def add_message_command(self, message_command: InvokableMessageCommand) -> None: """Adds an :class:`InvokableMessageCommand` into the internal list of message commands. @@ -456,7 +455,7 @@ def _remove_app_commands( def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name: str) -> Any: type_info = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name - warn_deprecated( + utils.warn_deprecated( f"remove_{type_info}_command is deprecated and will be removed in a future version. " "Use remove_app_command instead.", stacklevel=3, From 80c9a896cd359e0803017d71c982baf24e18efd2 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 15:56:53 +0200 Subject: [PATCH 50/67] docs: adjust `slash_command` `guild_ids` type hint to match --- disnake/ext/commands/slash_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index acbb738d98..fb466992bf 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -937,7 +937,7 @@ def slash_command( .. versionadded:: 2.5 - guild_ids: :class:`list`\[:class:`int`] + guild_ids: :class:`~collections.abc.Sequence`\[:class:`int`] | :data:`None` If specified, the client will register the command in these guilds. Otherwise, this command will be registered globally, unless parameter ``test_guilds`` is specified in the bot constructor, in which case From db1af6b93e4dd67b0851d1d2f156d588d486a053 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 16:01:29 +0200 Subject: [PATCH 51/67] fix: apply contexts/install_types defaults again --- disnake/ext/commands/interaction_bot_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index f4a54e6000..73adc22461 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -328,11 +328,15 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: test_guilds = (None,) if self._test_guilds is None else self._test_guilds guild_ids = app_command.guild_ids or test_guilds + app_command._apply_defaults(self) + app_command.body.localize(self.i18n) + for guild_id in guild_ids: cmd_index = AppCmdIndex( type=app_command.body.type, name=app_command.name, guild_id=guild_id ) if cmd_index in self._all_app_commands: + # FIXME: if this fails, the command might have been added to some but not others raise ApplicationCommandRegistrationError( cmd_index.type, cmd_index.name, cmd_index.guild_id ) @@ -341,7 +345,6 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: # this ensures that any changes that happen to app_command after add_app_command # (such as hook attachments or permission modifications) apply properly self._all_app_commands[cmd_index] = app_command - app_command.body.localize(self.i18n) @utils.deprecated("Use `.add_app_command` instead.") def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: From 4d2058d4ef79b64cc352b3059ca19d2f66d9cced Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 17:20:31 +0200 Subject: [PATCH 52/67] chore: misc grammar/phrasing fixes --- disnake/ext/commands/base_core.py | 6 +-- disnake/ext/commands/ctx_menus_core.py | 4 +- disnake/ext/commands/interaction_bot_base.py | 43 ++++++++++---------- disnake/ext/commands/slash_core.py | 2 +- disnake/interactions/application_command.py | 4 +- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index c09f2a025a..be13ef2f30 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -97,8 +97,8 @@ async def wrapped(*args, **kwargs): class AppCmdIndex(NamedTuple): - """A named tuple used for indexation of :class:`InvokableApplicationCommand` - objects stored in bot's cache. + """A named tuple used for indexing :class:`InvokableApplicationCommand` + objects handled by the bot. .. versionadded:: |vnext| @@ -109,7 +109,7 @@ class AppCmdIndex(NamedTuple): name: :class:`str` The name of the application command being stored. guild_id: :class:`int` | :data:`None` - One of the guild IDs this command should be registered to, + One of the guild IDs this command is registered to, or :data:`None` if it's a global command. """ diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 4d810ee3c9..956c965e06 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -417,7 +417,7 @@ def user_command( guild_ids: :class:`~collections.abc.Sequence`\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless - parameter ``test_guilds`` is specified in the bot constructor, in which case + the ``test_guilds`` parameter is specified in the bot constructor, in which case this command will be registered to those guilds. extras: :class:`dict`\[:class:`str`, :data:`~typing.Any`] A dict of user provided extras to attach to the command. @@ -564,7 +564,7 @@ def message_command( guild_ids: :class:`~collections.abc.Sequence`\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless - parameter ``test_guilds`` is specified in the bot constructor, in which case + the ``test_guilds`` parameter is specified in the bot constructor, in which case this command will be registered to those guilds. extras: :class:`dict`\[:class:`str`, :data:`~typing.Any`] A dict of user provided extras to attach to the command. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 73adc22461..f02e11b251 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -253,7 +253,6 @@ def message_commands(self) -> set[InvokableMessageCommand]: @property @utils.deprecated("Use `.slash_commands` instead.") def all_slash_commands(self) -> dict[str, InvokableSlashCommand]: - # no docstring because it was an attribute and now it's deprecated return { cmd.name: cmd for cmd in self._all_app_commands.values() @@ -263,7 +262,6 @@ def all_slash_commands(self) -> dict[str, InvokableSlashCommand]: @property @utils.deprecated("Use `.user_commands` instead.") def all_user_commands(self) -> dict[str, InvokableUserCommand]: - # no docstring because it was an attribute and now it's deprecated return { cmd.name: cmd for cmd in self._all_app_commands.values() @@ -273,7 +271,6 @@ def all_user_commands(self) -> dict[str, InvokableUserCommand]: @property @utils.deprecated("Use `.message_commands` instead.") def all_message_commands(self) -> dict[str, InvokableMessageCommand]: - # no docstring because it was an attribute and now it's deprecated return { cmd.name: cmd for cmd in self._all_app_commands.values() @@ -288,13 +285,13 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: The app command is registered to guilds specified in the ``guild_ids`` attribute. If this attribute is :data:`None` then the command is registered globally, unless - parameter ``test_guilds`` is specified in the bot constructor, in which case + the ``test_guilds`` parameter is specified in the bot constructor, in which case this command is registered to those guilds. .. versionadded:: |vnext| .. note:: - Any change to the ``app_command``'s ``name`` and ``guild_ids`` attributes + Any change to the command's ``name`` and ``guild_ids`` attributes after the command was added to the client are not supported and may result in undefined behaviour. @@ -341,8 +338,8 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: cmd_index.type, cmd_index.name, cmd_index.guild_id ) - # note that we're adding the same command object for each guild_id - # this ensures that any changes that happen to app_command after add_app_command + # note that we're adding the same command object for each guild_id; + # this ensures that any changes that happen to the command after add_app_command # (such as hook attachments or permission modifications) apply properly self._all_app_commands[cmd_index] = app_command @@ -540,14 +537,14 @@ def get_slash_command( The name of the slash command to get. guild_id: :class:`int` | :data:`None` The guild ID corresponding to the slash command or :data:`None` if it's a global command. - If this is not specified then the first match is returned instead. + If this is not specified but multiple commands with this name exist, and error will be raised. Raises ------ TypeError The name is not a string. ValueError - Parameter ``guild_id`` was not provided in a case where different slash commands + The ``guild_id`` parameter was not provided in a case where different slash commands have the same name but different guild_ids. Returns @@ -569,7 +566,7 @@ def get_slash_command( return None return _match_subcommand_chain(command, chain) # pyright: ignore[reportArgumentType] - # this is mostly for backwards compatibility, as previously guild_id arg didn't exist + # this is mostly for backwards compatibility, as previously the guild_id arg didn't exist result = None for command in self._all_app_commands.values(): if not isinstance(command, InvokableSlashCommand): @@ -582,7 +579,7 @@ def get_slash_command( # we should check whether there's an ambiguity in command search elif chain_match is not result: msg = ( - "Argument guild_id must be provided if there're different slash commands " + "The `guild_id` argument must be provided if there are different slash commands " "with the same name but different guilds or one of them is global." ) raise ValueError(msg) @@ -601,12 +598,12 @@ def get_user_command( The name of the user command to get. guild_id: :class:`int` | :data:`None` The guild ID corresponding to the user command or :data:`None` if it's a global command. - If this is not specified then the first match is returned instead. + If this is not specified but multiple commands with this name exist, and error will be raised. Raises ------ ValueError - Parameter ``guild_id`` was not provided in a case where different user commands + The ``guild_id`` parameter was not provided in a case where different user commands have the same name but different guild_ids. Returns @@ -620,7 +617,8 @@ def get_user_command( if command is None: return None return command # pyright: ignore[reportReturnType] - # this is mostly for backwards compatibility, as previously guild_id arg didn't exist + + # this is mostly for backwards compatibility, as previously the guild_id arg didn't exist result = None for command in self._all_app_commands.values(): if not isinstance(command, InvokableUserCommand) or command.name != name: @@ -630,7 +628,7 @@ def get_user_command( # we should check whether there's an ambiguity in command search elif command is not result: msg = ( - "Argument guild_id must be provided if there're different user commands " + "The `guild_id` argument must be provided if there are different user commands " "with the same name but different guilds or one of them is global." ) raise ValueError(msg) @@ -649,12 +647,12 @@ def get_message_command( The name of the message command to get. guild_id: :class:`int` | :data:`None` The guild ID corresponding to the message command or :data:`None` if it's a global command. - If this is not specified then the first match is returned instead. + If this is not specified but multiple commands with this name exist, and error will be raised. Raises ------ ValueError - Parameter ``guild_id`` was not provided in a case where different message commands + The ``guild_id`` parameter was not provided in a case where different message commands have the same name but different guild_ids. Returns @@ -670,7 +668,8 @@ def get_message_command( if command is None: return None return command # pyright: ignore[reportReturnType] - # this is mostly for backwards compatibility, as previously guild_id arg didn't exist + + # this is mostly for backwards compatibility, as previously the guild_id arg didn't exist result = None for command in self._all_app_commands.values(): if not isinstance(command, InvokableMessageCommand) or command.name != name: @@ -680,7 +679,7 @@ def get_message_command( # we should check whether there's an ambiguity in command search elif command is not result: msg = ( - "Argument guild_id must be provided if there're different message commands " + "The `guild_id` argument must be provided if there are different message commands " "with the same name but different guilds or one of them is global." ) raise ValueError(msg) @@ -801,7 +800,7 @@ def slash_command( guild_ids: :class:`~collections.abc.Sequence`\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless - parameter ``test_guilds`` is specified in the bot constructor, in which case + the ``test_guilds`` parameter is specified in the bot constructor, in which case this command will be registered to those guilds. connectors: :class:`dict`\[:class:`str`, :class:`str`] Binds function names to option names. If the name @@ -945,7 +944,7 @@ def user_command( guild_ids: :class:`~collections.abc.Sequence`\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless - parameter ``test_guilds`` is specified in the bot constructor, in which case + the ``test_guilds`` parameter is specified in the bot constructor, in which case this command will be registered to those guilds. extras: :class:`dict`\[:class:`str`, :data:`~typing.Any`] A dict of user provided extras to attach to the command. @@ -1082,7 +1081,7 @@ def message_command( guild_ids: :class:`~collections.abc.Sequence`\[:class:`int`] | :data:`None` If specified, the client will register the command to these guilds. Otherwise the command will be registered globally, unless - parameter ``test_guilds`` is specified in the bot constructor, in which case + the ``test_guilds`` parameter is specified in the bot constructor, in which case this command will be registered to those guilds. extras: :class:`dict`\[:class:`str`, :data:`~typing.Any`] A dict of user provided extras to attach to the command. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index fb466992bf..c3eea8903d 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -940,7 +940,7 @@ def slash_command( guild_ids: :class:`~collections.abc.Sequence`\[:class:`int`] | :data:`None` If specified, the client will register the command in these guilds. Otherwise, this command will be registered globally, unless - parameter ``test_guilds`` is specified in the bot constructor, in which case + the ``test_guilds`` parameter is specified in the bot constructor, in which case this command will be registered to those guilds. connectors: :class:`dict`\[:class:`str`, :class:`str`] Binds function names to option names. If the name diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 0479b38c6b..e286329abe 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -256,9 +256,7 @@ def __init__( self, *, data: ApplicationCommandInteractionDataPayload, - parent: ApplicationCommandInteraction[ - ClientT - ], # the ID of the guild where this command has been invoked + parent: ApplicationCommandInteraction[ClientT], ) -> None: super().__init__(data) self.id: int = int(data["id"]) From 2dfaa1e757941cd771f92ac1ffee2a6ea7726cf9 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 17:22:30 +0200 Subject: [PATCH 53/67] fix: use `deprecated` decorator for `remove_*` methds --- disnake/ext/commands/interaction_bot_base.py | 27 +++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index f02e11b251..f8849f56ef 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -350,6 +350,9 @@ def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: This is usually not called, instead the :meth:`.slash_command` or shortcut decorators are used. + .. deprecated:: |vnext| + Use :meth:`add_app_command` instead. + Parameters ---------- slash_command: :class:`InvokableSlashCommand` @@ -371,6 +374,9 @@ def add_user_command(self, user_command: InvokableUserCommand) -> None: This is usually not called, instead the :meth:`.user_command` or shortcut decorators are used. + .. deprecated:: |vnext| + Use :meth:`add_app_command` instead. + Parameters ---------- user_command: :class:`InvokableUserCommand` @@ -392,6 +398,9 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: This is usually not called, instead the :meth:`.message_command` or shortcut decorators are used. + .. deprecated:: |vnext| + Use :meth:`add_app_command` instead. + Parameters ---------- message_command: :class:`InvokableMessageCommand` @@ -454,12 +463,6 @@ def _remove_app_commands( self._all_app_commands.pop(cmd_index, None) def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name: str) -> Any: - type_info = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name - utils.warn_deprecated( - f"remove_{type_info}_command is deprecated and will be removed in a future version. " - "Use remove_app_command instead.", - stacklevel=3, - ) bad_keys: list[AppCmdIndex] = [] for key in self._all_app_commands: if key.type is cmd_type and key.name == name: @@ -473,10 +476,14 @@ def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name return result + @utils.deprecated("Use `.remove_app_command` instead.") def remove_slash_command(self, name: str) -> InvokableSlashCommand | None: """Removes an :class:`InvokableSlashCommand` from the internal list of slash commands. + .. deprecated:: |vnext| + Use :meth:`remove_app_command` instead. + Parameters ---------- name: :class:`str` @@ -489,10 +496,14 @@ def remove_slash_command(self, name: str) -> InvokableSlashCommand | None: """ return self._emulate_old_app_command_remove(ApplicationCommandType.chat_input, name) + @utils.deprecated("Use `.remove_app_command` instead.") def remove_user_command(self, name: str) -> InvokableUserCommand | None: """Removes an :class:`InvokableUserCommand` from the internal list of user commands. + .. deprecated:: |vnext| + Use :meth:`remove_app_command` instead. + Parameters ---------- name: :class:`str` @@ -505,10 +516,14 @@ def remove_user_command(self, name: str) -> InvokableUserCommand | None: """ return self._emulate_old_app_command_remove(ApplicationCommandType.user, name) + @utils.deprecated("Use `.remove_app_command` instead.") def remove_message_command(self, name: str) -> InvokableMessageCommand | None: """Removes an :class:`InvokableMessageCommand` from the internal list of message commands. + .. deprecated:: |vnext| + Use :meth:`remove_app_command` instead. + Parameters ---------- name: :class:`str` From 242bd3baa0bd90935ef7417b75fa221e4b9927b8 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 17:31:07 +0200 Subject: [PATCH 54/67] refactor: deduplicate `remove_app_command` + `_remove_app_commands` --- disnake/ext/commands/interaction_bot_base.py | 31 ++++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index f8849f56ef..27cb3976b5 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -438,29 +438,28 @@ def remove_app_command( :class:`InvokableApplicationCommand` | :data:`None` The app command that was removed. If no matching command was found, then :data:`None` is returned instead. """ - if guild_id is not None or self._test_guilds is None: - cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) - return self._all_app_commands.pop(cmd_index, None) - - result = None - for guild_id in self._test_guilds: - cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) - cmd = self._all_app_commands.pop(cmd_index, None) - if result is None: - result = cmd - - return result + return self._remove_app_commands( + cmd_type, name, guild_ids=[guild_id] if guild_id is not None else None + ) def _remove_app_commands( self, cmd_type: ApplicationCommandType, name: str, *, guild_ids: Sequence[int] | None - ) -> None: - test_guilds = (None,) if self._test_guilds is None else self._test_guilds + ) -> InvokableApplicationCommand | None: # this is consistent with the behavior of command synchronisation - final_guild_ids = guild_ids or test_guilds + if guild_ids: + final_guild_ids = guild_ids + else: + # use default guilds + final_guild_ids = (None,) if self._test_guilds is None else self._test_guilds + result: InvokableApplicationCommand | None = None for guild_id in final_guild_ids: cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) - self._all_app_commands.pop(cmd_index, None) + cmd = self._all_app_commands.pop(cmd_index, None) + if result is None: + result = cmd + + return result def _emulate_old_app_command_remove(self, cmd_type: ApplicationCommandType, name: str) -> Any: bad_keys: list[AppCmdIndex] = [] From 5da2aba24c86c39dbb79ab2a086f69204cf3302c Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 17:36:02 +0200 Subject: [PATCH 55/67] fix: avoid partially adding commands to cache in case of ambiguity errors --- disnake/ext/commands/interaction_bot_base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 27cb3976b5..9ead1cb3e8 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -328,16 +328,21 @@ def add_app_command(self, app_command: InvokableApplicationCommand) -> None: app_command._apply_defaults(self) app_command.body.localize(self.i18n) + # note: this is done in two separate steps/loops, so that any + # errors from duplicate commands/indices don't result in an only + # partially updated command cache + indices: list[AppCmdIndex] = [] for guild_id in guild_ids: cmd_index = AppCmdIndex( type=app_command.body.type, name=app_command.name, guild_id=guild_id ) if cmd_index in self._all_app_commands: - # FIXME: if this fails, the command might have been added to some but not others raise ApplicationCommandRegistrationError( cmd_index.type, cmd_index.name, cmd_index.guild_id ) + indices.append(cmd_index) + for cmd_index in indices: # note that we're adding the same command object for each guild_id; # this ensures that any changes that happen to the command after add_app_command # (such as hook attachments or permission modifications) apply properly From 18d50b0759bbae05f6c15071db81ebe7e5d1a1b8 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 17:38:59 +0200 Subject: [PATCH 56/67] feat: make `add_app_command` param positional-only --- disnake/ext/commands/interaction_bot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 9ead1cb3e8..588066133a 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -277,7 +277,7 @@ def all_message_commands(self) -> dict[str, InvokableMessageCommand]: if isinstance(cmd, InvokableMessageCommand) } - def add_app_command(self, app_command: InvokableApplicationCommand) -> None: + def add_app_command(self, app_command: InvokableApplicationCommand, /) -> None: """Adds an :class:`InvokableApplicationCommand` into the internal list of app commands. This is usually not called, instead shortcut decorators are used, such as From 6eafade44e21d6c53ea7b95bbc9d17f4e1bf3360 Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 17:58:54 +0200 Subject: [PATCH 57/67] refactor: extract common logic from `get_*_command` --- disnake/ext/commands/interaction_bot_base.py | 120 ++++++------------- 1 file changed, 38 insertions(+), 82 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 588066133a..8de7d9b667 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -136,7 +136,7 @@ def _format_diff(diff: _Diff) -> str: def _match_subcommand_chain( - command: InvokableSlashCommand, chain: list[str] + command: InvokableSlashCommand, chain: Sequence[str] ) -> InvokableSlashCommand | SubCommand | SubCommandGroup | None: """An internal function that returns a subcommand with a route matching the chain. If there's no match then :data:`None` is returned. @@ -540,6 +540,36 @@ def remove_message_command(self, name: str) -> InvokableMessageCommand | None: """ return self._emulate_old_app_command_remove(ApplicationCommandType.message, name) + def _get_app_command( + self, cmd_type: ApplicationCommandType, name: str, *, guild_id: int | None = MISSING + ) -> InvokableApplicationCommand | None: + if not isinstance(name, str): + msg = f"Expected name to be str, not {name.__class__}" + raise TypeError(msg) + + if guild_id is not MISSING: + cmd_index = AppCmdIndex(type=cmd_type, name=name, guild_id=guild_id) + return self._all_app_commands.get(cmd_index) + + # this is mostly for backwards compatibility, as previously the guild_id arg didn't exist + result: InvokableApplicationCommand | None = None + for command in self._all_app_commands.values(): + if command.body.type != cmd_type or command.name != name: + continue + + if result is None: + result = command + # we should check whether there's an ambiguity in command search + elif command is not result: + type_ = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name + msg = ( + f"The `guild_id` argument must be provided if there are different {type_} commands " + "with the same name but different guilds or one of them is global." + ) + raise ValueError(msg) + + return result + def get_slash_command( self, name: str, *, guild_id: int | None = MISSING ) -> InvokableSlashCommand | SubCommandGroup | SubCommand | None: @@ -547,7 +577,7 @@ def get_slash_command( If the name contains spaces, then it will assume that you are looking for a :class:`SubCommand` or a :class:`SubCommandGroup`. - e.g: ``'foo bar'`` will get the sub command group, or the sub command ``bar`` of the top-level slash command + e.g: ``'foo bar'`` will get the sub command or group named ``bar`` of the top-level slash command ``foo`` if found, otherwise :data:`None`. Parameters @@ -571,39 +601,11 @@ def get_slash_command( :class:`InvokableSlashCommand` | :class:`SubCommandGroup` | :class:`SubCommand` | :data:`None` The slash command that was requested. If not found, returns :data:`None`. """ - if not isinstance(name, str): - msg = f"Expected name to be str, not {name.__class__}" - raise TypeError(msg) - chain = name.split() - if guild_id is not MISSING: - cmd_index = AppCmdIndex( - type=ApplicationCommandType.chat_input, name=chain[0], guild_id=guild_id - ) - command = self._all_app_commands.get(cmd_index) - if command is None: - return None - return _match_subcommand_chain(command, chain) # pyright: ignore[reportArgumentType] - - # this is mostly for backwards compatibility, as previously the guild_id arg didn't exist - result = None - for command in self._all_app_commands.values(): - if not isinstance(command, InvokableSlashCommand): - continue - chain_match = _match_subcommand_chain(command, chain) - if chain_match is None: - continue - if result is None: - result = chain_match - # we should check whether there's an ambiguity in command search - elif chain_match is not result: - msg = ( - "The `guild_id` argument must be provided if there are different slash commands " - "with the same name but different guilds or one of them is global." - ) - raise ValueError(msg) - - return result + cmd = self._get_app_command(ApplicationCommandType.chat_input, chain[0], guild_id=guild_id) + if cmd is None: + return None + return _match_subcommand_chain(cast("InvokableSlashCommand", cmd), chain) def get_user_command( self, name: str, *, guild_id: int | None = MISSING @@ -630,29 +632,7 @@ def get_user_command( :class:`InvokableUserCommand` | :data:`None` The user command that was requested. If not found, returns :data:`None`. """ - if guild_id is not MISSING: - cmd_index = AppCmdIndex(type=ApplicationCommandType.user, name=name, guild_id=guild_id) - command = self._all_app_commands.get(cmd_index) - if command is None: - return None - return command # pyright: ignore[reportReturnType] - - # this is mostly for backwards compatibility, as previously the guild_id arg didn't exist - result = None - for command in self._all_app_commands.values(): - if not isinstance(command, InvokableUserCommand) or command.name != name: - continue - if result is None: - result = command - # we should check whether there's an ambiguity in command search - elif command is not result: - msg = ( - "The `guild_id` argument must be provided if there are different user commands " - "with the same name but different guilds or one of them is global." - ) - raise ValueError(msg) - - return result + return self._get_app_command(ApplicationCommandType.user, name, guild_id=guild_id) # pyright: ignore[reportReturnType] def get_message_command( self, name: str, *, guild_id: int | None = MISSING @@ -679,31 +659,7 @@ def get_message_command( :class:`InvokableMessageCommand` | :data:`None` The message command that was requested. If not found, returns :data:`None`. """ - if guild_id is not MISSING: - cmd_index = AppCmdIndex( - type=ApplicationCommandType.message, name=name, guild_id=guild_id - ) - command = self._all_app_commands.get(cmd_index) - if command is None: - return None - return command # pyright: ignore[reportReturnType] - - # this is mostly for backwards compatibility, as previously the guild_id arg didn't exist - result = None - for command in self._all_app_commands.values(): - if not isinstance(command, InvokableMessageCommand) or command.name != name: - continue - if result is None: - result = command - # we should check whether there's an ambiguity in command search - elif command is not result: - msg = ( - "The `guild_id` argument must be provided if there are different message commands " - "with the same name but different guilds or one of them is global." - ) - raise ValueError(msg) - - return result + return self._get_app_command(ApplicationCommandType.message, name, guild_id=guild_id) # pyright: ignore[reportReturnType] @overload def slash_command( From c154a3d07f6b09b8babd29274bad4031f2331e3e Mon Sep 17 00:00:00 2001 From: vi Date: Thu, 30 Jul 2026 18:16:40 +0200 Subject: [PATCH 58/67] refactor: move `chat_input -> slash` special casing into property --- disnake/enums.py | 13 ++++++++++ disnake/ext/commands/base_core.py | 27 +++++--------------- disnake/ext/commands/errors.py | 9 ++++--- disnake/ext/commands/interaction_bot_base.py | 22 +++++----------- 4 files changed, 30 insertions(+), 41 deletions(-) diff --git a/disnake/enums.py b/disnake/enums.py index f5527bcd35..b6179e5dec 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -8,6 +8,7 @@ TYPE_CHECKING, Any, ClassVar, + Literal, NamedTuple, NoReturn, TypeVar, @@ -1378,6 +1379,18 @@ class ApplicationCommandType(Enum): message = 3 """Represents a message command from the context menu.""" + @property + def _command_name(self): + # n.b. this currently won't return `None`, but it will once non-invocable command types are added + return _COMMAND_NAME_LOOKUP.get(self) + + +_COMMAND_NAME_LOOKUP: dict[ApplicationCommandType, Literal["slash", "user", "message"]] = { + ApplicationCommandType.chat_input: "slash", + ApplicationCommandType.user: "user", + ApplicationCommandType.message: "message", +} + class ApplicationCommandPermissionType(Enum): """Represents the type of a permission of an application command. diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index be13ef2f30..173f07327b 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -575,14 +575,9 @@ async def call_before_hooks(self, inter: ApplicationCommandInteraction) -> None: else: await self._before_invoke(inter) # pyright: ignore[reportArgumentType, reportCallIssue] - if inter.data.type is ApplicationCommandType.chat_input: - partial_attr_name = "slash_command" - elif inter.data.type is ApplicationCommandType.user: - partial_attr_name = "user_command" - elif inter.data.type is ApplicationCommandType.message: - partial_attr_name = "message_command" - else: + if not (type_name := inter.data.type._command_name): return + partial_attr_name = f"{type_name}_command" # call the cog local hook if applicable: if cog is not None: @@ -605,14 +600,9 @@ async def call_after_hooks(self, inter: ApplicationCommandInteraction) -> None: else: await self._after_invoke(inter) # pyright: ignore[reportArgumentType, reportCallIssue] - if inter.data.type is ApplicationCommandType.chat_input: - partial_attr_name = "slash_command" - elif inter.data.type is ApplicationCommandType.user: - partial_attr_name = "user_command" - elif inter.data.type is ApplicationCommandType.message: - partial_attr_name = "message_command" - else: + if not (type_name := inter.data.type._command_name): return + partial_attr_name = f"{type_name}_command" # call the cog local hook if applicable: if cog is not None: @@ -704,14 +694,9 @@ async def can_run(self, inter: ApplicationCommandInteraction) -> bool: original = inter.application_command inter.application_command = self - if inter.data.type is ApplicationCommandType.chat_input: - partial_attr_name = "slash_command" - elif inter.data.type is ApplicationCommandType.user: - partial_attr_name = "user_command" - elif inter.data.type is ApplicationCommandType.message: - partial_attr_name = "message_command" - else: + if not (type_name := inter.data.type._command_name): return True + partial_attr_name = f"{type_name}_command" try: if inter.bot and not await inter.bot.application_command_can_run(inter): diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index baf0a63046..83f76be452 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1108,12 +1108,13 @@ def __init__(self, cmd_type: ApplicationCommandType, name: str, guild_id: int | self.guild_id: int | None = guild_id # backwards compatibility self.alias_conflict: bool = False - # fixed API naming here because no one calls slash commands "chat input" - type_ = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name if guild_id is None: - msg = f"Global {type_} command {name} was specified earlier in your code." + msg = f"Global {cmd_type._command_name} command {name} was specified earlier in your code." else: - msg = f"Local {type_} command {name} with guild ID {guild_id} was specified earlier in your code." + msg = ( + f"Local {cmd_type._command_name} command {name} with" + f" guild ID {guild_id} was specified earlier in your code." + ) # this bypasses CommandRegistrationError.__init__ super(CommandRegistrationError, self).__init__(msg) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 8de7d9b667..df6e80481c 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -561,10 +561,9 @@ def _get_app_command( result = command # we should check whether there's an ambiguity in command search elif command is not result: - type_ = "slash" if cmd_type is ApplicationCommandType.chat_input else cmd_type.name msg = ( - f"The `guild_id` argument must be provided if there are different {type_} commands " - "with the same name but different guilds or one of them is global." + f"The `guild_id` argument must be provided if there are different {cmd_type._command_name}" + " commands with the same name but different guilds or one of them is global." ) raise ValueError(msg) @@ -1559,13 +1558,10 @@ async def application_command_can_run( ) -> bool: if inter.data.type is ApplicationCommandType.chat_input: checks = self._slash_command_check_once if call_once else self._slash_command_checks - elif inter.data.type is ApplicationCommandType.user: checks = self._user_command_check_once if call_once else self._user_command_checks - elif inter.data.type is ApplicationCommandType.message: checks = self._message_command_check_once if call_once else self._message_command_checks - else: return True @@ -1730,25 +1726,19 @@ async def process_application_commands( return command_type = interaction.data.type - event_name = None # `inter.data.guild_id` is the guild ID the command is registered to, # so this is correct even when a global command is called from a guild cmd_index = AppCmdIndex( type=command_type, name=interaction.data.name, guild_id=interaction.data.guild_id ) - app_command = self._all_app_commands.get(cmd_index) - - if command_type is ApplicationCommandType.chat_input: - event_name = "slash_command" - elif command_type is ApplicationCommandType.user: - event_name = "user_command" - elif command_type is ApplicationCommandType.message: - event_name = "message_command" - if event_name is None or app_command is None: + app_command = self._all_app_commands.get(cmd_index) + type_name = command_type._command_name + if app_command is None or type_name is None: # If we are here, the command being invoked is either unknown or has an unknown type. # This usually happens if the auto sync is disabled, so let's just ignore this. return + event_name = f"{type_name}_command" self.dispatch(event_name, interaction) try: From ad6cac7488d999d44196faada0b5298a21168145 Mon Sep 17 00:00:00 2001 From: vi Date: Fri, 31 Jul 2026 18:55:36 +0200 Subject: [PATCH 59/67] docs: add versionadded to new `guild_id` parameters --- disnake/ext/commands/interaction_bot_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index df6e80481c..aad6ac8798 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -587,6 +587,8 @@ def get_slash_command( The guild ID corresponding to the slash command or :data:`None` if it's a global command. If this is not specified but multiple commands with this name exist, and error will be raised. + .. versionadded:: |vnext| + Raises ------ TypeError @@ -620,6 +622,8 @@ def get_user_command( The guild ID corresponding to the user command or :data:`None` if it's a global command. If this is not specified but multiple commands with this name exist, and error will be raised. + .. versionadded:: |vnext| + Raises ------ ValueError @@ -647,6 +651,8 @@ def get_message_command( The guild ID corresponding to the message command or :data:`None` if it's a global command. If this is not specified but multiple commands with this name exist, and error will be raised. + .. versionadded:: |vnext| + Raises ------ ValueError From 92b1793f42c7fb9cc7b0ffaeacfc787187feea30 Mon Sep 17 00:00:00 2001 From: vi Date: Fri, 31 Jul 2026 18:56:09 +0200 Subject: [PATCH 60/67] chore: opinionated style changes --- disnake/ext/commands/interaction_bot_base.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index aad6ac8798..1f6103e223 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -1658,14 +1658,13 @@ async def process_app_command_autocompletion( cmd_index = AppCmdIndex( type=inter.data.type, name=inter.data.name, guild_id=inter.data.guild_id ) - # this happens to always be a slash command - slash_command = self._all_app_commands.get(cmd_index) - if slash_command is None: + # this happens to always be a slash command, the instance check is + # for `None` and to appease the type-checker + slash_command = self._all_app_commands.get(cmd_index) + if not isinstance(slash_command, InvokableSlashCommand): return - slash_command = cast("InvokableSlashCommand", slash_command) - inter.application_command = slash_command if slash_command.guild_ids is None or inter.guild_id in slash_command.guild_ids: await slash_command._call_relevant_autocompleter(inter) @@ -1738,11 +1737,12 @@ async def process_application_commands( type=command_type, name=interaction.data.name, guild_id=interaction.data.guild_id ) - app_command = self._all_app_commands.get(cmd_index) - type_name = command_type._command_name - if app_command is None or type_name is None: + if (app_command := self._all_app_commands.get(cmd_index)) is None: # If we are here, the command being invoked is either unknown or has an unknown type. - # This usually happens if the auto sync is disabled, so let's just ignore this. + # This usually happens if auto sync is disabled, so let's just ignore this. + return + if (type_name := command_type._command_name) is None: + # see above return event_name = f"{type_name}_command" From f0fc6b186be228e760e964790350eb58a0c03310 Mon Sep 17 00:00:00 2001 From: vi Date: Mon, 3 Aug 2026 16:00:21 +0200 Subject: [PATCH 61/67] docs: merge changelog fragments --- changelog/260.deprecate.0.rst | 1 - changelog/260.deprecate.1.rst | 1 - changelog/260.deprecate.2.rst | 1 - changelog/260.deprecate.rst | 3 +++ changelog/260.feature.0.rst | 1 - changelog/260.feature.1.rst | 1 - changelog/260.feature.2.rst | 1 - changelog/260.feature.rst | 4 ++++ 8 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 changelog/260.deprecate.0.rst delete mode 100644 changelog/260.deprecate.1.rst delete mode 100644 changelog/260.deprecate.2.rst create mode 100644 changelog/260.deprecate.rst delete mode 100644 changelog/260.feature.0.rst delete mode 100644 changelog/260.feature.1.rst delete mode 100644 changelog/260.feature.2.rst create mode 100644 changelog/260.feature.rst diff --git a/changelog/260.deprecate.0.rst b/changelog/260.deprecate.0.rst deleted file mode 100644 index 51658a46cd..0000000000 --- a/changelog/260.deprecate.0.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :attr:`.ext.commands.InteractionBot.all_slash_commands`, :attr:`.ext.commands.InteractionBot.all_user_commands`, :attr:`.ext.commands.InteractionBot.all_message_commands` are deprecated. diff --git a/changelog/260.deprecate.1.rst b/changelog/260.deprecate.1.rst deleted file mode 100644 index d383c8394b..0000000000 --- a/changelog/260.deprecate.1.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`.ext.commands.InteractionBot.add_slash_command`, :meth:`.ext.commands.InteractionBot.add_user_command`, :meth:`.ext.commands.InteractionBot.add_message_command` are deprecated. diff --git a/changelog/260.deprecate.2.rst b/changelog/260.deprecate.2.rst deleted file mode 100644 index bf20c12a10..0000000000 --- a/changelog/260.deprecate.2.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| :meth:`.ext.commands.InteractionBot.remove_slash_command`, :meth:`.ext.commands.InteractionBot.remove_user_command`, :meth:`.ext.commands.InteractionBot.remove_message_command` are deprecated. diff --git a/changelog/260.deprecate.rst b/changelog/260.deprecate.rst new file mode 100644 index 0000000000..9194ff69ec --- /dev/null +++ b/changelog/260.deprecate.rst @@ -0,0 +1,3 @@ +|commands| Deprecate several command-type-specific methods: +- :meth:`Bot.add_slash_command <.ext.commands.Bot.add_slash_command>`, :meth:`Bot.add_user_command <.ext.commands.Bot.add_user_command>`, and :meth:`Bot.add_message_command <.ext.commands.Bot.add_message_command>` are deprecated in favour of :meth:`Bot.add_app_command <.ext.commands.Bot.add_app_command>`. +- :meth:`Bot.remove_slash_command <.ext.commands.Bot.remove_slash_command>`, :meth:`Bot.remove_user_command <.ext.commands.Bot.remove_user_command>`, and :meth:`Bot.remove_message_command <.ext.commands.Bot.remove_message_command>` are deprecated in favour of :meth:`Bot.remove_app_command <.ext.commands.Bot.remove_app_command>`. diff --git a/changelog/260.feature.0.rst b/changelog/260.feature.0.rst deleted file mode 100644 index 2c5ae38652..0000000000 --- a/changelog/260.feature.0.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| Add :meth:`.ext.commands.InteractionBot.add_app_command` and :meth:`.ext.commands.InteractionBot.remove_app_command`. diff --git a/changelog/260.feature.1.rst b/changelog/260.feature.1.rst deleted file mode 100644 index e48cb5a22e..0000000000 --- a/changelog/260.feature.1.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| Add ``guild_id`` parameter to :meth:`.ext.commands.InteractionBot.get_slash_command`, :meth:`.ext.commands.InteractionBot.get_user_command`, and :meth:`.ext.commands.InteractionBot.get_message_command`. diff --git a/changelog/260.feature.2.rst b/changelog/260.feature.2.rst deleted file mode 100644 index 191f973b7e..0000000000 --- a/changelog/260.feature.2.rst +++ /dev/null @@ -1 +0,0 @@ -|commands| Add :class:`.ext.commands.ApplicationCommandRegistrationError`. diff --git a/changelog/260.feature.rst b/changelog/260.feature.rst new file mode 100644 index 0000000000..ff1382685f --- /dev/null +++ b/changelog/260.feature.rst @@ -0,0 +1,4 @@ +|commands| Support creating multiple per-guild application commands with the same name in different guilds. +- Add :meth:`Bot.add_app_command <.ext.commands.Bot.add_app_command>` and :meth:`Bot.remove_app_command <.ext.commands.Bot.remove_app_command>` (superseding ``.add_(slash|user|message)_command`` and ``.remove_(slash|user|message)_command``). +- Add :class:`~.ext.commands.ApplicationCommandRegistrationError`, which is raised when an identical command already exists. +- Add ``guild_id`` parameter to :meth:`Bot.get_slash_command <.ext.commands.Bot.get_slash_command>`, :meth:`Bot.get_user_command <.ext.commands.Bot.get_user_command>`, and :meth:`Bot.get_message_command <.ext.commands.Bot.get_message_command>`. From bbde5d66c27f0929d8337cd70e674c871ad67693 Mon Sep 17 00:00:00 2001 From: vi Date: Sun, 9 Aug 2026 18:33:53 +0200 Subject: [PATCH 62/67] docs: remove module prefix --- disnake/ext/commands/base_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 846d4db839..ff857ced9e 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -95,7 +95,7 @@ class AppCmdIndex(NamedTuple): Attributes ---------- - type: :class:`disnake.ApplicationCommandType` + type: :class:`.ApplicationCommandType` The type of the application command being stored. name: :class:`str` The name of the application command being stored. From 35455ee6a8f8b820ac03c4f955dd51156a77b99c Mon Sep 17 00:00:00 2001 From: vi Date: Fri, 21 Aug 2026 12:50:32 +0200 Subject: [PATCH 63/67] docs: add changelog fragment for `inter.data.guild_id` --- changelog/1107.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1107.feature.rst diff --git a/changelog/1107.feature.rst b/changelog/1107.feature.rst new file mode 100644 index 0000000000..af1a71a88d --- /dev/null +++ b/changelog/1107.feature.rst @@ -0,0 +1 @@ +Add :attr:`ApplicationCommandInteractionData.guild_id`, representing the guild the invoked command was registered to. From 7d149ef7d0ad76715d15b9a85e2650bcc2b86433 Mon Sep 17 00:00:00 2001 From: vi Date: Mon, 24 Aug 2026 20:06:06 +0200 Subject: [PATCH 64/67] docs: remove implicitly raised error --- disnake/ext/commands/interaction_bot_base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 1f6103e223..f4c601c090 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -591,8 +591,6 @@ def get_slash_command( Raises ------ - TypeError - The name is not a string. ValueError The ``guild_id`` parameter was not provided in a case where different slash commands have the same name but different guild_ids. From b4905e7e55118c1149a7259a34f8dfb2aeb2fc45 Mon Sep 17 00:00:00 2001 From: vi <8530778+shiftinv@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:11:20 +0200 Subject: [PATCH 65/67] docs: grammar fix Co-authored-by: Eneg <42005170+Enegg@users.noreply.github.com> Signed-off-by: vi <8530778+shiftinv@users.noreply.github.com> --- disnake/ext/commands/interaction_bot_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index f4c601c090..44dd8e0627 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -585,7 +585,7 @@ def get_slash_command( The name of the slash command to get. guild_id: :class:`int` | :data:`None` The guild ID corresponding to the slash command or :data:`None` if it's a global command. - If this is not specified but multiple commands with this name exist, and error will be raised. + If this is not specified but multiple commands with this name exist, an error will be raised. .. versionadded:: |vnext| @@ -618,7 +618,7 @@ def get_user_command( The name of the user command to get. guild_id: :class:`int` | :data:`None` The guild ID corresponding to the user command or :data:`None` if it's a global command. - If this is not specified but multiple commands with this name exist, and error will be raised. + If this is not specified but multiple commands with this name exist, an error will be raised. .. versionadded:: |vnext| @@ -647,7 +647,7 @@ def get_message_command( The name of the message command to get. guild_id: :class:`int` | :data:`None` The guild ID corresponding to the message command or :data:`None` if it's a global command. - If this is not specified but multiple commands with this name exist, and error will be raised. + If this is not specified but multiple commands with this name exist, an error will be raised. .. versionadded:: |vnext| From 91fe8dc7ec857cd7d299bb7f2ed2fe0d27590dd2 Mon Sep 17 00:00:00 2001 From: vi Date: Mon, 24 Aug 2026 20:22:25 +0200 Subject: [PATCH 66/67] feat: raise ValueError if slash name chain is 4+ parts --- disnake/ext/commands/interaction_bot_base.py | 26 ++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 44dd8e0627..e7c6b1baf3 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -143,15 +143,20 @@ def _match_subcommand_chain( """ if command.name != chain[0]: return None - if len(chain) == 1: - return command - if len(chain) == 2: - return command.children.get(chain[1]) - if len(chain) == 3: - group = command.children.get(chain[1]) - if isinstance(group, SubCommandGroup): - return group.children.get(chain[2]) - return None + + match len(chain): + case 1: + return command + case 2: + return command.children.get(chain[1]) + case 3: + group = command.children.get(chain[1]) + if isinstance(group, SubCommandGroup): + return group.children.get(chain[2]) + return None + case _: + msg = "Expected at most 3 command name parts" + raise ValueError(msg) class InteractionBotBase(CommonBotBase): @@ -593,7 +598,8 @@ def get_slash_command( ------ ValueError The ``guild_id`` parameter was not provided in a case where different slash commands - have the same name but different guild_ids. + have the same name but different guild_ids, or the ``name`` parameter contained + more than 3 space-separated parts. Returns ------- From 769e254a77d74552012309b3f8930bc01c8227fa Mon Sep 17 00:00:00 2001 From: vi Date: Mon, 24 Aug 2026 20:28:08 +0200 Subject: [PATCH 67/67] refactor: change `ApplicationCommandRegistrationError` to accept `AppCmdIndex` --- disnake/ext/commands/errors.py | 25 ++++++++++++-------- disnake/ext/commands/interaction_bot_base.py | 4 +--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 37dcfedddb..629919fc0c 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -18,6 +18,7 @@ from disnake.types.snowflake import Snowflake, SnowflakeList from ._types import Coro + from .base_core import AppCmdIndex from .context import AnyContext from .cooldowns import BucketType, Cooldown from .flag_converter import Flag @@ -1112,19 +1113,23 @@ class ApplicationCommandRegistrationError(CommandRegistrationError): or :data:`None` if it was a global command. """ - def __init__(self, cmd_type: ApplicationCommandType, name: str, guild_id: int | None) -> None: - self.cmd_type: ApplicationCommandType = cmd_type - self.name: str = name - self.guild_id: int | None = guild_id - # backwards compatibility - self.alias_conflict: bool = False - if guild_id is None: - msg = f"Global {cmd_type._command_name} command {name} was specified earlier in your code." + def __init__(self, index: AppCmdIndex) -> None: + self.cmd_type: ApplicationCommandType = index.type + self.name: str = index.name + self.guild_id: int | None = index.guild_id + if self.guild_id is None: + msg = ( + f"Global {self.cmd_type._command_name} command {self.name}" + " was specified earlier in your code." + ) else: msg = ( - f"Local {cmd_type._command_name} command {name} with" - f" guild ID {guild_id} was specified earlier in your code." + f"Local {self.cmd_type._command_name} command {self.name} with" + f" guild ID {self.guild_id} was specified earlier in your code." ) + + # backwards compatibility + self.alias_conflict: bool = False # this bypasses CommandRegistrationError.__init__ super(CommandRegistrationError, self).__init__(msg) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index e7c6b1baf3..caf1a4ee2b 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -342,9 +342,7 @@ def add_app_command(self, app_command: InvokableApplicationCommand, /) -> None: type=app_command.body.type, name=app_command.name, guild_id=guild_id ) if cmd_index in self._all_app_commands: - raise ApplicationCommandRegistrationError( - cmd_index.type, cmd_index.name, cmd_index.guild_id - ) + raise ApplicationCommandRegistrationError(cmd_index) indices.append(cmd_index) for cmd_index in indices: