9

Not sure how possible this is, but here goes:

I'm trying to write an object with some slightly more subtle behavior - which may or may not be a good idea, I haven't determined that yet.

I have this method:

def __getattr__(self, attr):                                                                                                      
    try:                                                                       
        return self.props[attr].value                                          
    except KeyError:                                                           
        pass #to hide the keyerror exception                                   

    msg = "'{}' object has no attribute '{}'"                                  
    raise AttributeError(msg.format(self.__dict__['type'], attr)) 

Now, when I create an instance of this like so:

t = Thing()
t.foo

I get a stacktrace containing my function:

Traceback (most recent call last):
  File "attrfun.py", line 23, in <module>
    t.foo
  File "attrfun.py", line 15, in __getattr__
    raise AttributeError(msg.format(self._type, attr))
AttributeError: 'Thing' object has no attribute 'foo'

I don't want that - I want the stack trace to read:

Traceback (most recent call last):
  File "attrfun.py", line 23, in <module>
    t.foo
AttributeError: 'Thing' object has no attribute 'foo'

Is this possible with a minimal amount of effort, or is there kind of a lot required? I found this answer which indicates that something looks to be possible, though perhaps involved. If there's an easier way, I'd love to hear it! Otherwise I'll just put that idea on the shelf for now.

Community
  • 1
  • 1
Wayne Werner
  • 49,299
  • 29
  • 200
  • 290
  • 2
    I would strongly advise against this. In a month or two, you will forget where and why your function throws this specific exception. – ivan_pozdeev Nov 18 '14 at 12:27
  • if you just want a custom print use the module traceback, if you want a clean traceback just let it like this. Why use an other dict as attribute container ? self.__dict__ already exist. And change the traceback is not simple and it's not possible with pure python, you need to hack the interpreter with ctypes – Ludovic Viaud Nov 18 '14 at 12:33
  • @ivan_pozdeev On the one hand it seems like a bad thing to do, on the other hand - I don't know why I would ever be confused reading `AttributeError: 'X' has no attribute 'Y'`. – Wayne Werner Nov 18 '14 at 16:07
  • @WayneWerner You still need to know where this happens to tell: which object is the 'X' and why it's queried for 'Y' (extra confusion if reflection is involved). – ivan_pozdeev Nov 18 '14 at 16:29
  • Don't forget the code can throw exceptions on its own, too - sometimes, those and where you never expect. – ivan_pozdeev Nov 18 '14 at 16:35
  • That's what I'm *trying* to show. For instance, if you do `x = 3; x.fnord` you will get `AttributeError: 'int' has no attribute 'fnord'` and the most recent line in the stack trace will be `x.fnord`. Not `raise AttributeError("'int' has no attribute 'fnord'")` – Wayne Werner Nov 18 '14 at 20:35
  • possible duplicate of [How can I modify a Python traceback object when raising an exception?](http://stackoverflow.com/questions/1603940/how-can-i-modify-a-python-traceback-object-when-raising-an-exception) – ivan_pozdeev Nov 19 '14 at 03:35

3 Answers3

6

You cannot tamper with traceback objects (and that's a good thing). You can only control how you process one that you've already got.

The only exceptions are: you can

For your purpose, the way to go appears to be the 1st option: re-raise an exception from a handler one level above your function.

And, I'll say this again, this is harmful for yourself or whoever will be using your module as it deletes valuable diagnostic information. If you're dead set on making your module proprietary with whatever rationale, it's more productive for that goal to make it a C extension.

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
  • That looks like it's specifically for _printing_ the error message - I'm interested in actually changing the Exception itself. – Wayne Werner Nov 18 '14 at 16:13
  • the [traceback](https://docs.python.org/2/library/traceback.html) module contains several functions which allows you to walk through the stack. Get the frame (using [tb_frame()](https://docs.python.org/2/library/inspect.html#types-and-members)) and jump in the list. Regarding the exception, you can define a [new one](https://docs.python.org/2/tutorial/errors.html) following this documentation. – dcexcal Nov 19 '14 at 14:40
1

The traceback object is created during stack unwinding, not directly when you raise the exception, so you can not alter it right in your function. What you could do instead (though it's probably a bad idea) is to alter the top level exception hook so that it hides your function from the traceback.

Suppose you have this code:

class MagicGetattr:
    def __getattr__(self, item):
        raise AttributeError(f"{item} not found")

orig_excepthook = sys.excepthook
def excepthook(type, value, traceback):
    iter_tb = traceback
    while iter_tb.tb_next is not None:
        if iter_tb.tb_next.tb_frame.f_code is MagicGetattr.__getattr__.__code__:
            iter_tb.tb_next = None
            break
        iter_tb = iter_tb.tb_next

    orig_excepthook(type, value, traceback)
sys.excepthook = excepthook

# The next line will raise an error
MagicGetattr().foobar

You will get the following output:

Traceback (most recent call last):
  File "test.py", line 49, in <module>
    MagicGetattr().foobar
AttributeError: foobar not found

Note that this ignores the __cause__ and __context__ members of the exception, which you would probably want to visit too if you were to implement this in real life.

Noé Rubinstein
  • 786
  • 6
  • 17
-1

You can get the current frame and any other level using the inspect module. For instance, here is what I use when I'd like to know where I'm in my code :

from inspect import currentframe

def get_c_frame(level = 0) :
    """
    Return caller's frame
    """
    return currentframe(level)

...
def locate_error(level = 0) :
    """
    Return a string containing the filename, function name and line
    number where this function was called.

    Output is : ('file name' - 'function name' - 'line number')
    """
    fi = get_c_frame(level = level + 2)
    return '({} - {} - {})'.format(__file__,
                               fi.f_code,
                               fi.f_lineno)
dcexcal
  • 197
  • 7