André Caron
André Caron

Reputation: 45224

Python traceback with module names

Stack traces in Python show file paths. Is there any way to get them to show fully qualified function names instead?

Example:

class Foo(object):
    def bar(self):
        raise Exception, "Some error."

def inner():
    return Foo().bar()

def outer():
    return inner()

I'd like my output to look like this:

In __main__.Foo.bar ("scratchpad.py", line 3)
   __main__.inner ("scratchpad.py", line 6)
   __main__.outer ("scratchpad.py", line 9)
Exception: Some error.

If it changes anything, I'm using Python 2.7.


Here's what I have so far:

import sys

class Foo(object):
    def bar(self):
        raise Exception, "Dummy value."

def inner():
    "Inner function..."
    return Foo().bar()

def outer():
    return inner()


try:
    outer()
except Exception, error:
    traceback = sys.exc_info()[2]
    while traceback is not None:
        frame = traceback.tb_frame
        print 'Name', frame.f_globals['__name__']+'.'+frame.f_code.co_name
        docs = frame.f_code.co_consts[0]
        if docs and docs != -1: # docs can be None or -1.
            print 'Docs "%s"' % docs
        print 'Args', frame.f_code.co_argcount
        print 'File "%s", line %d' % (frame.f_code.co_filename, frame.f_lineno)
        print
        traceback = traceback.tb_next

When I run it, it prints

$ python pretty-stack.py
Name __main__.<module>
Args 0
File "pretty-stack.py", line 28

Name __main__.outer
Args 0
File "pretty-stack.py", line 14

Name __main__.inner
Docs "Inner function..."
Args 0
File "pretty-stack.py", line 11

Name __main__.bar
Args 1
File "pretty-stack.py", line 7

It's almost there, but I have trouble with important use cases. For example, I can't get the class name Foo for Foo.bar().

Upvotes: 5

Views: 2717

Answers (1)

Andr&#233; Caron
Andr&#233; Caron

Reputation: 45224

There's no direct way to access symbols from a traceback since only "code objects" are accessible and, as the Python docs on code objects say:

Unlike function objects, code objects are immutable and contain no references (directly or indirectly) to mutable objects.

It seems that to retrieve the modules, functions and classes involved in the traceback, we need to search for them.


I've got an experimental version that seems to work. This implementation is based on walking the module referenced by the code object to find the function or method that references the code object in question.

from collections import namedtuple
import inspect
import sys

from nested.external import Foo

def inner(a, b='qux'):
    "Inner function..."
    return Foo().bar()

def outer(a, *args, **kwds):
    return inner(a)

def resolve_signature(function):
    """Get a formatted string that looks like the function's signature."""
    prgs, vrgs, kwds, defs = inspect.getargspec(function)
    arguments = []
    if defs:
        for name in prgs[:len(defs)-1]:
            arguments.append(name)
        for i,name in enumerate(prgs[len(defs)-1]):
            arguments.append('%s=%r'%(name,defs[i]))
    else:
        arguments.extend(prgs)
    if vrgs:
        arguments.append('*'+vrgs)
    if kwds:
        arguments.append('**'+kwds)
    return '('+', '.join(arguments)+')'


def resolve_scope(module_name, code):
    """Resolve the scope name for a code object provided its module name."""
    # Resolve module.
    module = sys.modules.get(module_name, None)
    if not module:
        return '<hidden-module>' + '.' + code.co_name + '(?)'

    # Check module's functions.
    symbols = inspect.getmembers(module, inspect.isfunction)
    for symbol_name,symbol_info in symbols:
        if symbol_info.func_code is code:
            scope = module_name + '.'
            return scope + code.co_name + resolve_signature(symbol_info)

    # Check module's classes.
    symbols = inspect.getmembers(module, inspect.isclass)
    for symbol_name,symbol_info in symbols:
        # Check class' methods.
        members = inspect.getmembers(symbol_info, inspect.ismethod)
        for method_name,method_info in members:
            if method_info.__func__.func_code is code:
                scope = module_name + '.' + symbol_name + '.'
                return scope + code.co_name + resolve_signature(method_info)

    # Default to the thing's name.  This usually happens
    # when resolving the stack frame for module-level code.
    return code.co_name

Frame = namedtuple('Frame', ['call', 'file', 'line', 'help'])

def pretty_stack(traceback=None):
    """Returns a simple stack frame."""
    frames = []
    if traceback is None:
        traceback = sys.exc_info()[2]
    while traceback is not None:
        frame = traceback.tb_frame
        call = resolve_scope(frame.f_globals['__name__'], frame.f_code)
        path = frame.f_code.co_filename.replace('\\', '/')
        line = frame.f_lineno
        docs = frame.f_code.co_consts[0]
        if docs == -1:
            docs = None
        frames.append(Frame(call, path, line, docs))
        traceback = traceback.tb_next
    return frames

try:
    outer(1)
except Exception, error:
    frames = pretty_stack()
    for frame in frames:
        print frame.call
        print '  -> "%s", line %d.' % (frame.file, frame.line)
        if frame.help:
            print frame.help
        print

When I run this, I get something like:

$ python pretty-stack.py
<module>
  -> "pretty-stack.py", line 84.

__main__.outer(a, *args, **kwds)
  -> "pretty-stack.py", line 14.

__main__.inner(a='qux')
  -> "pretty-stack.py", line 11.
Inner function...

nested.external.Foo.bar(self)
  -> "C:/Users/acaron/Desktop/nested/external.py", line 3.

Note that this even prints function signatures and doc strings, which may help during debugging.

It may not work as expected in presence of descriptors and whatnot, I haven't tried very elaborate use cases. If you can find a case in which it doesn't work, let me know and I'll try to patch it.

Upvotes: 5

Related Questions