Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

raising an exception that appears to come from the caller

I have the same question as was asked here but erroneously closed as a duplicate of another related question:

How can a Python library raise an exception in such a way that its own code it not exposed in the traceback? The motivation is to make it clear that the library function was called incorrectly: the offending line in the caller should appear to bear the blame, rather than the line inside the library that (deliberately, and correctly) raised the exception.

As pointed out in Ian's comment on the closed question, this is not the same as asking how you can adjust the code in the caller to change the way a traceback appears.

My failed attempt is below. At the line marked QUESTION, I have tried modifying the attributes of tb, e.g. tb.tb_frame = tb.tb_frame.f_back but this results in AttributeError: readonly attribute. I have also attempted to create a duck-typed object with the same attributes as tb but this fails during reraise(), with TypeError: __traceback__ must be a traceback or None. (My attempts to outwit this by subclassing traceback are met with TypeError: type 'traceback' is not an acceptable base type).

Tweaking the traceback object itself may in any case be the wrong Y for this X - perhaps there are other strategies?

Let's suppose Alice writes the following library:

import sys

# home-made six-esque Python {2,3}-compatible reraise() definition
def reraise( cls, instance, tb=None ): # Python 3 definition
    raise ( cls() if instance is None else instance ).with_traceback( tb )
try: 
    Exception().with_traceback
except: # fall back to Python 2 definition
    exec( 'def reraise( cls, instance, tb=None ): raise cls, instance, tb' )
    # has to be wrapped in exec because this would be a syntax error in Python 3.0

def LibraryFunction( a ):
    if isinstance( a, (int, float) ):
        return a + 1
    else:
        err = TypeError( "expected int or float, got %r" % a )
        RaiseFromAbove( err )   # the traceback should NOT show this line
                                # because this function knows that it is operating
                                # correctly and that the caller is at fault

def RaiseFromAbove( exception, levels=1 ):
    # start by raising and immediately catching the exception
    # so that we can get a traceback from sys.exc_info()
    try:
        raise( exception )
    except:  
        cls, instance, tb = sys.exc_info()
        for i in range( levels + 1 ):
            pass # QUESTION: how can we manipulate tb here, to remove its deepest levels?
        reraise( cls, instance, tb )

Now, suppose Alice releases the library, and Bob downloads it. Bob writes code that calls it as follows:

from AlicesLibrary import LibraryFunction

def Foo():
    LibraryFunction( 'invalid input' )  # traceback should reach this line but go no deeper

Foo()

The point is that, as things stand without a working RaiseFromAbove, the traceback will show the exception as originating from line 17 of Alice's library. Therefore, Bob (or a significant subpopulation of the Bobs out there) will email Alice saying "hey, your code is broken on line 17." But in fact, LibraryFunction() knew exactly what it was doing in issuing the exception. Alice can try her best to re-word the exception to make it as clear as possible that the library was called wrongly, but the traceback draws attention away from this fact. The place where the mistake was actually made was line 4 of Bob's code. Furthermore, Alice's code knows this, and so it's not a misplacement of authority to allow Alice's code to assign the blame where it belongs. Therefore, for greatest possible transparency and to reduce the volume of support traffic, the traceback should go no deeper than line 4 of Bob's code, without Bob having to code this behavior himself.

mattbornski provides a "you shouldn't be wanting to do this" answer here which I think misses an important point. Sure, if you say "it's not my fault" and shift the blame, you don't know that you're necessarily shifting the blame to the right place. But you do know that you (LibraryFunction) have gone to the effort of making an explicit type check on the input arguments you were handed, and that this check has succeeded (in the sense that the check itself did not raise an exception) with a negative result. And sure, Bob's code may not be "at fault" in the sense that perhaps it did not generate the invalid input - maybe Bob is just passing that argument on from somewhere else. But the difference is that he has passed it on without checking. If Bob goes to the effort of checking, and the checking code itself doesn't raise an exception, then Bob should feel free to RaiseFromAbove too, thereby helping the users of his code.

