Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: store invokable application commands properly #1107

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3c37179
A draft of the app commands storage refactor
EQUENOS Sep 16, 2023
b650009
Add missing deprecation warnings and properties
EQUENOS Sep 18, 2023
ec3896d
Fix a bad check
EQUENOS Sep 18, 2023
ea1ad65
Fix linting issues
EQUENOS Sep 18, 2023
424ed03
Update docs and add changelog
EQUENOS Sep 18, 2023
a2b0677
Avoid unnecessary generators
EQUENOS Sep 18, 2023
dbd7920
Fix docs references
EQUENOS Sep 18, 2023
e86e5cb
Add errors for ambiguous app command searches
EQUENOS Sep 19, 2023
f80dbf6
First word of the docstring should not be "This"
EQUENOS Sep 19, 2023
4ed930d
docs: group some changelog entries
EQUENOS Sep 22, 2023
192d2b9
chore: rename `AppCommandRegistrationError`
EQUENOS Sep 22, 2023
686bd48
docs: fix references in changelog entries
EQUENOS Sep 22, 2023
8b5916c
docs: hide the `ext.commands` prefix in changelog entries
EQUENOS Sep 22, 2023
88584c7
docs: revert the removal of `ext.commands` prefix
EQUENOS Sep 23, 2023
1f10a8d
fix: subcommands trying to be top level commands
EQUENOS Sep 23, 2023
f5d5d57
chore: remove deprecated undocumented things
EQUENOS Sep 23, 2023
164df50
chore: fix a small linting issue
EQUENOS Sep 23, 2023
0064e9a
chore: replace `warn_deprecated` with `deprecated `
EQUENOS Sep 24, 2023
4736dad
chore: resolve `remove_app_command` nits
EQUENOS Sep 24, 2023
8c6bb90
chore: add suggested typings
EQUENOS Sep 24, 2023
c44806b
docs: fix typos and improve comments
EQUENOS Sep 24, 2023
ef591e6
chore: simplify ambiguity checks
EQUENOS Sep 24, 2023
42c7e6f
docs: explain command location priorities
EQUENOS Sep 25, 2023
9a4d1aa
Make `bot.all_app_commands` a mapping proxy
EQUENOS Nov 3, 2023
55299b2
fix: unintended breaking change (handling `guild_ids=[]`)
EQUENOS Nov 3, 2023
8dc756e
docs: `AppCmdIndex`
EQUENOS Nov 3, 2023
d519711
fix: docs and linting
EQUENOS Nov 3, 2023
413f759
Make `AppCmdIndex` public
EQUENOS Nov 3, 2023
304a829
docs: fix a reference
EQUENOS Nov 3, 2023
63ce007
docs: improve AppCmdIndex docstring
EQUENOS Nov 3, 2023
a580238
docs: small fixes; chore: move `AppCmdIndex` to a different file
EQUENOS Dec 11, 2023
764324a
Merge branch 'master' into refactor/app-command-storage
EQUENOS Dec 11, 2023
ebdeb33
fix: prevent an unintended breaking change
EQUENOS Dec 11, 2023
93880cf
refactor: `remove_app_command` now takes a single `guild_id`
EQUENOS Dec 11, 2023
84eff42
refactor: use `@deprecated` for `add_xxx_command`
EQUENOS Dec 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/260.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| Allow registering 2 commands with the same type and name in different guilds.
1 change: 1 addition & 0 deletions changelog/260.deprecate.0.rst
EQUENOS marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +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.
1 change: 1 addition & 0 deletions changelog/260.deprecate.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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.
1 change: 1 addition & 0 deletions changelog/260.deprecate.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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.
1 change: 1 addition & 0 deletions changelog/260.feature.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| Add :meth:`.ext.commands.InteractionBot.add_app_command` and :meth:`.ext.commands.InteractionBot.remove_app_command`.
1 change: 1 addition & 0 deletions changelog/260.feature.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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`.
1 change: 1 addition & 0 deletions changelog/260.feature.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
|commands| Add :class:`.ext.commands.ApplicationCommandRegistrationError`.
23 changes: 22 additions & 1 deletion disnake/ext/commands/base_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Callable,
Dict,
List,
NamedTuple,
Optional,
Tuple,
TypeVar,
Expand Down Expand Up @@ -49,7 +50,7 @@
]


__all__ = ("InvokableApplicationCommand", "default_member_permissions")
__all__ = ("AppCmdIndex", "InvokableApplicationCommand", "default_member_permissions")


T = TypeVar("T")
Expand Down Expand Up @@ -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.

Expand Down
29 changes: 10 additions & 19 deletions disnake/ext/commands/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -753,22 +753,16 @@ 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:
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_commands(
to_undo.body.type, to_undo.name, guild_ids=to_undo.guild_ids
)
raise

if not hasattr(self.cog_load.__func__, "__cog_special_method__"):
Expand Down Expand Up @@ -841,12 +835,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_commands(
app_command.body.type, app_command.name, guild_ids=app_command.guild_ids
)
Comment on lines 837 to +840
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't pose an issue right now, but may need to check for SubCommand(Group) here too.


for name, method_name in self.__cog_listeners__:
bot.remove_listener(getattr(self, method_name), name)
Expand Down
16 changes: 10 additions & 6 deletions disnake/ext/commands/ctx_menus_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
42 changes: 42 additions & 0 deletions disnake/ext/commands/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -74,6 +75,7 @@
"ExtensionFailed",
"ExtensionNotFound",
"CommandRegistrationError",
"ApplicationCommandRegistrationError",
"FlagError",
"BadFlagArgument",
"MissingFlagArgument",
Expand Down Expand Up @@ -1025,6 +1027,46 @@ def __init__(self, name: str, *, alias_conflict: bool = False) -> None:
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 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.

This inherits from :exc:`CommandRegistrationError`

.. versionadded:: 2.10

Attributes
----------
cmd_type: :class:`disnake.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
EQUENOS marked this conversation as resolved.
Show resolved Hide resolved
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:
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.

Expand Down
Loading