Browse Source

[commands] Add support for with_app_command in hybrid commands

This allows the user to make a text-only command without it registering
as an application command
pull/7999/head
Rapptz 3 years ago
parent
commit
ccc737eb07
  1. 16
      discord/ext/commands/bot.py
  2. 11
      discord/ext/commands/cog.py
  3. 114
      discord/ext/commands/hybrid.py

16
discord/ext/commands/bot.py

@ -224,23 +224,23 @@ class BotBase(GroupMixin[None]):
@discord.utils.copy_doc(GroupMixin.add_command) @discord.utils.copy_doc(GroupMixin.add_command)
def add_command(self, command: Command[Any, ..., Any], /) -> None: def add_command(self, command: Command[Any, ..., Any], /) -> None:
super().add_command(command) super().add_command(command)
if hasattr(command, '__commands_is_hybrid__'): if isinstance(command, (HybridCommand, HybridGroup)) and command.app_command:
# If a cog is also inheriting from app_commands.Group then it'll also # If a cog is also inheriting from app_commands.Group then it'll also
# add the hybrid commands as text commands, which would recursively add the # add the hybrid commands as text commands, which would recursively add the
# hybrid commands as slash commands. This check just terminates that recursion # hybrid commands as slash commands. This check just terminates that recursion
# from happening # from happening
if command.cog is None or not command.cog.__cog_is_app_commands_group__: if command.cog is None or not command.cog.__cog_is_app_commands_group__:
self.tree.add_command(command.app_command) # type: ignore self.tree.add_command(command.app_command)
@discord.utils.copy_doc(GroupMixin.remove_command) @discord.utils.copy_doc(GroupMixin.remove_command)
def remove_command(self, name: str, /) -> Optional[Command[Any, ..., Any]]: def remove_command(self, name: str, /) -> Optional[Command[Any, ..., Any]]:
cmd = super().remove_command(name) cmd: Optional[Command[Any, ..., Any]] = super().remove_command(name)
if cmd is not None and hasattr(cmd, '__commands_is_hybrid__'): if isinstance(cmd, (HybridCommand, HybridGroup)) and cmd.app_command:
# See above # See above
if cmd.cog is not None and cmd.cog.__cog_is_app_commands_group__: if cmd.cog is not None and cmd.cog.__cog_is_app_commands_group__:
return cmd return cmd
guild_ids: Optional[List[int]] = cmd.app_command._guild_ids # type: ignore guild_ids: Optional[List[int]] = cmd.app_command._guild_ids
if guild_ids is None: if guild_ids is None:
self.__tree.remove_command(name) self.__tree.remove_command(name)
else: else:
@ -252,6 +252,7 @@ class BotBase(GroupMixin[None]):
def hybrid_command( def hybrid_command(
self, self,
name: str = MISSING, name: str = MISSING,
with_app_command: bool = True,
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridCommand[Any, P, T]]: ) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridCommand[Any, P, T]]:
@ -266,7 +267,7 @@ class BotBase(GroupMixin[None]):
def decorator(func: CommandCallback[Any, ContextT, P, T]): def decorator(func: CommandCallback[Any, ContextT, P, T]):
kwargs.setdefault('parent', self) kwargs.setdefault('parent', self)
result = hybrid_command(name=name, *args, **kwargs)(func) result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
self.add_command(result) self.add_command(result)
return result return result
@ -275,6 +276,7 @@ class BotBase(GroupMixin[None]):
def hybrid_group( def hybrid_group(
self, self,
name: str = MISSING, name: str = MISSING,
with_app_command: bool = True,
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridGroup[Any, P, T]]: ) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridGroup[Any, P, T]]:
@ -289,7 +291,7 @@ class BotBase(GroupMixin[None]):
def decorator(func: CommandCallback[Any, ContextT, P, T]): def decorator(func: CommandCallback[Any, ContextT, P, T]):
kwargs.setdefault('parent', self) kwargs.setdefault('parent', self)
result = hybrid_group(name=name, *args, **kwargs)(func) result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
self.add_command(result) self.add_command(result)
return result return result

