Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get hold of the object missing an attribute

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
like image 800
flying sheep Avatar asked Oct 24 '14 12:10

flying sheep


2 Answers

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.

like image 66
Veedrac Avatar answered Sep 27 '22 20:09

Veedrac


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.

like image 32
SylvainD Avatar answered Sep 27 '22 18:09

SylvainD