from types import TracebackType import tempfile import contextlib import inspect # This file contains utilities for ensuring dynamically compile()'d # code fragments display their line numbers in backtraces. # # The constraints: # # - We don't have control over the user exception printer (in particular, # we cannot assume the linecache trick will work, c.f. # https://stackoverflow.com/q/50515651/23845 ) # # - We don't want to create temporary files every time we compile() # some code; file creation should happen lazily only at exception # time. Arguably, you *should* be willing to write out your # generated Python code to file system, but in some situations # (esp. library code) it would violate user expectation to write # to the file system, so we try to avoid it. In particular, we'd # like to keep the files around, so users can open up the files # mentioned in the trace; if the file is invisible, we want to # avoid clogging up the filesystem. # # - You have control over a context where the compiled code will get # executed, so that we can interpose while the stack is unwinding # (otherwise, we have no way to interpose on the exception printing # process.) # # There are two things you have to do to make use of the utilities here: # # - When you compile your source code, you must save its string source # in its f_globals under the magic name "__compile_source__" # # - Before running the compiled code, enter the # report_compile_source_on_error() context manager. @contextlib.contextmanager def report_compile_source_on_error(): try: yield except Exception as exc: tb = exc.__traceback__ # Walk the traceback, looking for frames that have # source attached stack = [] while tb is not None: filename = tb.tb_frame.f_code.co_filename source = tb.tb_frame.f_globals.get("__compile_source__") if filename == "" and source is not None: # Don't delete the temporary file so the user can inspect it with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".py") as f: f.write(source) # Create a frame. Python doesn't let you construct # FrameType directly, so just make one with compile frame = tb.tb_frame code = compile('__inspect_currentframe()', f.name, 'eval') # Python 3.8 only. In earlier versions of Python # just have less accurate name info if hasattr(code, 'replace'): code = code.replace(co_name=frame.f_code.co_name) fake_frame = eval( code, frame.f_globals, { **frame.f_locals, '__inspect_currentframe': inspect.currentframe } ) fake_tb = TracebackType( None, fake_frame, tb.tb_lasti, tb.tb_lineno ) stack.append(fake_tb) else: stack.append(tb) tb = tb.tb_next # Reconstruct the linked list tb_next = None for tb in reversed(stack): tb.tb_next = tb_next tb_next = tb raise exc.with_traceback(tb_next)