7

Suppose we try to access a non-existing attribute:

>>> {'foo': 'bar'}.gte('foo')  # well, I meant “get”!

Python’s AttributeError only has the attribute args with a string containing the finished error message: 'dict' object has no attribute 'gte'

Using the inspect and/or traceback modules with sys.last_traceback, is there a way to get hold of the actual dict object?

>>> offending_object = get_attributeerror_obj(sys.last_traceback)
>>> dir(offending_object)
[...
 'clear',
 'copy',
 'fromkeys',
 'get',       # ah, here it is!
 'items',
 ...]

Edit: since the cat is out of the bag anyway, I’ll share my findings and code (please don’t solve this and submit to PyPI, please ;))

The AttributeError is created here, which shows that there’s clearly no reference to the originating object attached.

Here the code with the same placeholder function:

import sys
import re
import difflib

AE_MSG_RE = re.compile(r"'(\w+)' object has no attribute '(\w+)'")

def get_attributeerror_obj(tb):
    ???

old_hook = sys.excepthook

def did_you_mean_hook(type, exc, tb):
    old_hook(type, exc, tb)

    if type is AttributeError:
        match = AE_MSG_RE.match(exc.args[0])
        sook = match.group(2)

        raising_obj = get_attributeerror_obj(tb)

        matches = difflib.get_close_matches(sook, dir(raising_obj))
        if matches:
            print('\n\nDid you mean?', matches[0], file=sys.stderr)

sys.excepthook = did_you_mean_hook
flying sheep
  • 8,475
  • 5
  • 56
  • 73
  • Do you mean to say something like asking the developer "did you mean get instead of gte"? – karthikr Oct 24 '14 at 12:45
  • 3
    Trying to replicate Ruby's [did you mean...](http://www.yukinishijima.net/2014/10/21/did-you-mean-experience-in-ruby.html) functionality, eh? :-) – Kevin Oct 24 '14 at 12:52
  • @Kevin: that would be an awesome package to have on PyPI! [DidYouMean](https://pypi.python.org/pypi/DidYouMean/) exists, but is about spellchecking text, not code. – ojdo Oct 24 '14 at 13:00
  • You might want to look at [this](https://github.com/dutc/didyoumean) – Noelkd Oct 25 '14 at 21:10

2 Answers2

1

It's not the answer you want, but I'm pretty sure you can't... at least not with sys.excepthook. This is because the reference counts are decremented as the frame is unwound, so it's perfectly valid for the object to be garbage collected before sys.excepthook is called. In fact, this is what happens in CPython:

import sys

class X:
    def __del__(self):
        print("deleting")

def error():
    X().wrong

old_hook = sys.excepthook
def did_you_mean_hook(type, exc, tb):
    print("Error!")
sys.excepthook = did_you_mean_hook

error()
#>>> deleting
#>>> Error!

That said, it isn't always the case. Because the exception object points to the frame, if your code looks like:

def error():
    x = X()
    x.wrong

x cannot yet be collected. x is owned by the frame, and the frame is alive. But since I've already proven that there is no explicit reference made to this object, it's not ever obvious what to do. For example,

def error():
    foo().wrong

may or may not have an object that has survived, and the only feasible way to find out is to run foo... but even then you have problems with side effects.

So no, this is not possible. If you don't mind going to any lengths whatsoever, you'll probably end up having to rewrite the AST on load (akin to FuckIt.py). You don't want to do that, though.


My suggestion would be to try using a linter to get the names of all known classes and their methods. You can use this to reverse-engineer the traceback string to get the class and incorrect method, and then run a fuzzy match to find the suggestion.

Veedrac
  • 58,273
  • 15
  • 112
  • 169
  • Thanks, this is a good answer. It didn't even occur to me to just retrieve the class from the frame using its name, and you explain well why the object isn't retrievable. I filed a feature request for CPython and will do what you proposed – flying sheep Oct 25 '14 at 15:03
1

Adding my 2 cents as I successfully (so far) tried to do something similar for DidYouMean-Python.

The trick here is that it is pretty much the one case where the error message contains enough information to infer what you actually meant. Indeed, what really matters here is that you tried to call gte on a dict object : you need the type, not the object itself.

If you had written {'foo': 'bar'}.get('foob') the situation would be much trickier to handle and I'd be happy to know if anyone had a solution.

Step one

Check that you are handling an AttributeError (using the first argument of the hook).

Step two

Retrieve the relevant information from the message (using the second argument). I did this with regexp. Please note that this exception can take multiple forms depending on the version of Python, the object you are calling the method on, etc.

So far, my regexp is : "^'?(\w+)'? (?:object|instance) has no attribute '(\w+)'$"

Step three

Get the type object corresponding to the type ('dict' in your case) so that you can call dir() on it. A dirty solution would be just use eval(type) but you can do better and cleaner by reusing the information in the trace (third argument of your hook) : the last element of the trace contains the frame in which the exception occured and in that frame, the type was properly defined (either as a local type, a global type or a builtin).

Once you have the type object, you just need to call dir() on it and extract the suggestion you like the most.

Please let me know if you need more details on what I did.

SylvainD
  • 1,743
  • 1
  • 11
  • 27
  • the same as `{'foo': 'bar'}.get('foob')` would be a case where the type isn’t in the traceback frame… I hope [PEP 437](http://legacy.python.org/dev/peps/pep-0473/) gets implemented! – flying sheep Nov 04 '14 at 18:01