Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Raise an exception from a higher level, a la warnings

In the module warnings (https://docs.python.org/3.5/library/warnings.html) there is the ability to raise a warning that appears to come from somewhere earlier in the stack:

warnings.warn('This is a test', stacklevel=2)

Is there an equivalent for raising errors? I know I can raise an error with an alternative traceback, but I can't create that traceback within the module since it needs to come from earlier. I imagine something like:

tb = magic_create_traceback_right_here()
raise ValueError('This is a test').with_traceback(tb.tb_next)

The reason is that I am developing a module that has a function module.check_raise that I want to raise an error that appears to originate from where the function is called. If I raise an error within the module.check_raise function, it appears to originate from within module.check_raise, which is undesired.

Also, I've tried tricks like raising a dummy exception, catching it, and passing the traceback along, but somehow the tb_next becomes None. I'm out of ideas.

Edit:

I would like the output of this minimal example (called tb2.py):

import check_raise

check_raise.raise_if_string_is_true('True')

to be only this:

Traceback (most recent call last):
  File "tb2.py", line 10, in <module>
    check_raise.raise_if_string_is_true(string)
RuntimeError: An exception was raised.
like image 443
Joel Avatar asked Dec 09 '15 09:12

Joel


2 Answers

I can't believe I am posting this

By doing this you are going against the zen.

Special cases aren't special enough to break the rules.

But if you insist here is your magical code.

check_raise.py

import sys
import traceback

def raise_if_string_is_true(string):
    if string == 'true':
        #the frame that called this one
        f = sys._getframe().f_back
        #the most USELESS error message ever
        e = RuntimeError("An exception was raised.")

        #the first line of an error message
        print('Traceback (most recent call last):',file=sys.stderr)
        #the stack information, from f and above
        traceback.print_stack(f)
        #the last line of the error
        print(*traceback.format_exception_only(type(e),e),
              file=sys.stderr, sep="",end="")

        #exit the program
        #if something catches this you will cause so much confusion
        raise SystemExit(1)
        # SystemExit is the only exception that doesn't trigger an error message by default.

This is pure python, does not interfere with sys.excepthook and even in a try block it is not caught with except Exception: although it is caught with except:

test.py

import check_raise

check_raise.raise_if_string_is_true("true")
print("this should never be printed")

will give you the (horribly uninformative and extremely forged) traceback message you desire.

Tadhgs-MacBook-Pro:Documents Tadhg$ python3 test.py
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    check_raise.raise_if_string_is_true("true")
RuntimeError: An exception was raised.
Tadhgs-MacBook-Pro:Documents Tadhg$
like image 128
Tadhg McDonald-Jensen Avatar answered Oct 24 '22 02:10

Tadhg McDonald-Jensen


If I understand correctly, you would like the output of this minimal example:

def check_raise(function):
    try:
        return function()
    except Exception:
        raise RuntimeError('An exception was raised.')

def function():
    1/0

check_raise(function)

to be only this:

Traceback (most recent call last):
  File "tb2.py", line 10, in <module>
    check_raise(function)
RuntimeError: An exception was raised.

In fact, it's a lot more output; there is exception chaining, which could be dealt with by handling the RuntimeError immediately, removing its __context__, and re-raising it, and there is another line of traceback for the RuntimeError itself:

  File "tb2.py", line 5, in check_raise
    raise RuntimeError('An exception was raised.')

As far as I can tell, it is not possible for pure Python code to substitute the traceback of an exception after it was raised; the interpreter has control of adding to it but it only exposes the current traceback whenever the exception is handled. There is no API (not even when using tracing functions) for passing your own traceback to the interpreter, and traceback objects are immutable (this is what's tackled by that Jinja hack involving C-level stuff).

So further assuming that you're interested in the shortened traceback not for further programmatic use but only for user-friendly output, your best bet will be an excepthook that controls how the traceback is printed to the console. For determining where to stop printing, a special local variable could be used (this is a bit more robust than limiting the traceback to its length minus 1 or such). This example requires Python 3.5 (for traceback.walk_tb):

import sys
import traceback

def check_raise(function):
    __exclude_from_traceback_from_here__ = True
    try:
        return function()
    except Exception:
        raise RuntimeError('An exception was raised.')

def print_traceback(exc_type, exc_value, tb):
    for i, (frame, lineno) in enumerate(traceback.walk_tb(tb)):
        if '__exclude_from_traceback_from_here__' in frame.f_code.co_varnames:
            limit = i
            break
    else:
        limit = None
    traceback.print_exception(
        exc_type, exc_value, tb, limit=limit, chain=False)

sys.excepthook = print_traceback

def function():
    1/0

check_raise(function)

This is the output now:

Traceback (most recent call last):
  File "tb2.py", line 26, in <module>
    check_raise(function)
RuntimeError: An exception was raised.
like image 2
Thomas Lotze Avatar answered Oct 24 '22 03:10

Thomas Lotze