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.