Thanks for the feedback! I put this aside for a while but I'm coming back
to it now and cleaning it up.

The approach used in this first post was obviously very clumsy. In my
latest version I am using module instance directly (as shown in Nathaniel's
reply) and using the qualified package name (as suggested by Roger). I
created an explicit blacklist (incomplete--still needs more testing) of
functions to hide in my custom backtraces and refactored a bit so I can
write tests for it. Code below.

One interesting thing I learned while working on this is that the
backtraces change depending on the asyncio debug mode, because in debug
mode couroutines are wrapped in CoroWrapper[1], which adds a frame
everytime a coroutine sends, throws, etc. So I am now thinking that my
custom excepthook is probably most useful in debug mode, but probably not
good to enable in production.

I'm working on a more general asyncio task group library that will include
this excepthook. I'll release the whole thing on PyPI when it's done.

[1]
https://github.com/python/cpython/blob/23ab5ee667a9b29014f6f7f01797c611f63ff743/Lib/asyncio/coroutines.py#L25

---


def _async_excepthook(type_, exc, tb):
    '''
    An ``excepthook`` that hides event loop internals and displays
    task group information.

    :param type type_: the exception type
    :param Exception exc: the exception itself
    :param tb tb: a traceback of the exception
    '''
    print(_async_excepthook_format(type_, exc, tb))


def _async_excepthook_format(type_, exc, tb):
    '''
    This helper function is used for testing.

    :param type type_: the exception type
    :param Exception exc: the exception itself
    :param tracetack tb: a traceback of the exception
    :return: the formatted traceback as a string
    '''
    format_str = ''
    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 = '\nDuring handling of the above exception, ' \
                    'another exception occurred:'

    if cause_exc:
        format_str += _async_excepthook_format(type(cause_exc), cause_exc,
            cause_exc.__traceback__)

    if cause_str:
        format_str += '\n{}\n\n'.format(cause_str)

    format_str += 'Async Traceback (most recent call last):\n'

    # Need file, line, function, text
    for frame, line_no in traceback.walk_tb(tb):
        if _async_excepthook_exclude(frame):
            format_str += '  ---\n'
        else:
            code = frame.f_code
            filename = code.co_filename
            line = linecache.getline(filename, line_no).strip()
            format_str += '  File "{}", line {}, in {}\n' \
                .format(filename, line_no, code.co_name)
            format_str += '    {}\n'.format(line)

    format_str += '{}: {}'.format(type_.__name__, exc)
    return format_str


_ASYNC_EXCEPTHOOK_BLACKLIST = {
    'asyncio.base_events': ('_run_once', 'call_later', 'call_soon'),
    'asyncio.coroutines': ('__next__', 'send', 'throw'),
    'asyncio.events': ('__init__', '_run'),
    'asyncio.tasks': ('_step', '_wakeup'),
    'traceback': ('extract', 'extract_stack'),
}

def _async_excepthook_exclude(frame):
    ''' Return True if ``frame`` should be excluded from tracebacks. '''
    module = frame.f_globals['__name__']
    function = frame.f_code.co_name
    return module in _ASYNC_EXCEPTHOOK_BLACKLIST and \
         function in _ASYNC_EXCEPTHOOK_BLACKLIST[module]


On Tue, Nov 14, 2017 at 7:15 PM, Nathaniel Smith <n...@pobox.com> wrote:

> On Tue, Nov 14, 2017 at 2:00 PM, Roger Pate <rogerp...@gmail.com> wrote:
> > On Tue, Nov 14, 2017 at 9:54 AM, Mark E. Haase <meha...@gmail.com>
> wrote:
> > ...
> >>         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('_'):
> > ...
> >> 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.
> >
> > It would be better to determine if the qualified module name is
> > "traceback" or starts with "asyncio." (or topmost package is
> > "asyncio", etc.) rather than allow false positives for
> > random_package.asyncio.module._any_function or
> > random_package.traceback._any_function.  I don't see an easy way to
> > get the module object at this point in your hook; however:
>
> You can't get the module from the cooked data that extract_tb returns,
> but it's there in the tb object itself. This walks the traceback and
> prints each frame's module:
>
> current = tb
> while current is not None:
>     print("Next module", current.tb_frame.f_globals.get("__name__"))
>     current = current.tb_next
>
> -n
>
> --
> Nathaniel J. Smith -- https://vorpus.org
>
_______________________________________________
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