Raise special CommandSyncFailure during sync for better errors

This is parsed from the error to allow for users to better debug
what exactly is causing the issue in sync.
This commit is contained in:
Rapptz 2022-08-12 10:20:33 -04:00
parent a9025ca3d1
commit 1fa7d7e402
5 changed files with 82 additions and 7 deletions

View File

@ -26,9 +26,8 @@ from __future__ import annotations
from typing import Any, TYPE_CHECKING, List, Optional, Union
from ..enums import AppCommandOptionType, AppCommandType, Locale
from ..errors import DiscordException
from ..errors import DiscordException, HTTPException, _flatten_error_dict
__all__ = (
'AppCommandError',
@ -47,6 +46,7 @@ __all__ = (
'BotMissingPermissions',
'CommandOnCooldown',
'MissingApplicationID',
'CommandSyncFailure',
)
if TYPE_CHECKING:
@ -56,6 +56,8 @@ if TYPE_CHECKING:
from ..types.snowflake import Snowflake, SnowflakeList
from .checks import Cooldown
CommandTypes = Union[Command[Any, ..., Any], Group, ContextMenu]
APP_ID_NOT_FOUND = (
'Client does not have an application_id set. Either the function was called before on_ready '
'was called or application_id was not passed to the Client constructor.'
@ -444,3 +446,58 @@ class MissingApplicationID(AppCommandError):
def __init__(self, message: Optional[str] = None):
super().__init__(message or APP_ID_NOT_FOUND)
def _get_command_error(index: str, inner: Any, commands: List[CommandTypes], messages: List[str]) -> None:
# Top level errors are:
# <number>: { <key>: <error> }
# The dicts could be nested, e.g.
# <number>: { <key>: { <second>: <error> } }
# Luckily, this is already handled by the flatten_error_dict utility
if not index.isdigit():
errors = _flatten_error_dict(inner, index)
messages.extend(f'In {k}: {v}' for k, v in errors.items())
return
idx = int(index)
try:
command = commands[idx]
except IndexError:
errors = _flatten_error_dict(inner, index)
messages.extend(f'In {k}: {v}' for k, v in errors.items())
return
callback = getattr(command, 'callback', None)
class_name = command.__class__.__name__
if callback:
messages.append(f'In {class_name} {command.qualified_name!r} defined in {callback.__qualname__!r}')
else:
messages.append(f'In {class_name} {command.qualified_name!r} defined in module {command.module!r}')
errors = _flatten_error_dict(inner)
messages.extend(f' {k}: {v}' for k, v in errors.items())
class CommandSyncFailure(AppCommandError, HTTPException):
"""An exception raised when :meth:`CommandTree.sync` failed.
This provides syncing failures in a slightly more readable format.
This inherits from :exc:`~discord.app_commands.AppCommandError`
and :exc:`~discord.HTTPException`.
.. versionadded:: 2.0
"""
def __init__(self, child: HTTPException, commands: List[CommandTypes]) -> None:
# Consume the child exception and make it seem as if we are that exception
self.__dict__.update(child.__dict__)
messages = [f'Failed to upload commands to Discord (HTTP status {self.status}, error code {self.code})']
if self._errors:
for index, inner in self._errors.items():
_get_command_error(index, inner, commands, messages)
# Equivalent to super().__init__(...) but skips other constructors
self.args = ('\n'.join(messages),)

View File

@ -56,10 +56,11 @@ from .errors import (
CommandNotFound,
CommandSignatureMismatch,
CommandLimitReached,
CommandSyncFailure,
MissingApplicationID,
)
from .translator import Translator, locale_str
from ..errors import ClientException
from ..errors import ClientException, HTTPException
from ..enums import AppCommandType, InteractionType
from ..utils import MISSING, _get_as_snowflake, _is_submodule
@ -1034,6 +1035,10 @@ class CommandTree(Generic[ClientT]):
-------
HTTPException
Syncing the commands failed.
CommandSyncFailure
Syncing the commands failed due to a user related error, typically because
the command has invalid data. This is equivalent to an HTTP status code of
400.
Forbidden
The client does not have the ``applications.commands`` scope in the guild.
MissingApplicationID
@ -1058,10 +1063,15 @@ class CommandTree(Generic[ClientT]):
else:
payload = [command.to_dict() for command in commands]
if guild is None:
data = await self._http.bulk_upsert_global_commands(self.client.application_id, payload=payload)
else:
data = await self._http.bulk_upsert_guild_commands(self.client.application_id, guild.id, payload=payload)
try:
if guild is None:
data = await self._http.bulk_upsert_global_commands(self.client.application_id, payload=payload)
else:
data = await self._http.bulk_upsert_guild_commands(self.client.application_id, guild.id, payload=payload)
except HTTPException as e:
if e.status == 400:
raise CommandSyncFailure(e, commands) from None
raise
return [AppCommand(data=d, state=self._state) for d in data]

View File

@ -121,6 +121,7 @@ class HTTPException(DiscordException):
self.code = message.get('code', 0)
base = message.get('message', '')
errors = message.get('errors')
self._errors: Optional[Dict[str, Any]] = errors
if errors:
errors = _flatten_error_dict(errors)
helpful = '\n'.join('In %s: %s' % t for t in errors.items())

View File

@ -4724,4 +4724,5 @@ Exception Hierarchy
- :exc:`Forbidden`
- :exc:`NotFound`
- :exc:`DiscordServerError`
- :exc:`app_commands.CommandSyncFailure`
- :exc:`RateLimited`

View File

@ -721,6 +721,9 @@ Exceptions
.. autoexception:: discord.app_commands.MissingApplicationID
:members:
.. autoexception:: discord.app_commands.CommandSyncFailure
:members:
Exception Hierarchy
~~~~~~~~~~~~~~~~~~~~
@ -743,3 +746,6 @@ Exception Hierarchy
- :exc:`~discord.app_commands.CommandSignatureMismatch`
- :exc:`~discord.app_commands.CommandNotFound`
- :exc:`~discord.app_commands.MissingApplicationID`
- :exc:`~discord.app_commands.CommandSyncFailure`
- :exc:`~discord.HTTPException`
- :exc:`~discord.app_commands.CommandSyncFailure`