-
Notifications
You must be signed in to change notification settings - Fork 0
feature/capr 30 create global error handler #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
448f4b9
014f1d8
ca0b84e
52f9e4a
92d02a7
d3af8ea
4250bd6
551fc9f
241e7e4
0649127
ac62d64
6de5bad
6bf241b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,24 @@ | ||
| from typing import Optional, TYPE_CHECKING | ||
| from __future__ import annotations | ||
|
|
||
| import warnings | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| from capy_discord.bot import Bot | ||
|
|
||
| instance: Optional["Bot"] = None | ||
| instance: Bot | None = None | ||
|
|
||
| _instance: Bot | None = None | ||
|
|
||
|
|
||
| def __getattr__(name: str) -> object: | ||
| if name == "instance": | ||
| warnings.warn( | ||
| "capy_discord.instance is deprecated. Use dependency injection.", | ||
| DeprecationWarning, | ||
| stacklevel=2, | ||
| ) | ||
| return _instance | ||
|
|
||
| msg = f"module {__name__!r} has no attribute {name!r}" | ||
| raise AttributeError(msg) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,72 @@ | ||
| import logging | ||
|
|
||
| from discord.ext.commands import AutoShardedBot | ||
| import discord | ||
| from discord import app_commands | ||
| from discord.ext import commands | ||
|
|
||
| from capy_discord.errors import UserFriendlyError | ||
| from capy_discord.ui.embeds import error_embed | ||
| from capy_discord.utils import EXTENSIONS | ||
|
|
||
|
|
||
| class Bot(AutoShardedBot): | ||
| class Bot(commands.AutoShardedBot): | ||
| """Bot class for Capy Discord.""" | ||
|
|
||
| async def setup_hook(self) -> None: | ||
| """Run before the bot starts.""" | ||
| self.log = logging.getLogger(__name__) | ||
| self.tree.on_error = self.on_tree_error # type: ignore | ||
| await self.load_extensions() | ||
|
|
||
| def _get_logger_for_command( | ||
| self, command: app_commands.Command | app_commands.ContextMenu | commands.Command | None | ||
| ) -> logging.Logger: | ||
| if command and hasattr(command, "module") and command.module: | ||
| return logging.getLogger(command.module) | ||
| return self.log | ||
|
|
||
| async def on_tree_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError) -> None: | ||
| """Handle errors in slash commands.""" | ||
| # Unpack CommandInvokeError to get the original exception | ||
| actual_error = error | ||
| if isinstance(error, app_commands.CommandInvokeError): | ||
| actual_error = error.original | ||
|
|
||
| if isinstance(actual_error, UserFriendlyError): | ||
| embed = error_embed(description=actual_error.user_message) | ||
| if interaction.response.is_done(): | ||
| await interaction.followup.send(embed=embed, ephemeral=True) | ||
| else: | ||
| await interaction.response.send_message(embed=embed, ephemeral=True) | ||
| return | ||
|
|
||
| # Generic error handling | ||
| logger = self._get_logger_for_command(interaction.command) | ||
| logger.exception("Slash command error: %s", error) | ||
|
Comment on lines
+43
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The logged error message uses the wrapper exception instead of the unwrapped In logger.exception("Slash command error: %s", actual_error)The same applies to the prefix command handler. Suggested implementation: # Generic error handling
logger = self._get_logger_for_command(interaction.command)
logger.exception("Slash command error: %s", actual_error)
embed = error_embed(description="An unexpected error occurred. Please try again later.")You should also update the prefix command handler ( actual_error = error.original
logger.exception("Command error: %s", error)change it to: actual_error = error.original
logger.exception("Command error: %s", actual_error)so that the underlying exception type and message are logged consistently for both slash and prefix commands. |
||
| embed = error_embed(description="An unexpected error occurred. Please try again later.") | ||
| if interaction.response.is_done(): | ||
| await interaction.followup.send(embed=embed, ephemeral=True) | ||
| else: | ||
| await interaction.response.send_message(embed=embed, ephemeral=True) | ||
|
|
||
| async def on_command_error(self, ctx: commands.Context, error: commands.CommandError) -> None: | ||
| """Handle errors in prefix commands.""" | ||
| # Unpack CommandInvokeError | ||
| actual_error = error | ||
| if isinstance(error, commands.CommandInvokeError): | ||
| actual_error = error.original | ||
|
|
||
| if isinstance(actual_error, UserFriendlyError): | ||
| embed = error_embed(description=actual_error.user_message) | ||
| await ctx.send(embed=embed) | ||
| return | ||
|
|
||
| # Generic error handling | ||
| logger = self._get_logger_for_command(ctx.command) | ||
| logger.exception("Command error: %s", error) | ||
| embed = error_embed(description="An unexpected error occurred. Please try again later.") | ||
| await ctx.send(embed=embed) | ||
|
|
||
| async def load_extensions(self) -> None: | ||
| """Load all enabled extensions.""" | ||
| for extension in EXTENSIONS: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| class CapyError(Exception): | ||
| """Base exception class for all Capy Discord errors.""" | ||
|
|
||
| pass | ||
|
|
||
|
|
||
| class UserFriendlyError(CapyError): | ||
| """An exception that can be safely displayed to the user. | ||
|
|
||
| Attributes: | ||
| user_message (str): The message to display to the user. | ||
| """ | ||
|
|
||
| def __init__(self, message: str, user_message: str) -> None: | ||
| """Initialize the error. | ||
|
|
||
| Args: | ||
| message: Internal log message. | ||
| user_message: User-facing message. | ||
| """ | ||
| super().__init__(message) | ||
| self.user_message = user_message |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import discord | ||
| from discord import app_commands | ||
| from discord.ext import commands | ||
|
|
||
| from capy_discord.errors import UserFriendlyError | ||
|
|
||
|
|
||
| class ErrorTest(commands.Cog): | ||
| def __init__(self, bot: commands.Bot) -> None: | ||
| self.bot = bot | ||
|
|
||
| @app_commands.command(name="error-test", description="Trigger various error types for verification") | ||
| @app_commands.choices( | ||
| error_type=[ | ||
| app_commands.Choice(name="generic", value="generic"), | ||
| app_commands.Choice(name="user-friendly", value="user-friendly"), | ||
| ] | ||
| ) | ||
| async def error_test(self, _interaction: discord.Interaction, error_type: str) -> None: | ||
| if error_type == "generic": | ||
| raise ValueError("Generic error") # noqa: TRY003 | ||
| if error_type == "user-friendly": | ||
| raise UserFriendlyError("Log", "User message") | ||
|
|
||
| @commands.command(name="error-test") | ||
| async def error_test_command(self, _ctx: commands.Context) -> None: | ||
| raise RuntimeError("Test Exception") # noqa: TRY003 | ||
|
|
||
|
|
||
| async def setup(bot: commands.Bot) -> None: | ||
| await bot.add_cog(ErrorTest(bot)) |
Uh oh!
There was an error while loading. Please reload this page.