11
discord/ext/commands/cog.py

@ -291,10 +291,15 @@ class Cog(metaclass=CogMeta):
parent.add_command(command) # type: ignore parent.add_command(command) # type: ignore
elif self.__cog_app_commands_group__: elif self.__cog_app_commands_group__:
if hasattr(command, '__commands_is_hybrid__') and command.parent is None: if hasattr(command, '__commands_is_hybrid__') and command.parent is None:
# In both of these, the type checker does not see the app_command attribute even though it exists
parent = self.__cog_app_commands_group__ parent = self.__cog_app_commands_group__
command.app_command = command.app_command._copy_with(parent=parent, binding=self) # type: ignore app_command: Optional[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = getattr(
children.append(command.app_command) # type: ignore command, 'app_command', None
)
if app_command:
app_command = app_command._copy_with(parent=parent, binding=self)
children.append(app_command)
# The type checker does not see the app_command attribute even though it exists
command.app_command = app_command # type: ignore
for command in cls.__cog_app_commands__: for command in cls.__cog_app_commands__:
copy = command._copy_with(parent=self.__cog_app_commands_group__, binding=self) copy = command._copy_with(parent=self.__cog_app_commands_group__, binding=self)

114
discord/ext/commands/hybrid.py

@ -386,7 +386,15 @@ class HybridCommand(Command[CogT, P, T]):
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
super().__init__(func, **kwargs) super().__init__(func, **kwargs)
self.app_command: HybridAppCommand[CogT, Any, T] = HybridAppCommand(self) self.with_app_command: bool = kwargs.pop('with_app_command', True)
self.with_command: bool = kwargs.pop('with_command', True)
if not self.with_command and not self.with_app_command:
raise TypeError('cannot set both with_command and with_app_command to False')
self.app_command: Optional[HybridAppCommand[CogT, Any, T]] = (
HybridAppCommand(self) if self.with_app_command else None
)
@property @property
def cog(self) -> CogT: def cog(self) -> CogT:
@ -395,25 +403,29 @@ class HybridCommand(Command[CogT, P, T]):
@cog.setter @cog.setter
def cog(self, value: CogT) -> None: def cog(self, value: CogT) -> None:
self._cog = value self._cog = value
self.app_command.binding = value if self.app_command is not None:
self.app_command.binding = value
async def can_run(self, ctx: Context[BotT], /) -> bool: async def can_run(self, ctx: Context[BotT], /) -> bool:
if ctx.interaction is None: if ctx.interaction is not None and self.app_command:
return await super().can_run(ctx)
else:
return await self.app_command._check_can_run(ctx.interaction) return await self.app_command._check_can_run(ctx.interaction)
else:
return await super().can_run(ctx)
async def _parse_arguments(self, ctx: Context[BotT]) -> None: async def _parse_arguments(self, ctx: Context[BotT]) -> None:
interaction = ctx.interaction interaction = ctx.interaction
if interaction is None: if interaction is None:
return await super()._parse_arguments(ctx) return await super()._parse_arguments(ctx)
else: elif self.app_command:
ctx.kwargs = await self.app_command._transform_arguments(interaction, interaction.namespace) ctx.kwargs = await self.app_command._transform_arguments(interaction, interaction.namespace)
def _ensure_assignment_on_copy(self, other: Self) -> Self: def _ensure_assignment_on_copy(self, other: Self) -> Self:
copy = super()._ensure_assignment_on_copy(other) copy = super()._ensure_assignment_on_copy(other)
copy.app_command = self.app_command.copy() if self.app_command is None:
copy.app_command.wrapped = copy copy.app_command = None
else:
copy.app_command = self.app_command.copy()
copy.app_command.wrapped = copy
return copy return copy
def autocomplete( def autocomplete(
@ -441,6 +453,9 @@ class HybridCommand(Command[CogT, P, T]):
The coroutine passed is not actually a coroutine or The coroutine passed is not actually a coroutine or
the parameter is not found or of an invalid type. the parameter is not found or of an invalid type.
""" """
if self.app_command is None:
raise TypeError('This command does not have a registered application command')
return self.app_command.autocomplete(name) return self.app_command.autocomplete(name)
@ -473,6 +488,8 @@ class HybridGroup(Group[CogT, P, T]):
def __init__(self, *args: Any, fallback: Optional[str] = None, **attrs: Any) -> None: def __init__(self, *args: Any, fallback: Optional[str] = None, **attrs: Any) -> None:
super().__init__(*args, **attrs) super().__init__(*args, **attrs)
self.invoke_without_command = True self.invoke_without_command = True
self.with_app_command: bool = attrs.pop('with_app_command', True)
parent = None parent = None
if self.parent is not None: if self.parent is not None:
if isinstance(self.parent, HybridGroup): if isinstance(self.parent, HybridGroup):
@ -480,28 +497,38 @@ class HybridGroup(Group[CogT, P, T]):
else: else:
raise TypeError(f'HybridGroup parent must be HybridGroup not {self.parent.__class__}') raise TypeError(f'HybridGroup parent must be HybridGroup not {self.parent.__class__}')
guild_ids = attrs.pop('guild_ids', None) or getattr(self.callback, '__discord_app_commands_default_guilds__', None) # I would love for this to be Optional[app_commands.Group]
guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False) # However, Python does not have conditional typing so it's very hard to
default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None) # make this type depend on the with_app_command bool without a lot of needless repetition
self.app_command: app_commands.Group = app_commands.Group( self.app_command: app_commands.Group = MISSING
name=self.name,
description=self.description or self.short_doc or '',
guild_ids=guild_ids,
guild_only=guild_only,
default_permissions=default_permissions,
)
# This prevents the group from re-adding the command at __init__
self.app_command.parent = parent
self.fallback: Optional[str] = fallback self.fallback: Optional[str] = fallback
if fallback is not None: if self.with_app_command:
command = HybridAppCommand(self) guild_ids = attrs.pop('guild_ids', None) or getattr(
command.name = fallback self.callback, '__discord_app_commands_default_guilds__', None
self.app_command.add_command(command) )
guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False)
default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None)
self.app_command = app_commands.Group(
name=self.name,
description=self.description or self.short_doc or '',
guild_ids=guild_ids,
guild_only=guild_only,
default_permissions=default_permissions,
)
# This prevents the group from re-adding the command at __init__
self.app_command.parent = parent
if fallback is not None:
command = HybridAppCommand(self)
command.name = fallback
self.app_command.add_command(command)
@property @property
def _fallback_command(self) -> Optional[HybridAppCommand[CogT, ..., T]]: def _fallback_command(self) -> Optional[HybridAppCommand[CogT, ..., T]]:
if self.app_command is MISSING:
return None
return self.app_command.get_command(self.fallback) # type: ignore return self.app_command.get_command(self.fallback) # type: ignore
@property @property
@ -596,7 +623,9 @@ class HybridGroup(Group[CogT, P, T]):
if isinstance(command, HybridGroup) and self.parent is not None: if isinstance(command, HybridGroup) and self.parent is not None:
raise ValueError(f'{command.qualified_name!r} is too nested, groups can only be nested at most one level') raise ValueError(f'{command.qualified_name!r} is too nested, groups can only be nested at most one level')
self.app_command.add_command(command.app_command) if command.app_command and self.app_command:
self.app_command.add_command(command.app_command)
command.parent = self command.parent = self
if command.name in self.all_commands: if command.name in self.all_commands:
@ -611,13 +640,15 @@ class HybridGroup(Group[CogT, P, T]):
def remove_command(self, name: str, /) -> Optional[Command[CogT, ..., Any]]: def remove_command(self, name: str, /) -> Optional[Command[CogT, ..., Any]]:
cmd = super().remove_command(name) cmd = super().remove_command(name)
self.app_command.remove_command(name) if self.app_command:
self.app_command.remove_command(name)
return cmd return cmd
def command( def command(
self, self,
name: str = MISSING, name: str = MISSING,
*args: Any, *args: Any,
with_app_command: bool = True,
**kwargs: Any, **kwargs: Any,
) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridCommand[CogT, P2, U]]: ) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridCommand[CogT, P2, U]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to """A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to
@ -631,7 +662,7 @@ class HybridGroup(Group[CogT, P, T]):
def decorator(func: CommandCallback[CogT, ContextT, P2, U]): def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
kwargs.setdefault('parent', self) kwargs.setdefault('parent', self)
result = hybrid_command(name=name, *args, **kwargs)(func) result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
self.add_command(result) self.add_command(result)
return result return result
@ -641,6 +672,7 @@ class HybridGroup(Group[CogT, P, T]):
self, self,
name: str = MISSING, name: str = MISSING,
*args: Any, *args: Any,
with_app_command: bool = True,
**kwargs: Any, **kwargs: Any,
) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridGroup[CogT, P2, U]]: ) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridGroup[CogT, P2, U]]:
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_group` and adds it to """A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_group` and adds it to
@ -654,7 +686,7 @@ class HybridGroup(Group[CogT, P, T]):
def decorator(func: CommandCallback[CogT, ContextT, P2, U]): def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
kwargs.setdefault('parent', self) kwargs.setdefault('parent', self)
result = hybrid_group(name=name, *args, **kwargs)(func) result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
self.add_command(result) self.add_command(result)
return result return result
@ -663,9 +695,11 @@ class HybridGroup(Group[CogT, P, T]):
def hybrid_command( def hybrid_command(
name: str = MISSING, name: str = MISSING,
*,
with_app_command: bool = True,
**attrs: Any, **attrs: Any,
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridCommand[CogT, P, T]]: ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridCommand[CogT, P, T]]:
"""A decorator that transforms a function into a :class:`.HybridCommand`. r"""A decorator that transforms a function into a :class:`.HybridCommand`.
A hybrid command is one that functions both as a regular :class:`.Command` A hybrid command is one that functions both as a regular :class:`.Command`
and one that is also a :class:`app_commands.Command <discord.app_commands.Command>`. and one that is also a :class:`app_commands.Command <discord.app_commands.Command>`.
@ -690,7 +724,9 @@ def hybrid_command(
name: :class:`str` name: :class:`str`
The name to create the command with. By default this uses the The name to create the command with. By default this uses the
function name unchanged. function name unchanged.
attrs with_app_command: :class:`bool`
Whether to register the command as an application command.
\*\*attrs
Keyword arguments to pass into the construction of the Keyword arguments to pass into the construction of the
hybrid command. hybrid command.
@ -703,24 +739,36 @@ def hybrid_command(
def decorator(func: CommandCallback[CogT, ContextT, P, T]): def decorator(func: CommandCallback[CogT, ContextT, P, T]):
if isinstance(func, Command): if isinstance(func, Command):
raise TypeError('Callback is already a command.') raise TypeError('Callback is already a command.')
return HybridCommand(func, name=name, **attrs) return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs)
return decorator return decorator
def hybrid_group( def hybrid_group(
name: str = MISSING, name: str = MISSING,
*,
with_app_command: bool = True,
**attrs: Any, **attrs: Any,
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridGroup[CogT, P, T]]: ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridGroup[CogT, P, T]]:
"""A decorator that transforms a function into a :class:`.HybridGroup`. """A decorator that transforms a function into a :class:`.HybridGroup`.
This is similar to the :func:`~discord.ext.commands.group` decorator except it creates This is similar to the :func:`~discord.ext.commands.group` decorator except it creates
a hybrid group instead. a hybrid group instead.
Parameters
-----------
with_app_command: :class:`bool`
Whether to register the command as an application command.
Raises
-------
TypeError
If the function is not a coroutine or is already a command.
""" """
def decorator(func: CommandCallback[CogT, ContextT, P, T]): def decorator(func: CommandCallback[CogT, ContextT, P, T]):
if isinstance(func, Command): if isinstance(func, Command):
raise TypeError('Callback is already a command.') raise TypeError('Callback is already a command.')
return HybridGroup(func, name=name, **attrs) return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs)
return decorator # type: ignore return decorator # type: ignore

Loading…
Cancel
Save