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.hybrid_command
.. autofunction:: redbot.core.commands.group
.. autofunction:: redbot.core.commands.hybrid_group
.. autoclass:: redbot.core.commands.Cog
.. automethod:: format_help_for_context
@ -21,13 +25,21 @@ extend functionalities used throughout the bot, as outlined below.
.. automethod:: red_delete_data_for_user
.. autoclass:: redbot.core.commands.GroupCog
.. autoclass:: redbot.core.commands.Command
:members:
:inherited-members: format_help_for_context
.. autoclass:: redbot.core.commands.HybridCommand
:members:
.. autoclass:: redbot.core.commands.Group
:members:
.. autoclass:: redbot.core.commands.HybridGroup
:members:
.. autoclass:: redbot.core.commands.Context
:members:

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import asyncio
import inspect
import logging
@ -29,6 +30,7 @@ from typing import (
MutableMapping,
Set,
overload,
TYPE_CHECKING,
)
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._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"
COMMAND_SCOPE = "COMMAND"
SHARED_API_TOKENS = "SHARED_API_TOKENS"
@ -1796,6 +1805,58 @@ class Red(
subcommand.requires.reset()
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:
"""Clear all permission overrides in a scope.

View File

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

View File

@ -14,11 +14,13 @@ from typing import (
Any,
Awaitable,
Callable,
ClassVar,
Dict,
List,
Literal,
Optional,
Tuple,
TypeVar,
Union,
MutableMapping,
TYPE_CHECKING,
@ -33,6 +35,9 @@ from discord.ext.commands import (
DisabledCommand,
command as dpy_command_deco,
Command as DPYCommand,
GroupCog as DPYGroupCog,
HybridCommand as DPYHybridCommand,
HybridGroup as DPYHybridGroup,
Cog as DPYCog,
CogMeta as DPYCogMeta,
Group as DPYGroup,
@ -43,9 +48,24 @@ from .errors import ConversionFailure
from .requires import PermState, PrivilegeLevel, Requires, PermStateAllowedStates
from ..i18n import Translator
_T = TypeVar("_T")
_CogT = TypeVar("_CogT", bound="Cog")
if TYPE_CHECKING:
# circular import avoidance
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__ = [
@ -55,9 +75,12 @@ __all__ = [
"CogGroupMixin",
"Command",
"Group",
"GroupCog",
"GroupMixin",
"command",
"group",
"hybrid_command",
"hybrid_group",
"RESERVED_COMMAND_NAMES",
"RedUnhandledAPI",
]
@ -287,9 +310,9 @@ class Command(CogCommandMixin, DPYCommand):
def __init__(self, *args, **kwargs):
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.translator = kwargs.pop("i18n", None)
super().__init__(*args, **kwargs)
if self.parent is None:
for name in (self.name, *self.aliases):
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__}
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):
"""A decorator which transforms an async function into a `Command`.