From 33a69681fccc2c3b6b6eb60a66cdcf3a183f7a18 Mon Sep 17 00:00:00 2001
From: Khazhismel Kumykov <khazhy@gmail.com>
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 | 107 ++++++++++++++++-------------------
 2 files changed, 78 insertions(+), 61 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
+        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())
+        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))
+        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
+        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
-
-        except CommandError as e:
-            self.handle_local_error(e, ctx)
-            ctx.bot.dispatch('command_error', e, ctx)
-            return False
+            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 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.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