From 33a69681fccc2c3b6b6eb60a66cdcf3a183f7a18 Mon Sep 17 00:00:00 2001 From: Khazhismel Kumykov Date: Sat, 4 Jun 2016 21:21:54 -0400 Subject: [PATCH] [commands] Dispatch command_error on command exec error. Provide fallback on_command_error - will only fire if no cog handlers and no local handler. Propagate exceptions in checks and argument parsing to bot. --- discord/ext/commands/bot.py | 32 +++++++++- discord/ext/commands/core.py | 113 ++++++++++++++++------------------- 2 files changed, 81 insertions(+), 64 deletions(-) diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 768e7cc16..59a498d74 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -29,11 +29,12 @@ import discord import inspect import importlib import sys +import traceback from .core import GroupMixin, Command, command from .view import StringView from .context import Context -from .errors import CommandNotFound +from .errors import CommandNotFound, CommandError from .formatter import HelpFormatter def _get_variable(name): @@ -247,6 +248,26 @@ class Bot(GroupMixin, discord.Client): coro = self._run_extra(event, event_name, *args, **kwargs) discord.compat.create_task(coro, loop=self.loop) + @asyncio.coroutine + def on_command_error(self, exception, context): + """|coro| + + The default command error handler provided by the bot. + + By default this prints to ``sys.stderr`` however it could be + overridden to have a different implementation. + + This only fires if you do not specify any listeners for command error. + """ + if self.extra_events.get('on_command_error', None): + return + + if hasattr(context.command, "on_error"): + return + + print('Ignoring exception in command {}'.format(context.command), file=sys.stderr) + traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr) + # utility "send_*" functions def say(self, *args, **kwargs): @@ -618,8 +639,13 @@ class Bot(GroupMixin, discord.Client): command = self.commands[invoker] self.dispatch('command', command, ctx) ctx.command = command - yield from command.invoke(ctx) - self.dispatch('command_completion', command, ctx) + try: + yield from command.invoke(ctx) + except CommandError as e: + command.handle_local_error(e, ctx) + self.dispatch('command_error', e, ctx) + else: + self.dispatch('command_completion', command, ctx) else: exc = CommandNotFound('Command "{}" is not found'.format(invoker)) self.dispatch('command_error', exc, ctx) diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index bc0953758..65967bdf6 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -44,7 +44,10 @@ def inject_context(ctx, coro): _internal_channel = ctx.message.channel _internal_author = ctx.message.author - ret = yield from coro(*args, **kwargs) + try: + ret = yield from coro(*args, **kwargs) + except Exception as e: + raise CommandError("Exception raised while executing command") from e return ret return wrapped @@ -306,72 +309,60 @@ class Command: @asyncio.coroutine def _parse_arguments(self, ctx): - try: - ctx.args = [] if self.instance is None else [self.instance] - ctx.kwargs = {} - args = ctx.args - kwargs = ctx.kwargs - - first = True - view = ctx.view - iterator = iter(self.params.items()) - - if self.instance is not None: - # we have 'self' as the first parameter so just advance - # the iterator and resume parsing - try: - next(iterator) - except StopIteration: - fmt = 'Callback for {0.name} command is missing "self" parameter.' - raise discord.ClientException(fmt.format(self)) - - for name, param in iterator: - if first and self.pass_context: - args.append(ctx) - first = False - continue - - if param.kind == param.POSITIONAL_OR_KEYWORD: - transformed = yield from self.transform(ctx, param) - args.append(transformed) - elif param.kind == param.KEYWORD_ONLY: - # kwarg only param denotes "consume rest" semantics - if self.rest_is_raw: - converter = self._get_converter(param) - argument = view.read_rest() - kwargs[name] = yield from self.do_conversion(ctx.bot, ctx.message, converter, argument) - else: - kwargs[name] = yield from self.transform(ctx, param) - break - elif param.kind == param.VAR_POSITIONAL: - while not view.eof: - try: - transformed = yield from self.transform(ctx, param) - args.append(transformed) - except RuntimeError: - break + ctx.args = [] if self.instance is None else [self.instance] + ctx.kwargs = {} + args = ctx.args + kwargs = ctx.kwargs - except CommandError as e: - self.handle_local_error(e, ctx) - ctx.bot.dispatch('command_error', e, ctx) - return False + first = True + view = ctx.view + iterator = iter(self.params.items()) + + if self.instance is not None: + # we have 'self' as the first parameter so just advance + # the iterator and resume parsing + try: + next(iterator) + except StopIteration: + fmt = 'Callback for {0.name} command is missing "self" parameter.' + raise discord.ClientException(fmt.format(self)) + + for name, param in iterator: + if first and self.pass_context: + args.append(ctx) + first = False + continue + + if param.kind == param.POSITIONAL_OR_KEYWORD: + transformed = yield from self.transform(ctx, param) + args.append(transformed) + elif param.kind == param.KEYWORD_ONLY: + # kwarg only param denotes "consume rest" semantics + if self.rest_is_raw: + converter = self._get_converter(param) + argument = view.read_rest() + kwargs[name] = yield from self.do_conversion(ctx.bot, ctx.message, converter, argument) + else: + kwargs[name] = yield from self.transform(ctx, param) + break + elif param.kind == param.VAR_POSITIONAL: + while not view.eof: + try: + transformed = yield from self.transform(ctx, param) + args.append(transformed) + except RuntimeError: + break return True def _verify_checks(self, ctx): - try: - if not self.enabled: - raise DisabledCommand('{0.name} command is disabled'.format(self)) - - if self.no_pm and ctx.message.channel.is_private: - raise NoPrivateMessage('This command cannot be used in private messages.') + if not self.enabled: + raise DisabledCommand('{0.name} command is disabled'.format(self)) - if not self.can_run(ctx): - raise CheckFailure('The check functions for command {0.name} failed.'.format(self)) - except CommandError as exc: - self.handle_local_error(exc, ctx) - ctx.bot.dispatch('command_error', exc, ctx) - return False + if self.no_pm and ctx.message.channel.is_private: + raise NoPrivateMessage('This command cannot be used in private messages.') + if not self.can_run(ctx): + raise CheckFailure('The check functions for command {0.name} failed.'.format(self)) return True @asyncio.coroutine