Raise an exception from a higher level, a la warnings

后端 未结 3 1870
情书的邮戳
情书的邮戳 2020-12-31 10:56

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:<

相关标签:
3条回答
  • 2020-12-31 11:36

    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.
    
    0 讨论(0)
  • 2020-12-31 11:39

    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$
    
    0 讨论(0)
  • 2020-12-31 11:45

    EDIT: The previous version did not provide quotes or explanations.

    I suggest referring to PEP 3134 which states in the Motivation:

    Sometimes it can be useful for an exception handler to intentionally re-raise an exception, either to provide extra information or to translate an exception to another type. The __cause__ attribute provides an explicit way to record the direct cause of an exception.

    When an Exception is raised with a __cause__ attribute the traceback message takes the form of:

    Traceback (most recent call last):
     <CAUSE TRACEBACK>
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      <MAIN TRACEBACK>
    

    To my understanding this is exactly what you are trying to accomplish; clearly indicate that the reason for the error is not your module but somewhere else. If you are instead trying to omit information to the traceback like your edit suggests then the rest of this answer won't do you any good.


    Just a note on syntax:

    The __cause__ attribute on exception objects is always initialized to None. It is set by a new form of the 'raise' statement:

       raise EXCEPTION from CAUSE
    

    which is equivalent to:

        exc = EXCEPTION
        exc.__cause__ = CAUSE
        raise exc
    

    so the bare minimum example would be something like this:

    def function():
        int("fail")
    
    def check_raise(function):
        try:
            function()
        except Exception as original_error:
            err = RuntimeError("An exception was raised.")
            raise err from original_error
    
    check_raise(function)
    

    which gives an error message like this:

    Traceback (most recent call last):
      File "/PATH/test.py", line 7, in check_raise
        function()
      File "/PATH/test.py", line 3, in function
        int("fail")
    ValueError: invalid literal for int() with base 10: 'fail'
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/PATH/test.py", line 12, in <module>
        check_raise(function)
      File "/PATH/test.py", line 10, in check_raise
        raise err from original_error
    RuntimeError: An exception was raised.
    

    However the first line of the cause is the statement in the try block of check_raise:

      File "/PATH/test.py", line 7, in check_raise
        function()
    

    so before raising err it may (or may not) be desirable to remove the outer most traceback frame from original_error:

    except Exception as original_error:
        err = RuntimeError("An exception was raised.")
        original_error.__traceback__ = original_error.__traceback__.tb_next
        raise err from original_error
    

    This way the only line in the traceback that appears to come from check_raise is the very last raise statement which cannot be omitted with pure python code although depending on how informative the message is you can make it very clear that your module was not the cause of the problem:

    err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
    the traceback for the error is shown above.""".format(function,check_raise))
    

    The advantage to raising exception like this is that the original Traceback message is not lost when the new error is raised, which means that a very complex series of exceptions can be raised and python will still display all the relevant information correctly:

    def check_raise(function):
        try:
            function()
        except Exception as original_error:
            err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
    the traceback for the error is shown above.""".format(function,check_raise))
            original_error.__traceback__ = original_error.__traceback__.tb_next
            raise err from original_error
    
    def test_chain():
        check_raise(test)
    
    def test():
        raise ValueError
    
    check_raise(test_chain)
    

    gives me the following error message:

    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 16, in test
        raise ValueError
    ValueError
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 13, in test_chain
        check_raise(test)
      File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
        raise err from original_error
    RuntimeError: test encountered an error during call to __main__.check_raise
    the traceback for the error is shown above.
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 18, in <module>
        check_raise(test_chain)
      File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
        raise err from original_error
    RuntimeError: test_chain encountered an error during call to __main__.check_raise
    the traceback for the error is shown above.
    

    Yes it is long but it is significantly more informative then:

    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 18, in <module>
        check_raise(test_chain)
    RuntimeError: An exception was raised.
    

    not to mention that the original error is still usable even if the program doesn't end:

    import traceback
    
    def check_raise(function):
        ...
    
    def fail():
        raise ValueError
    
    try:
        check_raise(fail)
    except RuntimeError as e:
        cause = e.__cause__
        print("check_raise failed because of this error:")
        traceback.print_exception(type(cause), cause, cause.__traceback__)
    
    print("and the program continues...")
    
    0 讨论(0)
提交回复
热议问题