Skip to content
This repository was archived by the owner on Aug 28, 2019. It is now read-only.

Commit ccc737e

Browse files
committed
[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
1 parent e3ea470 commit ccc737e

File tree

3 files changed

+98
-43
lines changed

3 files changed

+98
-43
lines changed

discord/ext/commands/bot.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,23 +224,23 @@ async def close(self) -> None:
224224
@discord.utils.copy_doc(GroupMixin.add_command)
225225
def add_command(self, command: Command[Any, ..., Any], /) -> None:
226226
super().add_command(command)
227-
if hasattr(command, '__commands_is_hybrid__'):
227+
if isinstance(command, (HybridCommand, HybridGroup)) and command.app_command:
228228
# If a cog is also inheriting from app_commands.Group then it'll also
229229
# add the hybrid commands as text commands, which would recursively add the
230230
# hybrid commands as slash commands. This check just terminates that recursion
231231
# from happening
232232
if command.cog is None or not command.cog.__cog_is_app_commands_group__:
233-
self.tree.add_command(command.app_command) # type: ignore
233+
self.tree.add_command(command.app_command)
234234

235235
@discord.utils.copy_doc(GroupMixin.remove_command)
236236
def remove_command(self, name: str, /) -> Optional[Command[Any, ..., Any]]:
237-
cmd = super().remove_command(name)
238-
if cmd is not None and hasattr(cmd, '__commands_is_hybrid__'):
237+
cmd: Optional[Command[Any, ..., Any]] = super().remove_command(name)
238+
if isinstance(cmd, (HybridCommand, HybridGroup)) and cmd.app_command:
239239
# See above
240240
if cmd.cog is not None and cmd.cog.__cog_is_app_commands_group__:
241241
return cmd
242242

243-
guild_ids: Optional[List[int]] = cmd.app_command._guild_ids # type: ignore
243+
guild_ids: Optional[List[int]] = cmd.app_command._guild_ids
244244
if guild_ids is None:
245245
self.__tree.remove_command(name)
246246
else:
@@ -252,6 +252,7 @@ def remove_command(self, name: str, /) -> Optional[Command[Any, ..., Any]]:
252252
def hybrid_command(
253253
self,
254254
name: str = MISSING,
255+
with_app_command: bool = True,
255256
*args: Any,
256257
**kwargs: Any,
257258
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridCommand[Any, P, T]]:
@@ -266,7 +267,7 @@ def hybrid_command(
266267

267268
def decorator(func: CommandCallback[Any, ContextT, P, T]):
268269
kwargs.setdefault('parent', self)
269-
result = hybrid_command(name=name, *args, **kwargs)(func)
270+
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
270271
self.add_command(result)
271272
return result
272273

@@ -275,6 +276,7 @@ def decorator(func: CommandCallback[Any, ContextT, P, T]):
275276
def hybrid_group(
276277
self,
277278
name: str = MISSING,
279+
with_app_command: bool = True,
278280
*args: Any,
279281
**kwargs: Any,
280282
) -> Callable[[CommandCallback[Any, ContextT, P, T]], HybridGroup[Any, P, T]]:
@@ -289,7 +291,7 @@ def hybrid_group(
289291

290292
def decorator(func: CommandCallback[Any, ContextT, P, T]):
291293
kwargs.setdefault('parent', self)
292-
result = hybrid_group(name=name, *args, **kwargs)(func)
294+
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
293295
self.add_command(result)
294296
return result
295297

discord/ext/commands/cog.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,15 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self:
291291
parent.add_command(command) # type: ignore
292292
elif self.__cog_app_commands_group__:
293293
if hasattr(command, '__commands_is_hybrid__') and command.parent is None:
294-
# In both of these, the type checker does not see the app_command attribute even though it exists
295294
parent = self.__cog_app_commands_group__
296-
command.app_command = command.app_command._copy_with(parent=parent, binding=self) # type: ignore
297-
children.append(command.app_command) # type: ignore
295+
app_command: Optional[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = getattr(
296+
command, 'app_command', None
297+
)
298+
if app_command:
299+
app_command = app_command._copy_with(parent=parent, binding=self)
300+
children.append(app_command)
301+
# The type checker does not see the app_command attribute even though it exists
302+
command.app_command = app_command # type: ignore
298303

299304
for command in cls.__cog_app_commands__:
300305
copy = command._copy_with(parent=self.__cog_app_commands_group__, binding=self)

discord/ext/commands/hybrid.py

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,15 @@ def __init__(
386386
**kwargs: Any,
387387
) -> None:
388388
super().__init__(func, **kwargs)
389-
self.app_command: HybridAppCommand[CogT, Any, T] = HybridAppCommand(self)
389+
self.with_app_command: bool = kwargs.pop('with_app_command', True)
390+
self.with_command: bool = kwargs.pop('with_command', True)
391+
392+
if not self.with_command and not self.with_app_command:
393+
raise TypeError('cannot set both with_command and with_app_command to False')
394+
395+
self.app_command: Optional[HybridAppCommand[CogT, Any, T]] = (
396+
HybridAppCommand(self) if self.with_app_command else None
397+
)
390398

391399
@property
392400
def cog(self) -> CogT:
@@ -395,25 +403,29 @@ def cog(self) -> CogT:
395403
@cog.setter
396404
def cog(self, value: CogT) -> None:
397405
self._cog = value
398-
self.app_command.binding = value
406+
if self.app_command is not None:
407+
self.app_command.binding = value
399408

400409
async def can_run(self, ctx: Context[BotT], /) -> bool:
401-
if ctx.interaction is None:
402-
return await super().can_run(ctx)
403-
else:
410+
if ctx.interaction is not None and self.app_command:
404411
return await self.app_command._check_can_run(ctx.interaction)
412+
else:
413+
return await super().can_run(ctx)
405414

406415
async def _parse_arguments(self, ctx: Context[BotT]) -> None:
407416
interaction = ctx.interaction
408417
if interaction is None:
409418
return await super()._parse_arguments(ctx)
410-
else:
419+
elif self.app_command:
411420
ctx.kwargs = await self.app_command._transform_arguments(interaction, interaction.namespace)
412421

413422
def _ensure_assignment_on_copy(self, other: Self) -> Self:
414423
copy = super()._ensure_assignment_on_copy(other)
415-
copy.app_command = self.app_command.copy()
416-
copy.app_command.wrapped = copy
424+
if self.app_command is None:
425+
copy.app_command = None
426+
else:
427+
copy.app_command = self.app_command.copy()
428+
copy.app_command.wrapped = copy
417429
return copy
418430

419431
def autocomplete(
@@ -441,6 +453,9 @@ def autocomplete(
441453
The coroutine passed is not actually a coroutine or
442454
the parameter is not found or of an invalid type.
443455
"""
456+
if self.app_command is None:
457+
raise TypeError('This command does not have a registered application command')
458+
444459
return self.app_command.autocomplete(name)
445460

446461

@@ -473,35 +488,47 @@ class HybridGroup(Group[CogT, P, T]):
473488
def __init__(self, *args: Any, fallback: Optional[str] = None, **attrs: Any) -> None:
474489
super().__init__(*args, **attrs)
475490
self.invoke_without_command = True
491+
self.with_app_command: bool = attrs.pop('with_app_command', True)
492+
476493
parent = None
477494
if self.parent is not None:
478495
if isinstance(self.parent, HybridGroup):
479496
parent = self.parent.app_command
480497
else:
481498
raise TypeError(f'HybridGroup parent must be HybridGroup not {self.parent.__class__}')
482499

483-
guild_ids = attrs.pop('guild_ids', None) or getattr(self.callback, '__discord_app_commands_default_guilds__', None)
484-
guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False)
485-
default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None)
486-
self.app_command: app_commands.Group = app_commands.Group(
487-
name=self.name,
488-
description=self.description or self.short_doc or '…',
489-
guild_ids=guild_ids,
490-
guild_only=guild_only,
491-
default_permissions=default_permissions,
492-
)
493-
494-
# This prevents the group from re-adding the command at __init__
495-
self.app_command.parent = parent
500+
# I would love for this to be Optional[app_commands.Group]
501+
# However, Python does not have conditional typing so it's very hard to
502+
# make this type depend on the with_app_command bool without a lot of needless repetition
503+
self.app_command: app_commands.Group = MISSING
496504
self.fallback: Optional[str] = fallback
497505

498-
if fallback is not None:
499-
command = HybridAppCommand(self)
500-
command.name = fallback
501-
self.app_command.add_command(command)
506+
if self.with_app_command:
507+
guild_ids = attrs.pop('guild_ids', None) or getattr(
508+
self.callback, '__discord_app_commands_default_guilds__', None
509+
)
510+
guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False)
511+
default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None)
512+
self.app_command = app_commands.Group(
513+
name=self.name,
514+
description=self.description or self.short_doc or '…',
515+
guild_ids=guild_ids,
516+
guild_only=guild_only,
517+
default_permissions=default_permissions,
518+
)
519+
520+
# This prevents the group from re-adding the command at __init__
521+
self.app_command.parent = parent
522+
523+
if fallback is not None:
524+
command = HybridAppCommand(self)
525+
command.name = fallback
526+
self.app_command.add_command(command)
502527

503528
@property
504529
def _fallback_command(self) -> Optional[HybridAppCommand[CogT, ..., T]]:
530+
if self.app_command is MISSING:
531+
return None
505532
return self.app_command.get_command(self.fallback) # type: ignore
506533

507534
@property
@@ -596,7 +623,9 @@ def add_command(self, command: Union[HybridGroup[CogT, ..., Any], HybridCommand[
596623
if isinstance(command, HybridGroup) and self.parent is not None:
597624
raise ValueError(f'{command.qualified_name!r} is too nested, groups can only be nested at most one level')
598625

599-
self.app_command.add_command(command.app_command)
626+
if command.app_command and self.app_command:
627+
self.app_command.add_command(command.app_command)
628+
600629
command.parent = self
601630

602631
if command.name in self.all_commands:
@@ -611,13 +640,15 @@ def add_command(self, command: Union[HybridGroup[CogT, ..., Any], HybridCommand[
611640

612641
def remove_command(self, name: str, /) -> Optional[Command[CogT, ..., Any]]:
613642
cmd = super().remove_command(name)
614-
self.app_command.remove_command(name)
643+
if self.app_command:
644+
self.app_command.remove_command(name)
615645
return cmd
616646

617647
def command(
618648
self,
619649
name: str = MISSING,
620650
*args: Any,
651+
with_app_command: bool = True,
621652
**kwargs: Any,
622653
) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridCommand[CogT, P2, U]]:
623654
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_command` and adds it to
@@ -631,7 +662,7 @@ def command(
631662

632663
def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
633664
kwargs.setdefault('parent', self)
634-
result = hybrid_command(name=name, *args, **kwargs)(func)
665+
result = hybrid_command(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
635666
self.add_command(result)
636667
return result
637668

@@ -641,6 +672,7 @@ def group(
641672
self,
642673
name: str = MISSING,
643674
*args: Any,
675+
with_app_command: bool = True,
644676
**kwargs: Any,
645677
) -> Callable[[CommandCallback[CogT, ContextT, P2, U]], HybridGroup[CogT, P2, U]]:
646678
"""A shortcut decorator that invokes :func:`~discord.ext.commands.hybrid_group` and adds it to
@@ -654,7 +686,7 @@ def group(
654686

655687
def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
656688
kwargs.setdefault('parent', self)
657-
result = hybrid_group(name=name, *args, **kwargs)(func)
689+
result = hybrid_group(name=name, *args, with_app_command=with_app_command, **kwargs)(func)
658690
self.add_command(result)
659691
return result
660692

@@ -663,9 +695,11 @@ def decorator(func: CommandCallback[CogT, ContextT, P2, U]):
663695

664696
def hybrid_command(
665697
name: str = MISSING,
698+
*,
699+
with_app_command: bool = True,
666700
**attrs: Any,
667701
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridCommand[CogT, P, T]]:
668-
"""A decorator that transforms a function into a :class:`.HybridCommand`.
702+
r"""A decorator that transforms a function into a :class:`.HybridCommand`.
669703
670704
A hybrid command is one that functions both as a regular :class:`.Command`
671705
and one that is also a :class:`app_commands.Command <discord.app_commands.Command>`.
@@ -690,7 +724,9 @@ def hybrid_command(
690724
name: :class:`str`
691725
The name to create the command with. By default this uses the
692726
function name unchanged.
693-
attrs
727+
with_app_command: :class:`bool`
728+
Whether to register the command as an application command.
729+
\*\*attrs
694730
Keyword arguments to pass into the construction of the
695731
hybrid command.
696732
@@ -703,24 +739,36 @@ def hybrid_command(
703739
def decorator(func: CommandCallback[CogT, ContextT, P, T]):
704740
if isinstance(func, Command):
705741
raise TypeError('Callback is already a command.')
706-
return HybridCommand(func, name=name, **attrs)
742+
return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs)
707743

708744
return decorator
709745

710746

711747
def hybrid_group(
712748
name: str = MISSING,
749+
*,
750+
with_app_command: bool = True,
713751
**attrs: Any,
714752
) -> Callable[[CommandCallback[CogT, ContextT, P, T]], HybridGroup[CogT, P, T]]:
715753
"""A decorator that transforms a function into a :class:`.HybridGroup`.
716754
717755
This is similar to the :func:`~discord.ext.commands.group` decorator except it creates
718756
a hybrid group instead.
757+
758+
Parameters
759+
-----------
760+
with_app_command: :class:`bool`
761+
Whether to register the command as an application command.
762+
763+
Raises
764+
-------
765+
TypeError
766+
If the function is not a coroutine or is already a command.
719767
"""
720768

721769
def decorator(func: CommandCallback[CogT, ContextT, P, T]):
722770
if isinstance(func, Command):
723771
raise TypeError('Callback is already a command.')
724-
return HybridGroup(func, name=name, **attrs)
772+
return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs)
725773

726774
return decorator # type: ignore

0 commit comments

Comments
 (0)