diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index a3301bd80..fc22475d6 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -38,7 +38,7 @@ import discord from .core import GroupMixin, Command from .view import StringView from .context import Context -from .errors import CommandNotFound, CommandError +from . import errors from .help import HelpCommand, DefaultHelpCommand from .cog import Cog @@ -571,14 +571,14 @@ class BotBase(GroupMixin): setup = getattr(lib, 'setup') except AttributeError: del sys.modules[key] - raise discord.ClientException('extension {!r} ({!r}) does not have a setup function.'.format(key, lib)) + raise errors.NoEntryPointError(key) try: setup(self) - except Exception: + except Exception as e: self._remove_module_references(lib.__name__) self._call_module_finalizers(lib, key) - raise + raise errors.ExtensionFailed(key, e) from e else: self._extensions[key] = lib @@ -601,20 +601,25 @@ class BotBase(GroupMixin): Raises -------- - ClientException - The extension does not have a setup function. - ImportError + ExtensionNotFound The extension could not be imported. - Exception - Any other exception raised by the extension will be raised back - to the caller. + ExtensionAlreadyLoaded + The extension is already loaded. + NoEntryPointError + The extension does not have a setup function. + ExtensionFailed + The extension setup function had an execution error. """ if name in self._extensions: - return + raise errors.ExtensionAlreadyLoaded(name) - lib = importlib.import_module(name) - self._load_from_module_spec(lib, name) + try: + lib = importlib.import_module(name) + except ImportError as e: + raise errors.ExtensionNotFound(name, e) from e + else: + self._load_from_module_spec(lib, name) def unload_extension(self, name): """Unloads an extension. @@ -633,11 +638,16 @@ class BotBase(GroupMixin): The extension name to unload. It must be dot separated like regular Python imports if accessing a sub-module. e.g. ``foo.test`` if you want to import ``foo/test.py``. + + Raises + ------- + ExtensionNotLoaded + The extension was not loaded. """ lib = self._extensions.get(name) if lib is None: - return + raise errors.ExtensionNotLoaded(name) self._remove_module_references(lib.__name__) self._call_module_finalizers(lib, name) @@ -659,14 +669,19 @@ class BotBase(GroupMixin): Raises ------- - Exception - Any exception raised by the extension will be raised back - to the caller. + ExtensionNotLoaded + The extension was not loaded. + ExtensionNotFound + The extension could not be imported. + NoEntryPointError + The extension does not have a setup function. + ExtensionFailed + The extension setup function had an execution error. """ lib = self._extensions.get(name) if lib is None: - return + raise errors.ExtensionNotLoaded(name) # get the previous module states from sys modules modules = { @@ -843,12 +858,12 @@ class BotBase(GroupMixin): try: if await self.can_run(ctx, call_once=True): await ctx.command.invoke(ctx) - except CommandError as exc: + except errors.CommandError as exc: await ctx.command.dispatch_error(ctx, exc) else: self.dispatch('command_completion', ctx) elif ctx.invoked_with: - exc = CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) + exc = errors.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) self.dispatch('command_error', ctx, exc) async def process_commands(self, message): diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 599827570..bb58fcdd9 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -34,7 +34,9 @@ __all__ = ['CommandError', 'MissingRequiredArgument', 'BadArgument', 'MissingPermissions', 'BotMissingPermissions', 'ConversionError', 'BadUnionArgument', 'ArgumentParsingError', 'UnexpectedQuoteError', 'InvalidEndOfQuotedStringError', - 'ExpectedClosingQuoteError', ] + 'ExpectedClosingQuoteError', 'ExtensionError', 'ExtensionAlreadyLoaded', + 'ExtensionNotLoaded', 'NoEntryPointError', 'ExtensionFailed', + 'ExtensionNotFound' ] class CommandError(DiscordException): r"""The base exception type for all command related errors. @@ -283,3 +285,80 @@ class ExpectedClosingQuoteError(ArgumentParsingError): def __init__(self, close_quote): self.close_quote = close_quote super().__init__('Expected closing {}.'.format(close_quote)) + +class ExtensionError(DiscordException): + """Base exception for extension related errors. + + This inherits from :exc:`~discord.DiscordException`. + + Parameter + ----------- + name: :class:`str` + The extension that had an error. + """ + def __init__(self, message=None, *args, name): + self.name = name + message = message or 'Extension {!r} had an error.'.format(name) + # clean-up @everyone and @here mentions + m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere') + super().__init__(m, *args) + +class ExtensionAlreadyLoaded(ExtensionError): + """An exception raised when an extension has already been loaded. + + This inherits from :exc:`ExtensionError` + """ + def __init__(self, name): + super().__init__('Extension {!r} is already loaded.'.format(name), name=name) + +class ExtensionNotLoaded(ExtensionError): + """An exception raised when an extension was not loaded. + + This inherits from :exc:`ExtensionError` + """ + def __init__(self, name): + super().__init__('Extension {!r} has not been loaded.'.format(name), name=name) + +class NoEntryPointError(ExtensionError): + """An exception raised when an extension does not have a ``setup`` entry point function. + + This inherits from :exc:`ExtensionError` + """ + def __init__(self, name): + super().__init__("Extension {!r} has no 'setup' function.".format(name), name=name) + +class ExtensionFailed(ExtensionError): + """An exception raised when an extension failed to load during execution of the ``setup`` entry point. + + This inherits from :exc:`ExtensionError` + + Attributes + ----------- + name: :class:`str` + The extension that had the error. + original: :exc:`Exception` + The original exception that was raised. You can also get this via + the ``__cause__`` attribute. + """ + def __init__(self, name, original): + self.original = original + fmt = 'Extension {0!r} raised an error: {1.__class__.__name__}: {1}' + super().__init__(fmt.format(name, original), name=name) + +class ExtensionNotFound(ExtensionError): + """An exception raised when an extension failed to be imported. + + This inherits from :exc:`ExtensionError` + + Attributes + ----------- + name: :class:`str` + The extension that had the error. + original: :exc:`ImportError` + The original exception that was raised. You can also get this via + the ``__cause__`` attribute. + """ + def __init__(self, name, original): + self.original = original + fmt = 'Extension {0!r} could not be loaded.' + super().__init__(fmt.format(name), name=name) diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index ca822c401..4722a56a4 100644 --- a/docs/ext/commands/api.rst +++ b/docs/ext/commands/api.rst @@ -285,6 +285,25 @@ Exceptions .. autoexception:: discord.ext.commands.BotMissingPermissions :members: +.. autoexception:: discord.ext.commands.ExtensionError + :members: + +.. autoexception:: discord.ext.commands.ExtensionAlreadyLoaded + :members: + +.. autoexception:: discord.ext.commands.ExtensionNotLoaded + :members: + +.. autoexception:: discord.ext.commands.NoEntryPointError + :members: + +.. autoexception:: discord.ext.commands.ExtensionFailed + :members: + +.. autoexception:: discord.ext.commands.ExtensionNotFound + :members: + + Exception Hierarchy +++++++++++++++++++++ @@ -311,3 +330,9 @@ Exception Hierarchy - :exc:`~.commands.DisabledCommand` - :exc:`~.commands.CommandInvokeError` - :exc:`~.commands.CommandOnCooldown` + - :exc:`~.commands.ExtensionError` + - :exc:`~.commands.ExtensionAlreadyLoaded` + - :exc:`~.commands.ExtensionNotLoaded` + - :exc:`~.commands.NoEntryPointError` + - :exc:`~.commands.ExtensionFailed` + - :exc:`~.commands.ExtensionNotFound`