Add support for Hybrid commands in Red (#5681)

Co-authored-by: Kowlin <10947836+Kowlin@users.noreply.github.com>
Co-authored-by: aikaterna <20862007+aikaterna@users.noreply.github.com>
Co-authored-by: Jakub Kuczys <6032823+jack1142@users.noreply.github.com>
Co-authored-by: Kreusada <67752638+Kreusada@users.noreply.github.com>
Co-authored-by: Candy <28566705+mina9999@users.noreply.github.com>
Co-authored-by: Matt Chandra <55866950+matcha19@users.noreply.github.com>
Co-authored-by: Lemon Rose <78662983+japandotorg@users.noreply.github.com>
Co-authored-by: Honkertonken <94032937+Honkertonken@users.noreply.github.com>
Co-authored-by: Flame442 <34169552+Flame442@users.noreply.github.com>
Co-authored-by: River <18037011+RheingoldRiver@users.noreply.github.com>
Co-authored-by: AAA3A <89632044+AAA3A-AAA3A@users.noreply.github.com>
Co-authored-by: Lemon Rose <japandotorg@users.noreply.github.com>
Co-authored-by: Julien Mauroy <pro.julien.mauroy@gmail.com>
Co-authored-by: TheThomanski <15034759+TheThomanski@users.noreply.github.com>
This commit is contained in:
TrustyJAID 2022-10-11 14:52:43 -06:00 committed by GitHub
parent 7ff89302b2
commit f8b0cc6c6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 228 additions and 1 deletions

View File

@ -11,8 +11,12 @@ extend functionalities used throughout the bot, as outlined below.
.. autofunction:: redbot.core.commands.command .. autofunction:: redbot.core.commands.command
.. autofunction:: redbot.core.commands.hybrid_command
.. autofunction:: redbot.core.commands.group .. autofunction:: redbot.core.commands.group
.. autofunction:: redbot.core.commands.hybrid_group
.. autoclass:: redbot.core.commands.Cog .. autoclass:: redbot.core.commands.Cog
.. automethod:: format_help_for_context .. automethod:: format_help_for_context
@ -21,13 +25,21 @@ extend functionalities used throughout the bot, as outlined below.
.. automethod:: red_delete_data_for_user .. automethod:: red_delete_data_for_user
.. autoclass:: redbot.core.commands.GroupCog
.. autoclass:: redbot.core.commands.Command .. autoclass:: redbot.core.commands.Command
:members: :members:
:inherited-members: format_help_for_context :inherited-members: format_help_for_context
.. autoclass:: redbot.core.commands.HybridCommand
:members:
.. autoclass:: redbot.core.commands.Group .. autoclass:: redbot.core.commands.Group
:members: :members:
.. autoclass:: redbot.core.commands.HybridGroup
:members:
.. autoclass:: redbot.core.commands.Context .. autoclass:: redbot.core.commands.Context
:members: :members:

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import asyncio import asyncio
import inspect import inspect
import logging import logging
@ -29,6 +30,7 @@ from typing import (
MutableMapping, MutableMapping,
Set, Set,
overload, overload,
TYPE_CHECKING,
) )
from types import MappingProxyType from types import MappingProxyType
@ -54,6 +56,13 @@ from .rpc import RPCMixin
from .utils import can_user_send_messages_in, common_filters, AsyncIter from .utils import can_user_send_messages_in, common_filters, AsyncIter
from .utils._internal_utils import send_to_owners_with_prefix_replaced from .utils._internal_utils import send_to_owners_with_prefix_replaced
if TYPE_CHECKING:
from discord.ext.commands.hybrid import CommandCallback, ContextT, P
from discord import app_commands
_T = TypeVar("_T")
CUSTOM_GROUPS = "CUSTOM_GROUPS" CUSTOM_GROUPS = "CUSTOM_GROUPS"
COMMAND_SCOPE = "COMMAND" COMMAND_SCOPE = "COMMAND"
SHARED_API_TOKENS = "SHARED_API_TOKENS" SHARED_API_TOKENS = "SHARED_API_TOKENS"
@ -1796,6 +1805,58 @@ class Red(
subcommand.requires.reset() subcommand.requires.reset()
return command return command
def hybrid_command(
self,
name: Union[str, app_commands.locale_str] = discord.utils.MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
) -> Callable[[CommandCallback[Any, ContextT, P, _T]], commands.HybridCommand[Any, P, _T]]:
"""A shortcut decorator that invokes :func:`~redbot.core.commands.hybrid_command` and adds it to
the internal command list via :meth:`add_command`.
Returns
--------
Callable[..., :class:`HybridCommand`]
A decorator that converts the provided method into a Command, adds it to the bot, then returns it.
"""
def decorator(func: CommandCallback[Any, ContextT, P, _T]):
kwargs.setdefault("parent", self)
result = commands.hybrid_command(
name=name, *args, with_app_command=with_app_command, **kwargs
)(func)
self.add_command(result)
return result
return decorator
def hybrid_group(
self,
name: Union[str, app_commands.locale_str] = discord.utils.MISSING,
with_app_command: bool = True,
*args: Any,
**kwargs: Any,
) -> Callable[[CommandCallback[Any, ContextT, P, _T]], commands.HybridGroup[Any, P, _T]]:
"""A shortcut decorator that invokes :func:`~redbot.core.commands.hybrid_group` and adds it to
the internal command list via :meth:`add_command`.
Returns
--------
Callable[..., :class:`HybridGroup`]
A decorator that converts the provided method into a Group, adds it to the bot, then returns it.
"""
def decorator(func: CommandCallback[Any, ContextT, P, _T]):
kwargs.setdefault("parent", self)
result = commands.hybrid_group(
name=name, *args, with_app_command=with_app_command, **kwargs
)(func)
self.add_command(result)
return result
return decorator
def clear_permission_rules(self, guild_id: Optional[int], **kwargs) -> None: def clear_permission_rules(self, guild_id: Optional[int], **kwargs) -> None:
"""Clear all permission overrides in a scope. """Clear all permission overrides in a scope.

View File

@ -12,8 +12,13 @@ from .commands import (
CogGroupMixin as CogGroupMixin, CogGroupMixin as CogGroupMixin,
Command as Command, Command as Command,
Group as Group, Group as Group,
GroupCog as GroupCog,
GroupMixin as GroupMixin, GroupMixin as GroupMixin,
command as command, command as command,
HybridCommand as HybridCommand,
HybridGroup as HybridGroup,
hybrid_command as hybrid_command,
hybrid_group as hybrid_group,
group as group, group as group,
RedUnhandledAPI as RedUnhandledAPI, RedUnhandledAPI as RedUnhandledAPI,
RESERVED_COMMAND_NAMES as RESERVED_COMMAND_NAMES, RESERVED_COMMAND_NAMES as RESERVED_COMMAND_NAMES,
@ -198,4 +203,5 @@ from discord.ext.commands import (
Range as Range, Range as Range,
RangeError as RangeError, RangeError as RangeError,
parameter as parameter, parameter as parameter,
HybridCommandError as HybridCommandError,
) )

View File

@ -14,11 +14,13 @@ from typing import (
Any, Any,
Awaitable, Awaitable,
Callable, Callable,
ClassVar,
Dict, Dict,
List, List,
Literal, Literal,
Optional, Optional,
Tuple, Tuple,
TypeVar,
Union, Union,
MutableMapping, MutableMapping,
TYPE_CHECKING, TYPE_CHECKING,
@ -33,6 +35,9 @@ from discord.ext.commands import (
DisabledCommand, DisabledCommand,
command as dpy_command_deco, command as dpy_command_deco,
Command as DPYCommand, Command as DPYCommand,
GroupCog as DPYGroupCog,
HybridCommand as DPYHybridCommand,
HybridGroup as DPYHybridGroup,
Cog as DPYCog, Cog as DPYCog,
CogMeta as DPYCogMeta, CogMeta as DPYCogMeta,
Group as DPYGroup, Group as DPYGroup,
@ -43,9 +48,24 @@ from .errors import ConversionFailure
from .requires import PermState, PrivilegeLevel, Requires, PermStateAllowedStates from .requires import PermState, PrivilegeLevel, Requires, PermStateAllowedStates
from ..i18n import Translator from ..i18n import Translator
_T = TypeVar("_T")
_CogT = TypeVar("_CogT", bound="Cog")
if TYPE_CHECKING: if TYPE_CHECKING:
# circular import avoidance # circular import avoidance
from .context import Context from .context import Context
from typing_extensions import ParamSpec, Concatenate
from discord.ext.commands._types import ContextT, Coro
_P = ParamSpec("_P")
CommandCallback = Union[
Callable[Concatenate[_CogT, ContextT, _P], Coro[_T]],
Callable[Concatenate[ContextT, _P], Coro[_T]],
]
else:
_P = TypeVar("_P")
__all__ = [ __all__ = [
@ -55,9 +75,12 @@ __all__ = [
"CogGroupMixin", "CogGroupMixin",
"Command", "Command",
"Group", "Group",
"GroupCog",
"GroupMixin", "GroupMixin",
"command", "command",
"group", "group",
"hybrid_command",
"hybrid_group",
"RESERVED_COMMAND_NAMES", "RESERVED_COMMAND_NAMES",
"RedUnhandledAPI", "RedUnhandledAPI",
] ]
@ -287,9 +310,9 @@ class Command(CogCommandMixin, DPYCommand):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.ignore_optional_for_conversion = kwargs.pop("ignore_optional_for_conversion", False) self.ignore_optional_for_conversion = kwargs.pop("ignore_optional_for_conversion", False)
super().__init__(*args, **kwargs)
self._help_override = kwargs.pop("help_override", None) self._help_override = kwargs.pop("help_override", None)
self.translator = kwargs.pop("i18n", None) self.translator = kwargs.pop("i18n", None)
super().__init__(*args, **kwargs)
if self.parent is None: if self.parent is None:
for name in (self.name, *self.aliases): for name in (self.name, *self.aliases):
if name in RESERVED_COMMAND_NAMES: if name in RESERVED_COMMAND_NAMES:
@ -984,6 +1007,131 @@ class Cog(CogMixin, DPYCog, metaclass=DPYCogMeta):
return {cmd.name: cmd for cmd in self.__cog_commands__} return {cmd.name: cmd for cmd in self.__cog_commands__}
class GroupCog(Cog, DPYGroupCog):
"""
Red's Cog base class with app commands group as the base.
This class inherits from `Cog` and `discord.ext.commands.GroupCog`
"""
class HybridCommand(Command, DPYHybridCommand[_CogT, _P, _T]):
"""HybridCommand class for Red.
This should not be created directly, and instead via the decorator.
This class inherits from `Command` and `discord.ext.commands.HybridCommand`.
.. warning::
This class is not intended to be subclassed.
"""
class HybridGroup(Group, DPYHybridGroup[_CogT, _P, _T]):
"""HybridGroup command class for Red.
This should not be created directly, and instead via the decorator.
This class inherits from `Group` and `discord.ext.commands.HybridGroup`.
.. note::
Red's HybridGroups differ from `discord.ext.commands.HybridGroup`
by setting `discord.ext.commands.Group.invoke_without_command` to be `False` by default.
If `discord.ext.commands.HybridGroup.fallback` is provided then
`discord.ext.commands.Group.invoke_without_command` is
set to `True`.
.. warning::
This class is not intended to be subclassed.
"""
def __init__(self, *args: Any, **kwargs: Any):
fallback = "fallback" in kwargs and kwargs["fallback"] is not None
invoke_without_command = kwargs.pop("invoke_without_command", False) or fallback
kwargs["invoke_without_command"] = invoke_without_command
super().__init__(*args, **kwargs)
self.invoke_without_command = invoke_without_command
@property
def callback(self):
return self._callback
@callback.setter
def callback(self, function):
# Below should be mostly the same as discord.py
super(__class__, __class__).callback.__set__(self, function)
if not self.invoke_without_command and self.params:
raise TypeError(
"You cannot have a group command with callbacks and `invoke_without_command` set to False."
)
def command(self, name: str = discord.utils.MISSING, *args: Any, **kwargs: Any):
def decorator(func):
kwargs.setdefault("parent", self)
result = hybrid_command(name=name, *args, **kwargs)(func)
self.add_command(result)
return result
return decorator
def group(
self,
name: str = discord.utils.MISSING,
*args: Any,
**kwargs: Any,
):
def decorator(func):
kwargs.setdefault("parent", self)
result = hybrid_group(name=name, *args, **kwargs)(func)
self.add_command(result)
return result
return decorator
def hybrid_command(
name: Union[str, discord.app_commands.locale_str] = discord.utils.MISSING,
*,
with_app_command: bool = True,
**attrs: Any,
) -> Callable[[CommandCallback[_CogT, ContextT, _P, _T]], HybridCommand[_CogT, _P, _T]]:
"""A decorator which transforms an async function into a `HybridCommand`.
Same interface as `discord.ext.commands.hybrid_command`.
"""
def decorator(func: CommandCallback[_CogT, ContextT, _P, _T]) -> HybridCommand[_CogT, _P, _T]:
if isinstance(func, Command):
raise TypeError("callback is already a command.")
attrs["help_override"] = attrs.pop("help", None)
return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs)
return decorator
def hybrid_group(
name: Union[str, discord.app_commands.locale_str] = discord.utils.MISSING,
*,
with_app_command: bool = True,
**attrs: Any,
) -> Callable[[CommandCallback[_CogT, ContextT, _P, _T]], HybridGroup[_CogT, _P, _T]]:
"""A decorator which transforms an async function into a `HybridGroup`.
Same interface as `discord.ext.commands.hybrid_group`.
"""
def decorator(func: CommandCallback[_CogT, ContextT, _P, _T]):
if isinstance(func, Command):
raise TypeError("callback is already a command.")
attrs["help_override"] = attrs.pop("help", None)
return HybridGroup(func, name=name, with_app_command=with_app_command, **attrs)
return decorator
def command(name=None, cls=Command, **attrs): def command(name=None, cls=Command, **attrs):
"""A decorator which transforms an async function into a `Command`. """A decorator which transforms an async function into a `Command`.