Raise an exception with traceback starting from caller - python

I'm trying to make an automated test framework for a side-project and could use some help creating the assertion checks.
Running this in python...
assert(False)
Gives you this...
Traceback (most recent call last):
File "test.py", line 1, in <module>
assert(False)
AssertionError
As you can see the traceback lowest level is assert(False). So I made my custom assert that prints when the assert succeeds.
def custom_assert(condition):
if condition:
print("Yay! It werks!")
else:
raise Exception("Nay, it don't werks...")
custom_assert(False)
But instead of what assert gives, custom_assert gives me this.
Traceback (most recent call last):
File "test.py", line 14, in <module>
custom_assert(False)
File "test.py", line 12, in custom_assert
raise Exception("Nay, it don't werks...")
Exception: Nay, it don't werks...
Which is of course the default behavior. Perfectly useful 99.9999% of the time, but this is that one time it could be improved. It's not useful to know that the method I called to raise an error when the condition is false raised the error.
How can I make my custom_assert raise an exception with a traceback starting from the caller, the same way assert does?
P.S.: I don't want to print it, I want the exception to have properly modified traceback so it works properly with debuggers and other tools too!
Edit
To clarify, the traceback I want would be like this.
Traceback (most recent call last):
File "test.py", line 14, in <module>
custom_assert(False)
Exception: Nay, it don't werks...

Essentially what you want to do is something similar to this:
tb = None
try:
raise Exception('foo')
except Exception:
tb = sys.exc_info()[2]
tb.tb_frame = tb.tb_frame.f_back # This line doesn't work
raise Exception('Nay it doesnt werks').with_traceback(tb)
but you can't assign tb_frame, and from mucking around in the CPython code, this is C-generated data structures (not python) (see sys._getframe())
So your only option left is to mock the entire machinery and then convince python to use your stack. This looks like what jinja2 is doing. If that's what you choose to do, good luck! (It's out of my scope at that point)

Related

Hiding the raise call from traceback

