Basic problem
It appears that SyntaxError
s (and TypeError
s) raised by the compile()
function are not included in the stack trace returned by sys.exc_info()
, but are printed as part of the formatted output using traceback.print_exc
.
Example
For example, given the following code (where filename
is the name of a file that contains Python code with the line $ flagrant syntax error
):
import sys
from traceback import extract_tb
try:
with open(filename) as f:
code = compile(f.read(), filename, "exec")
except:
print "using sys.exc_info:"
tb_list = extract_tb(sys.exc_info()[2])
for f in tb_list:
print f
print "using traceback.print_exc:"
from traceback import print_exc
print_exc()
I get the following output (where <scriptname>
is the name of the script containing the code above):
using sys.exc_info:
('<scriptname>', 6, 'exec_file', 'code = compile(f.read(), filename, "exec")')
using traceback.print_exc:
Traceback (most recent call last):
File "<scriptname>", line 6, in exec_file
code = compile(f.read(), filename, "exec")
File "<filename>", line 3
$ flagrant syntax error
^
SyntaxError: invalid syntax
My questions
I have three questions:
- Why doesn't the traceback from
sys.exc_info()
include the frame where theSyntaxError
was generated? - How does
traceback.print_exc
get the missing frame info? - What's the best way to save the "extracted" traceback info, including the
SyntaxError
, in a list? Does the last list element (i.e. the info from the stack frame representing where theSyntaxError
occurred) need to be manually constructed usingfilename
and theSyntaxError
exception object itself?
Example use-case
For context, here's the my use-case for trying to get a complete stack-trace extraction.
I have a program that essentially implements a DSL by exec
ing some files containing user-written Python code. (Regardless of whether or not this is a good DSL-implementation strategy, I'm more or less stuck with it.) Upon encountering an error in the user code, I would (in some cases) like the interpreter to save the error for later rather than vomiting up a stack trace and dying. So I have a ScriptExcInfo
class specifically made to store this information. Here's (a slightly edited version of) that class's __init__
method, complete with a rather ugly workaround for the problem described above:
def __init__(self, scriptpath, add_frame):
self.exc, tb = sys.exc_info()[1:]
self.tb_list = traceback.extract_tb(tb)
if add_frame:
# Note: I'm pretty sure that the names of the linenumber and
# message attributes are undocumented, and I don't think there's
# actually a good way to access them.
if isinstance(exc, TypeError):
lineno = -1
text = exc.message
else:
lineno = exc.lineno
text = exc.text
# '?' is used as the function name since there's no function context
# in which the SyntaxError or TypeError can occur.
self.tb_list.append((scriptpath, lineno, '?', text))
else:
# Pop off the frames related to this `exec` infrastructure.
# Note that there's no straightforward way to remove the unwanted
# frames for the above case where the desired frame was explicitly
# constructed and appended to tb_list, and in fact the resulting
# list is out of order (!!!!).
while scriptpath != self.tb_list[-1][0]:
self.tb_list.pop()
Note that by "rather ugly," I mean that this workaround turns what should be a single-argument, 4-line __init__
function to require two arguments and take up 13 lines.
SyntaxError
occurred) need to be manually constructed using filename and theSyntaxError
exception object itself?” From the docstring onprint_exception
: “This differs fromprint_tb()
in the following ways: […] (3) if type isSyntaxError
and value has the appropriate format, it prints the line where the syntax error occurred with a caret on the next line indicating the approximate position of the error.” – Renegeprint_exc
and not really in the traceback. You will have to inspect the exception object and reconstruct it yourself, or reuse some code fromtraceback
for that purpose. – Renegeexc_info
. – Pekoe