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)
def add_command(self, command: Command[Any, ..., Any], /) -> None:
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
# add the hybrid commands as text commands, which would recursively add the
# hybrid commands as slash commands. This check just terminates that recursion
# from happening
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)
def remove_command(self, name: str, /) -> Optional[Command[Any, ..., Any]]:
cmd = super().remove_command(name)
if cmd is not None and hasattr(cmd, '__commands_is_hybrid__'):
cmd: Optional[Command[Any, ..., Any]] = super().remove_command(name)
if isinstance(cmd, (HybridCommand, HybridGroup)) and cmd.app_command:
# See above
if cmd.cog is not None and cmd.cog.__cog_is_app_commands_group__:
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:
self.__tree.remove_command(name)
else:
@ -252,6 +252,7 @@ class BotBase(GroupMixin[None]):
def hybrid_command(
self,
name: str = MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
) -> 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]):
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)
return result
@ -275,6 +276,7 @@ class BotBase(GroupMixin[None]):
def hybrid_group(
self,
name: str = MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
) -> 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]):
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)
return result

11
discord/ext/commands/cog.py

@ -291,10 +291,15 @@ class Cog(metaclass=CogMeta):
parent.add_command(command) # type: ignore
elif self.__cog_app_commands_group__:
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__
command.app_command = command.app_command._copy_with(parent=parent, binding=self) # type: ignore
children.append(command.app_command) # type: ignore
app_command: Optional[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = getattr(
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__:
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,
) -> None:
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
def cog(self) -> CogT:
@ -395,25 +403,29 @@ class HybridCommand(Command[CogT, P, T]):
@cog.setter
def cog(self, value: CogT) -> None:
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:
if ctx.interaction is None:
return await super().can_run(ctx)
else:
if ctx.interaction is not None and self.app_command:
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:
interaction = ctx.interaction
if interaction is None:
return await super()._parse_arguments(ctx)
else:
elif self.app_command:
ctx.kwargs = await self.app_command._transform_arguments(interaction, interaction.namespace)
def _ensure_assignment_on_copy(self, other: Self) -> Self:
copy = super()._ensure_assignment_on_copy(other)
copy.app_command = self.app_command.copy()
copy.app_command.wrapped = copy
if self.app_command is None:
copy.app_command = None
else:
copy.app_command = self.app_command.copy()
copy.app_command.wrapped = copy
return copy
def autocomplete(
@ -441,6 +453,9 @@ class HybridCommand(Command[CogT, P, T]):
The coroutine passed is not actually a coroutine or
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)
@ -473,6 +488,8 @@ class HybridGroup(Group[CogT, P, T]):
def __init__(self, *args: Any, fallback: Optional[str] = None, **attrs: Any) -> None:
super().__init__(*args, **attrs)
self.invoke_without_command = True
self.with_app_command: bool = attrs.pop('with_app_command', True)
parent = None
if self.parent is not None:
if isinstance(self.parent, HybridGroup):
@ -480,28 +497,38 @@ class HybridGroup(Group[CogT, P, T]):
else:
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)
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 = 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
# I would love for this to be Optional[app_commands.Group]
# However, Python does not have conditional typing so it's very hard to
# make this type depend on the with_app_command bool without a lot of needless repetition
self.app_command: app_commands.Group = MISSING
self.fallback: Optional[str] = fallback
if fallback is not None:
command = HybridAppCommand(self)
command.name = fallback
self.app_command.add_command(command)
if self.with_app_command:
guild_ids = attrs.pop('guild_ids', None) or getattr(
self.callback, '__discord_app_commands_default_guilds__', None
)
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
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
@property
@ -596,7 +623,9 @@ class HybridGroup(Group[CogT, P, T]):
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')
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
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]]:
cmd = super().remove_command(name)
self.app_command.remove_command(name)
if self.app_command:
self.app_command.remove_command(name)
return cmd
def command(
self,
name: str = MISSING,
*args: Any,
with_app_command: bool = True,
**kwargs: Any,
) -> 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
@ -631,7 +662,7 @@ class HybridGroup(Group[CogT, P, T]):
def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
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)
return result
@ -641,6 +672,7 @@ class HybridGroup(Group[CogT, P, T]):
self,
name: str = MISSING,
*args: Any,
with_app_command: bool = True,
**kwargs: Any,
) -> 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
@ -654,7 +686,7 @@ class HybridGroup(Group[CogT, P, T]):
def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
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)
return result
@ -663,9 +695,11 @@ class HybridGroup(Group[CogT, P, T]):
def hybrid_command(
name: str = MISSING,
*,
with_app_command: bool = True,
**attrs: Any,
) -> 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`
and one that is also a :class:`app_commands.Command <discord.app_commands.Command>`.
@ -690,7 +724,9 @@ def hybrid_command(
name: :class:`str`
The name to create the command with. By default this uses the
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
hybrid command.
@ -703,24 +739,36 @@ def hybrid_command(
def decorator(func: CommandCallback[CogT, ContextT, P, T]):
if isinstance(func, 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
def hybrid_group(
name: str = MISSING,
*,
with_app_command: bool = True,
**attrs: Any,
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridGroup[CogT, P, T]]:
"""A decorator that transforms a function into a :class:`.HybridGroup`.
This is similar to the :func:`~discord.ext.commands.group` decorator except it creates
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]):
if isinstance(func, 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

Loading…
Cancel
Save