like image 261
jez Avatar asked Sep 16 '17 04:09

jez


People also ask

What does raising an exception mean?

Raising an exception is a technique for interrupting the normal flow of execution in a program, signaling that some exceptional circumstance has arisen, and returning directly to an enclosing part of the program that was designated to react to that circumstance.

How do you handle a raised exception?

In Python, exceptions can be handled using a try statement. The critical operation which can raise an exception is placed inside the try clause. The code that handles the exceptions is written in the except clause. We can thus choose what operations to perform once we have caught the exception.

Which is used to raised the exception?

The raise keyword is used to raise an exception.

What is the purpose of raising an exception in your code?

Errors come in two forms: syntax errors and exceptions. While syntax errors occur when Python can't parse a line of code, raising exceptions allows us to distinguish between regular events and something exceptional, such as errors (e.g. dividing by zero) or something you might not expect to handle.


2 Answers

There has been no good/direct/authoritative answer to the problem. I issued the bounty under the category "authoritative reference needed" and the closest thing to that have been Martijn's comments, i.e.:

  1. the way to do it definitively is to alter the traceback object or generate a new one;
  2. this cannot be done in pure Python, but must be done by mucking about with unsupported API infrastructure;
  3. this isn't worth it.

I suspected as much. So unless/until anyone can actually provide the authoritative reference to the impossibility of this approach, I'll post it here as the "accepted" answer.

But I don't accept that it's not a worthwhile wish-list item for Python. The question has generated a fair amount of "you shouldn't be wanting to do this" sentiment with which I still disagree:

  • Sure, Bob should learn to read tracebacks properly, but what's wrong with making it easier for him to do so - help Alice to help him direct his attention to the right place? The scenario of Bob being naïve enough to reach out to Alice and report a bug in her code was an exaggerated (albeit possible) example to make the point clear. More likely, he'll just have an unnecessary 2-second pause as he thinks "problem on line 17 of... oh wait, no, the caller is the problem". But why not spare him that, and make the programming UX smoother? Python's philosophy seems to me to have revolved around removing exactly this kind of friction.

  • Sure, any putative RaiseFromAbove could be used indiscriminately, or otherwise abused, by Alice, and hence could make things more confusing for Bob rather than less. To me that's a spurious argument since it would apply equally to any number of other unwise coding decisions Alice could make, and indeed to many powerful features that already exist in Python and in other languages. The worth of a sharp tool should be judged on its value when used correctly in compliance with instructions and safety warnings.

Anyway, the bounty deadline is approaching and if I do nothing I believe half the bounty goes to the highest-voted answer. Currently that's Tore's, but to me the idea of just adding 1 and letting Bob do the detective work is the opposite of what I'm driving at: that makes it look more like there's a problem in Alice's code. Bob might become a better programmer from the intellectual exercise of tracing the problem, but he might be in a hurry, and anyway by that logic we'd all be programming on the bare metal. So I'll award the bounty to yinnonsanders' answer, not because it's a full and satisfactory solution but because it's at least aligned with the spirit of the question and might work in some situations.

like image 67
jez Avatar answered Nov 14 '22 02:11

jez


You could redefine the function sys.excepthook as in this answer:

import os
import sys
import traceback

def RaiseFromAbove( exception ):
    sys.excepthook = print_traceback
    raise( exception )

def print_traceback(exc_type, exc_value, tb):
    for i, (frame, _) in enumerate(traceback.walk_tb(tb)):
        # for example:
        if os.path.basename(frame.f_code.co_filename) == 'AlicesLibrary.py':
            limit = i
            break
    else:
        limit = None
    traceback.print_exception(exc_type, exc_value, tb, limit=limit, chain=False)

This requires Python 3.5 for walk_tb

like image 21
yinnonsanders Avatar answered Nov 14 '22 02:11

yinnonsanders