mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-06-07 20:28:38 +00:00
Add support for error handlers
This commit is contained in:
parent
cdb7b3728e
commit
c10ed93cef
@ -51,7 +51,7 @@ from .enums import AppCommandOptionType, AppCommandType
|
||||
from ..interactions import Interaction
|
||||
from ..enums import ChannelType, try_enum
|
||||
from .models import AppCommandChannel, AppCommandThread, Choice
|
||||
from .errors import CommandSignatureMismatch, CommandAlreadyRegistered
|
||||
from .errors import AppCommandError, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
|
||||
from ..utils import resolve_annotation, MISSING, is_inside_class
|
||||
from ..user import User
|
||||
from ..member import Member
|
||||
@ -62,7 +62,6 @@ from ..permissions import Permissions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec, Concatenate
|
||||
from ..interactions import Interaction
|
||||
from ..types.interactions import (
|
||||
ResolvedData,
|
||||
PartialThread,
|
||||
@ -89,6 +88,10 @@ else:
|
||||
T = TypeVar('T')
|
||||
GroupT = TypeVar('GroupT', bound='Group')
|
||||
Coro = Coroutine[Any, Any, T]
|
||||
Error = Union[
|
||||
Callable[[GroupT, Interaction, AppCommandError], Coro[Any]],
|
||||
Callable[[Interaction, AppCommandError], Coro[Any]],
|
||||
]
|
||||
|
||||
ContextMenuCallback = Union[
|
||||
# If groups end up support context menus these would be uncommented
|
||||
@ -444,6 +447,7 @@ class Command(Generic[GroupT, P, T]):
|
||||
self._callback: CommandCallback[GroupT, P, T] = callback
|
||||
self.parent: Optional[Group] = parent
|
||||
self.binding: Optional[GroupT] = None
|
||||
self.on_error: Optional[Error[GroupT]] = None
|
||||
self._params: Dict[str, CommandParameter] = _extract_parameters_from_callback(callback, callback.__globals__)
|
||||
|
||||
def _copy_with_binding(self, binding: GroupT) -> Command:
|
||||
@ -453,6 +457,7 @@ class Command(Generic[GroupT, P, T]):
|
||||
copy.description = self.description
|
||||
copy._callback = self._callback
|
||||
copy.parent = self.parent
|
||||
copy.on_error = self.on_error
|
||||
copy._params = self._params.copy()
|
||||
copy.binding = binding
|
||||
return copy
|
||||
@ -468,6 +473,21 @@ class Command(Generic[GroupT, P, T]):
|
||||
'options': [param.to_dict() for param in self._params.values()],
|
||||
}
|
||||
|
||||
async def _invoke_error_handler(self, interaction: Interaction, error: AppCommandError) -> None:
|
||||
# These type ignores are because the type checker can't narrow this type properly.
|
||||
if self.on_error is not None:
|
||||
if self.binding is not None:
|
||||
await self.on_error(self.binding, interaction, error) # type: ignore
|
||||
else:
|
||||
await self.on_error(interaction, error) # type: ignore
|
||||
|
||||
parent = self.parent
|
||||
if parent is not None:
|
||||
await parent.on_error(interaction, self, error)
|
||||
|
||||
if parent.parent is not None:
|
||||
await parent.parent.on_error(interaction, self, error)
|
||||
|
||||
async def _invoke_with_namespace(self, interaction: Interaction, namespace: Namespace) -> T:
|
||||
defaults = ((name, param.default) for name, param in self._params.items() if not param.required)
|
||||
namespace._update_with_defaults(defaults)
|
||||
@ -477,7 +497,7 @@ class Command(Generic[GroupT, P, T]):
|
||||
if self.binding is not None:
|
||||
return await self._callback(self.binding, interaction, **namespace.__dict__) # type: ignore
|
||||
return await self._callback(interaction, **namespace.__dict__) # type: ignore
|
||||
except TypeError:
|
||||
except TypeError as e:
|
||||
# In order to detect mismatch from the provided signature and the Discord data,
|
||||
# there are many ways it can go wrong yet all of them eventually lead to a TypeError
|
||||
# from the Python compiler showcasing that the signature is incorrect. This lovely
|
||||
@ -490,7 +510,11 @@ class Command(Generic[GroupT, P, T]):
|
||||
frame = inspect.trace()[-1].frame
|
||||
if frame.f_locals.get('self') is self:
|
||||
raise CommandSignatureMismatch(self) from None
|
||||
raise CommandInvokeError(self, e) from e
|
||||
except AppCommandError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise CommandInvokeError(self, e) from e
|
||||
|
||||
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]:
|
||||
return None
|
||||
@ -503,6 +527,32 @@ class Command(Generic[GroupT, P, T]):
|
||||
parent = self.parent
|
||||
return parent.parent or parent
|
||||
|
||||
def error(self, coro: Error[GroupT]) -> Error[GroupT]:
|
||||
"""A decorator that registers a coroutine as a local error handler.
|
||||
|
||||
The local error handler is called whenever an exception is raised in the body
|
||||
of the command or during handling of the command. The error handler must take
|
||||
2 parameters, the interaction and the error.
|
||||
|
||||
The error passed will be derived from :exc:`AppCommandError`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
coro: :ref:`coroutine <coroutine>`
|
||||
The coroutine to register as the local error handler.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The coroutine passed is not actually a coroutine.
|
||||
"""
|
||||
|
||||
if not inspect.iscoroutinefunction(coro):
|
||||
raise TypeError('The error handler must be a coroutine.')
|
||||
|
||||
self.on_error = coro
|
||||
return coro
|
||||
|
||||
|
||||
class ContextMenu:
|
||||
"""A class that implements a context menu application command.
|
||||
@ -560,7 +610,12 @@ class ContextMenu:
|
||||
}
|
||||
|
||||
async def _invoke(self, interaction: Interaction, arg: Any):
|
||||
try:
|
||||
await self._callback(interaction, arg)
|
||||
except AppCommandError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise CommandInvokeError(self, e) from e
|
||||
|
||||
|
||||
class Group:
|
||||
@ -668,6 +723,25 @@ class Group:
|
||||
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]:
|
||||
return self._children.get(name)
|
||||
|
||||
async def on_error(self, interaction: Interaction, command: Command, error: AppCommandError) -> None:
|
||||
"""|coro|
|
||||
|
||||
A callback that is called when a child's command raises an :exc:`AppCommandError`.
|
||||
|
||||
The default implementation does nothing.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`~discord.Interaction`
|
||||
The interaction that is being handled.
|
||||
command: :class`~discord.app_commands.Command`
|
||||
The command that failed.
|
||||
error: :exc:`AppCommandError`
|
||||
The exception that was raised.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def add_command(self, command: Union[Command, Group], /, *, override: bool = False):
|
||||
"""Adds a command or group to this group's internal list of commands.
|
||||
|
||||
|
@ -30,6 +30,8 @@ from .enums import AppCommandType
|
||||
from ..errors import DiscordException
|
||||
|
||||
__all__ = (
|
||||
'AppCommandError',
|
||||
'CommandInvokeError',
|
||||
'CommandAlreadyRegistered',
|
||||
'CommandSignatureMismatch',
|
||||
'CommandNotFound',
|
||||
@ -39,9 +41,54 @@ if TYPE_CHECKING:
|
||||
from .commands import Command, Group, ContextMenu
|
||||
|
||||
|
||||
class CommandAlreadyRegistered(DiscordException):
|
||||
class AppCommandError(DiscordException):
|
||||
"""The base exception type for all application command related errors.
|
||||
|
||||
This inherits from :exc:`discord.DiscordException`.
|
||||
|
||||
This exception and exceptions inherited from it are handled
|
||||
in a special way as they are caught and passed into various error handlers
|
||||
in this order:
|
||||
|
||||
- :meth:`Command.error <discord.app_commands.Command.error>`
|
||||
- :meth:`Group.on_error <discord.app_commands.Group.on_error>`
|
||||
- :meth:`CommandTree.on_error <discord.app_commands.CommandTree.on_error>`
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommandInvokeError(AppCommandError):
|
||||
"""An exception raised when the command being invoked raised an exception.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
command: Union[:class:`Command`, :class:`ContextMenu`]
|
||||
The command that failed.
|
||||
"""
|
||||
|
||||
def __init__(self, command: Union[Command, ContextMenu], e: Exception) -> None:
|
||||
self.original: Exception = e
|
||||
self.command: Union[Command, ContextMenu] = command
|
||||
super().__init__(f'Command {command.name!r} raised an exception: {e.__class__.__name__}: {e}')
|
||||
|
||||
|
||||
class CommandAlreadyRegistered(AppCommandError):
|
||||
"""An exception raised when a command is already registered.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -57,9 +104,13 @@ class CommandAlreadyRegistered(DiscordException):
|
||||
super().__init__(f'Command {name!r} already registered.')
|
||||
|
||||
|
||||
class CommandNotFound(DiscordException):
|
||||
class CommandNotFound(AppCommandError):
|
||||
"""An exception raised when an application command could not be found.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
name: :class:`str`
|
||||
@ -78,12 +129,16 @@ class CommandNotFound(DiscordException):
|
||||
super().__init__(f'Application command {name!r} not found')
|
||||
|
||||
|
||||
class CommandSignatureMismatch(DiscordException):
|
||||
class CommandSignatureMismatch(AppCommandError):
|
||||
"""An exception raised when an application command from Discord has a different signature
|
||||
from the one provided in the code. This happens because your command definition differs
|
||||
from the command definition you provided Discord. Either your code is out of date or the
|
||||
data from Discord is out of sync.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
command: Union[:class:`~.app_commands.Command`, :class:`~.app_commands.ContextMenu`, :class:`~.app_commands.Group`]
|
||||
|
@ -24,6 +24,8 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
import inspect
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Callable, Dict, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, Union, overload
|
||||
|
||||
|
||||
@ -31,7 +33,12 @@ from .namespace import Namespace, ResolveKey
|
||||
from .models import AppCommand
|
||||
from .commands import Command, ContextMenu, Group, _shorten
|
||||
from .enums import AppCommandType
|
||||
from .errors import CommandAlreadyRegistered, CommandNotFound, CommandSignatureMismatch
|
||||
from .errors import (
|
||||
AppCommandError,
|
||||
CommandAlreadyRegistered,
|
||||
CommandNotFound,
|
||||
CommandSignatureMismatch,
|
||||
)
|
||||
from ..errors import ClientException
|
||||
from ..utils import MISSING
|
||||
|
||||
@ -385,6 +392,35 @@ class CommandTree:
|
||||
base.extend(cmd for ((_, g, _), cmd) in self._context_menus.items() if g == guild_id)
|
||||
return base
|
||||
|
||||
async def on_error(
|
||||
self,
|
||||
interaction: Interaction,
|
||||
command: Optional[Union[ContextMenu, Command]],
|
||||
error: AppCommandError,
|
||||
) -> None:
|
||||
"""|coro|
|
||||
|
||||
A callback that is called when any command raises an :exc:`AppCommandError`.
|
||||
|
||||
The default implementation prints the traceback to stderr.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`~discord.Interaction`
|
||||
The interaction that is being handled.
|
||||
command: Optional[Union[:class:`~discord.app_commands.Command`, :class:`~discord.app_commands.ContextMenu`]]
|
||||
The command that failed, if any.
|
||||
error: :exc:`AppCommandError`
|
||||
The exception that was raised.
|
||||
"""
|
||||
|
||||
if command is not None:
|
||||
print(f'Ignoring exception in command {command.name!r}:', file=sys.stderr)
|
||||
else:
|
||||
print(f'Ignoring exception in command tree:', file=sys.stderr)
|
||||
|
||||
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)
|
||||
|
||||
def command(
|
||||
self,
|
||||
*,
|
||||
@ -519,8 +555,8 @@ class CommandTree:
|
||||
async def wrapper():
|
||||
try:
|
||||
await self.call(interaction)
|
||||
except Exception as e:
|
||||
print(f'Error:', e)
|
||||
except AppCommandError as e:
|
||||
await self.on_error(interaction, None, e)
|
||||
|
||||
self.client.loop.create_task(wrapper(), name='CommandTree-invoker')
|
||||
|
||||
@ -547,7 +583,10 @@ class CommandTree:
|
||||
raise RuntimeError('This should not happen if Discord sent well-formed data.')
|
||||
|
||||
# I assume I don't have to type check here.
|
||||
try:
|
||||
await ctx_menu._invoke(interaction, value)
|
||||
except AppCommandError as e:
|
||||
await self.on_error(interaction, ctx_menu, e)
|
||||
|
||||
async def call(self, interaction: Interaction):
|
||||
"""|coro|
|
||||
@ -623,4 +662,9 @@ class CommandTree:
|
||||
# At this point options refers to the arguments of the command
|
||||
# and command refers to the class type we care about
|
||||
namespace = Namespace(interaction, data.get('resolved', {}), options)
|
||||
|
||||
try:
|
||||
await command._invoke_with_namespace(interaction, namespace)
|
||||
except AppCommandError as e:
|
||||
await command._invoke_error_handler(interaction, e)
|
||||
await self.on_error(interaction, command, e)
|
||||
|
Loading…
x
Reference in New Issue
Block a user