mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-06-08 04:38:42 +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 ..interactions import Interaction
|
||||||
from ..enums import ChannelType, try_enum
|
from ..enums import ChannelType, try_enum
|
||||||
from .models import AppCommandChannel, AppCommandThread, Choice
|
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 ..utils import resolve_annotation, MISSING, is_inside_class
|
||||||
from ..user import User
|
from ..user import User
|
||||||
from ..member import Member
|
from ..member import Member
|
||||||
@ -62,7 +62,6 @@ from ..permissions import Permissions
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import ParamSpec, Concatenate
|
from typing_extensions import ParamSpec, Concatenate
|
||||||
from ..interactions import Interaction
|
|
||||||
from ..types.interactions import (
|
from ..types.interactions import (
|
||||||
ResolvedData,
|
ResolvedData,
|
||||||
PartialThread,
|
PartialThread,
|
||||||
@ -89,6 +88,10 @@ else:
|
|||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
GroupT = TypeVar('GroupT', bound='Group')
|
GroupT = TypeVar('GroupT', bound='Group')
|
||||||
Coro = Coroutine[Any, Any, T]
|
Coro = Coroutine[Any, Any, T]
|
||||||
|
Error = Union[
|
||||||
|
Callable[[GroupT, Interaction, AppCommandError], Coro[Any]],
|
||||||
|
Callable[[Interaction, AppCommandError], Coro[Any]],
|
||||||
|
]
|
||||||
|
|
||||||
ContextMenuCallback = Union[
|
ContextMenuCallback = Union[
|
||||||
# If groups end up support context menus these would be uncommented
|
# 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._callback: CommandCallback[GroupT, P, T] = callback
|
||||||
self.parent: Optional[Group] = parent
|
self.parent: Optional[Group] = parent
|
||||||
self.binding: Optional[GroupT] = None
|
self.binding: Optional[GroupT] = None
|
||||||
|
self.on_error: Optional[Error[GroupT]] = None
|
||||||
self._params: Dict[str, CommandParameter] = _extract_parameters_from_callback(callback, callback.__globals__)
|
self._params: Dict[str, CommandParameter] = _extract_parameters_from_callback(callback, callback.__globals__)
|
||||||
|
|
||||||
def _copy_with_binding(self, binding: GroupT) -> Command:
|
def _copy_with_binding(self, binding: GroupT) -> Command:
|
||||||
@ -453,6 +457,7 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
copy.description = self.description
|
copy.description = self.description
|
||||||
copy._callback = self._callback
|
copy._callback = self._callback
|
||||||
copy.parent = self.parent
|
copy.parent = self.parent
|
||||||
|
copy.on_error = self.on_error
|
||||||
copy._params = self._params.copy()
|
copy._params = self._params.copy()
|
||||||
copy.binding = binding
|
copy.binding = binding
|
||||||
return copy
|
return copy
|
||||||
@ -468,6 +473,21 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
'options': [param.to_dict() for param in self._params.values()],
|
'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:
|
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)
|
defaults = ((name, param.default) for name, param in self._params.items() if not param.required)
|
||||||
namespace._update_with_defaults(defaults)
|
namespace._update_with_defaults(defaults)
|
||||||
@ -477,7 +497,7 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
if self.binding is not None:
|
if self.binding is not None:
|
||||||
return await self._callback(self.binding, interaction, **namespace.__dict__) # type: ignore
|
return await self._callback(self.binding, interaction, **namespace.__dict__) # type: ignore
|
||||||
return await self._callback(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,
|
# 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
|
# 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
|
# 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
|
frame = inspect.trace()[-1].frame
|
||||||
if frame.f_locals.get('self') is self:
|
if frame.f_locals.get('self') is self:
|
||||||
raise CommandSignatureMismatch(self) from None
|
raise CommandSignatureMismatch(self) from None
|
||||||
|
raise CommandInvokeError(self, e) from e
|
||||||
|
except AppCommandError:
|
||||||
raise
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise CommandInvokeError(self, e) from e
|
||||||
|
|
||||||
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]:
|
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]:
|
||||||
return None
|
return None
|
||||||
@ -503,6 +527,32 @@ class Command(Generic[GroupT, P, T]):
|
|||||||
parent = self.parent
|
parent = self.parent
|
||||||
return parent.parent or 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:
|
class ContextMenu:
|
||||||
"""A class that implements a context menu application command.
|
"""A class that implements a context menu application command.
|
||||||
@ -560,7 +610,12 @@ class ContextMenu:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def _invoke(self, interaction: Interaction, arg: Any):
|
async def _invoke(self, interaction: Interaction, arg: Any):
|
||||||
await self._callback(interaction, arg)
|
try:
|
||||||
|
await self._callback(interaction, arg)
|
||||||
|
except AppCommandError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise CommandInvokeError(self, e) from e
|
||||||
|
|
||||||
|
|
||||||
class Group:
|
class Group:
|
||||||
@ -668,6 +723,25 @@ class Group:
|
|||||||
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]:
|
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]:
|
||||||
return self._children.get(name)
|
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):
|
def add_command(self, command: Union[Command, Group], /, *, override: bool = False):
|
||||||
"""Adds a command or group to this group's internal list of commands.
|
"""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
|
from ..errors import DiscordException
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'AppCommandError',
|
||||||
|
'CommandInvokeError',
|
||||||
'CommandAlreadyRegistered',
|
'CommandAlreadyRegistered',
|
||||||
'CommandSignatureMismatch',
|
'CommandSignatureMismatch',
|
||||||
'CommandNotFound',
|
'CommandNotFound',
|
||||||
@ -39,9 +41,54 @@ if TYPE_CHECKING:
|
|||||||
from .commands import Command, Group, ContextMenu
|
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.
|
"""An exception raised when a command is already registered.
|
||||||
|
|
||||||
|
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@ -57,9 +104,13 @@ class CommandAlreadyRegistered(DiscordException):
|
|||||||
super().__init__(f'Command {name!r} already registered.')
|
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.
|
"""An exception raised when an application command could not be found.
|
||||||
|
|
||||||
|
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
------------
|
------------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@ -78,12 +129,16 @@ class CommandNotFound(DiscordException):
|
|||||||
super().__init__(f'Application command {name!r} not found')
|
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
|
"""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 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
|
from the command definition you provided Discord. Either your code is out of date or the
|
||||||
data from Discord is out of sync.
|
data from Discord is out of sync.
|
||||||
|
|
||||||
|
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
------------
|
------------
|
||||||
command: Union[:class:`~.app_commands.Command`, :class:`~.app_commands.ContextMenu`, :class:`~.app_commands.Group`]
|
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
|
from __future__ import annotations
|
||||||
import inspect
|
import inspect
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
from typing import Callable, Dict, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, Union, overload
|
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 .models import AppCommand
|
||||||
from .commands import Command, ContextMenu, Group, _shorten
|
from .commands import Command, ContextMenu, Group, _shorten
|
||||||
from .enums import AppCommandType
|
from .enums import AppCommandType
|
||||||
from .errors import CommandAlreadyRegistered, CommandNotFound, CommandSignatureMismatch
|
from .errors import (
|
||||||
|
AppCommandError,
|
||||||
|
CommandAlreadyRegistered,
|
||||||
|
CommandNotFound,
|
||||||
|
CommandSignatureMismatch,
|
||||||
|
)
|
||||||
from ..errors import ClientException
|
from ..errors import ClientException
|
||||||
from ..utils import MISSING
|
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)
|
base.extend(cmd for ((_, g, _), cmd) in self._context_menus.items() if g == guild_id)
|
||||||
return base
|
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(
|
def command(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@ -519,8 +555,8 @@ class CommandTree:
|
|||||||
async def wrapper():
|
async def wrapper():
|
||||||
try:
|
try:
|
||||||
await self.call(interaction)
|
await self.call(interaction)
|
||||||
except Exception as e:
|
except AppCommandError as e:
|
||||||
print(f'Error:', e)
|
await self.on_error(interaction, None, e)
|
||||||
|
|
||||||
self.client.loop.create_task(wrapper(), name='CommandTree-invoker')
|
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.')
|
raise RuntimeError('This should not happen if Discord sent well-formed data.')
|
||||||
|
|
||||||
# I assume I don't have to type check here.
|
# I assume I don't have to type check here.
|
||||||
await ctx_menu._invoke(interaction, value)
|
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):
|
async def call(self, interaction: Interaction):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
@ -623,4 +662,9 @@ class CommandTree:
|
|||||||
# At this point options refers to the arguments of the command
|
# At this point options refers to the arguments of the command
|
||||||
# and command refers to the class type we care about
|
# and command refers to the class type we care about
|
||||||
namespace = Namespace(interaction, data.get('resolved', {}), options)
|
namespace = Namespace(interaction, data.get('resolved', {}), options)
|
||||||
await command._invoke_with_namespace(interaction, namespace)
|
|
||||||
|
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