I've been trying to get a specific (and most likely useless) effect on the traceback when I raise an error.
I would like to hide the raise Exception("Message") of the traceback inside my function and instead have my trace indicate the function call that triggered the raise inside the function
def crash(something):
if something > 200:
raise ValueError("Value too large !")
if __name__ == '__main__':
crash(500)
Normally generates :
Traceback (most recent call last):
File "file.py", line 7, in <module>
crash(500)
File "file.py", line 3, in crash
raise ValueError("Value too large !")
ValueError: Value too large !
Instead I would like to have this :
Traceback (most recent call last):
File "file.py", line 7, in <module>
crash(500)
ValueError: Value too large !
I find it clearer for the user trying to use the module because it clearly shows where the problem is as otherwise he could potentially think the module itself is at fault
I tried to find solution that would "remove" the last call from the traceback but it always had "side effects" like showing part of the code trying to remove the last trace call INSIDE the traceback itself, making it even more confusing. Sometimes it would also repeat the the traceback over what I really want.
For example using this :
def crash():
frame = sys._getframe(1)
tb = types.TracebackType(None, frame, frame.f_lasti, frame.f_lineno)
raise ValueError("Wrong value").with_traceback(tb)
Prints this :
Traceback (most recent call last):
File "file.py", line 34, in <module>
crash()
File "file.py", line 9, in crash
raise ValueError("Wrong value").with_traceback(tb)
File "file.py", line 34, in <module>
crash()
ValueError: Wrong value
I also tried using another function to create the trace myself but ended up with a weird behavior
def crash():
raise exception_no_raise(ValueError("Wrong value"))
def exception_no_raise(exc: Exception):
tb = None
depth = 0
while True:
try:
sys._getframe(depth)
depth += 1
except ValueError:
break
# for i in range(depth-1, 1, -1):
# frame = sys._getframe(i)
# tb = types.TracebackType(tb, frame, frame.f_lasti, frame.f_lineno)
# traceback.print_tb(tb)
# print(file=sys.stderr)
frame = sys._getframe(depth-1)
tb = types.TracebackType(tb, frame, frame.f_lasti, frame.f_lineno)
return exc.with_traceback(tb)
Prints :
Traceback (most recent call last):
File "file.py", line 32, in <module>
crash()
File "file.py", line 7, in crash
raise exception_no_raise(ValueError("Wrong value"))
File "file.py", line 32, in <module>
crash()
ValueError: Wrong value
Even tho when you use traceback.print_tb(tb) (before the return) you get the exact trace I want even tho using it to raise the exception doesn't print it :
File "file.py", line 34, in <module>
crash()
The last solution I found was to "strip" the last traceback from the tb.tb_next chain of traces after catching the exception and then re-raise it (but it showed the raise code anyway so ...)
I have difficulties to grasp how exactly traceback works and it seems that it changes drastically from version to version as I found code from Python 2 and 3 and also code the works in Python 3.8 and not before (related to the fact that you couldn't write to the tb.next of a trace before
Thanks for your help and clarifications !

Shorten stack trace of exceptions?

I have a custom exception get raised when someone screws up with my library, which currently looks like this:
Traceback (most recent call last):
File "main.py", line 17, in <module>
StateMachine()
File "main.py", line 15, in __new__
activate(self)
File "/home/runner/va4un94x2qp/fsm.py", line 101, in activate
state = enable(machine, state)
File "/home/runner/va4un94x2qp/fsm.py", line 94, in enable
raise MissingReturnError("State '{}' in machine '{}' missing a return statement, or it returns None..".format(StateFound.fallmsg, machine.__name__))
fsm.MissingReturnError: State '__main__' in machine 'StateMachine' missing a return statement, or it returns None.
But I want it to look like this:
Traceback (most recent call last):
File "main.py", line 17, in <module>
StateMachine()
File "main.py", line 15, in __new__
activate(self)
MissingReturnError: State '__main__' in machine 'StateMachine' missing a return statement, or it returns None.
How do I do that, without any excepthook nonsense? (Because THAT doesn't just affect one exception, it affects all exceptions) I want to modify an exception. By the way, I've tried setting suppress_context to True, I've tried raising the exception from None. How else do I do it?
And if you ask, everything I want gone, isn't needed. All that is needed is the exception message, and the call of the activate() method.
And I'm not going to be messing with the Python interpreter, I'm going to be releasing this library to the public, and I want exceptions to look this way to everyone. (It's Python 3.X)

Push a custom string to current Python stack / traceback

How can I implement a context manager with the following API:
s = "this is my message"
with PushStackFrame(s):
raise RuntimeError("something")
such that when RuntimeError is raised, I get the following message:
Traceback (most recent call last):
File "foo.py", line 4, in <module>
## PushStackFrame: this is my message ## KEY LINE
File "foo.py", line 5, in <module>
raise RuntimeError("something")
RuntimeError: something
Most importantly, I want the string passed to PushStackFrame to be inserted verbatim into the stack trace, I don't want to see just the code.
One way to do this is to catch the exception on the way out of the context manager, figure out where in the traceback the context manager was called, and insert a new traceback frame, before rethrowing the exception with traceback. I'd prefer not to do this.

How does logging.exception gets hold of the exception?

Given the following code, could some python experts tell me how does logging.exception gets reference to the exception to print it out?
>>> try:
... 1/0
... except:
... logging.exception("message")
...
ERROR:root:message
Traceback (most recent call last):
File "<console>", line 2, in <module>
ZeroDivisionError: integer division or modulo by zero
I am assuming it's going through the traceback to get that exception message, but would be good to hear from the experts.
The logging module got the exception via sys.exc_info(). You could take a look at the source code in logging module

Python logging exceptions with traceback, but without displaying messages twice

If I run the following code:
import logging
logger = logging.getLogger('creator')
try:
# some stuff
except Exception as exception:
logger.exception(exception)
I get the following output on the screen:
creator : ERROR division by zero
Traceback (most recent call last):
File "/graph_creator.py", line 21, in run
1/0
ZeroDivisionError: division by zero
Are there ways to get such a output?
creator : ERROR ZeroDivisionError: division by zero
Traceback (most recent call last):
File "/graph_creator.py", line 21, in run
1/0
Of course, I can get this (but I don't like it):
creator : ERROR Сaught exception (and etc...)
Traceback (most recent call last):
File "/graph_creator.py", line 21, in run
1/0
ZeroDivisionError: division by zero
If you called exception like this:
logger.exception('%s: %s', exception.__class__.__name__, exception)
then you could get the exception class name in the initial line.
If you need more precise changes, you can use a custom Formatter subclass which formats things exactly as you like. This would need to override format_exception to change the formatting of the traceback.

Categories