If an exception is thrown while the `asyncio` event loop is running, the
stack trace is pretty complicated. Here's an example:

    Traceback (most recent call last):
      File "sample_client.py", line 616, in <module>
        loop.run_until_complete(main_task)
      File "/usr/lib/python3.6/asyncio/base_events.py", line 437, in
run_until_complete
        self.run_forever()
      File "/usr/lib/python3.6/asyncio/base_events.py", line 405, in
run_forever
        self._run_once()
      File "/usr/lib/python3.6/asyncio/base_events.py", line 1382, in
_run_once
        handle._run()
      File "/usr/lib/python3.6/asyncio/events.py", line 126, in _run
        self._callback(*self._args)
      File "/usr/lib/python3.6/asyncio/tasks.py", line 315, in _wakeup
        self._step()
      File "/usr/lib/python3.6/asyncio/tasks.py", line 239, in _step
        result = coro.send(None)
      File "sample_client.py", line 319, in main
        await actions[args.action](args, socket)
      File "/usr/lib/python3.6/asyncio/coroutines.py", line 105, in __next__
        return self.gen.send(None)
      File "sample_client.py", line 558, in sync_job
        await asyncio.sleep(args.delay)
      File "/usr/lib/python3.6/asyncio/coroutines.py", line 105, in __next__
        return self.gen.send(None)
      File "/usr/lib/python3.6/asyncio/tasks.py", line 522, in sleep
        future, result)
      File "/usr/lib/python3.6/asyncio/base_events.py", line 525, in
call_later
        timer = self.call_at(self.time() + delay, callback, *args)
      File "/usr/lib/python3.6/asyncio/base_events.py", line 541, in call_at
        timer = events.TimerHandle(when, callback, args, self)
      File "/usr/lib/python3.6/asyncio/events.py", line 148, in __init__
        super().__init__(callback, args, loop)
      File "/usr/lib/python3.6/asyncio/events.py", line 92, in __init__
        self._source_traceback = traceback.extract_stack(sys._getframe(1))
      File "/usr/lib/python3.6/traceback.py", line 207, in extract_stack
        stack = StackSummary.extract(walk_stack(f), limit=limit)
      File "/usr/lib/python3.6/traceback.py", line 352, in extract
        filename, lineno, name, lookup_line=False, locals=f_locals))
    KeyboardInterrupt

Most of the stack frames are inside asyncio (_run, _wakeup, _step) and
reveal the mechanics of the event loop. The stack trace would be a lot
easier to read (and more similar to a stack trace of an equivalent
synchronous program) if it looked like this:

    Traceback (most recent call last):
      File "sample_client.py", line 616, in <module>
        loop.run_until_complete(main_task)
      ...
      File "sample_client.py", line 319, in main
        await actions[args.action](args, socket)
      ...
      File "sample_client.py", line 558, in sync_job
        await asyncio.sleep(args.delay)
    KeyboardInterrupt

I recognize that the event loop mechanics are probably useful in some
circumstances, but in my experience working with asyncio for the last year,
I've found the stack traces are generally too noisy. For a while, I was
pasting stack traces into a text editor and cleaning them up by hand, but a
few months ago I wrote a custom excepthook that generates stack traces
similar to the one above.

    def async_excepthook(type_, exc, tb):
        cause_exc = None
        cause_str = None

        if exc.__cause__ is not None:
            cause_exc = exc.__cause__
            cause_str = 'The above exception was the direct cause ' \
                        'of the following exception:'
        elif exc.__context__ is not None and not exc.__suppress_context__:
            cause_exc = exc.__context__
            cause_str = 'During handling of the above exception, ' \
                        'another exception occurred:'

        if cause_exc:
            async_excepthook(type(cause_exc), cause_exc,
cause_exc.__traceback__)

        if cause_str:
            print('\n{}\n'.format(cause_str))

        print('Async Traceback (most recent call last):')
        for frame in traceback.extract_tb(tb):
            head, tail = os.path.split(frame.filename)
            if (head.endswith('asyncio') or tail == 'traceback.py') and \
                frame.name.startswith('_'):
                print('  ...')
                continue
            print('  File "{}", line {}, in {}'
                .format(frame.filename, frame.lineno, frame.name))
            print('    {}'.format(frame.line))
        print('{}: {}'.format(type_.__name__, exc))

The meat of it is towards the bottom, "if head.endswith('asyncio')..."There
are a lot of debatable details and this implementation is pretty hacky and
clumsy, but I have found it valuable in my own usage, and I haven't yet
missed the omitted stack frames.

I'm posting here to get constructive criticism on the concept and would
also like to hear if Curio or Trio have done anything like this. (Based on
a quick skim of the docs, I don't think they are.) I might publish a PyPI
package if anybody is interested.

Cheers,
Mark
_______________________________________________
Async-sig mailing list
Async-sig@python.org
https://mail.python.org/mailman/listinfo/async-sig
Code of Conduct: https://www.python.org/psf/codeofconduct/

Reply via email to