_traceback.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. from types import TracebackType
  2. import tempfile
  3. import contextlib
  4. import inspect
  5. # This file contains utilities for ensuring dynamically compile()'d
  6. # code fragments display their line numbers in backtraces.
  7. #
  8. # The constraints:
  9. #
  10. # - We don't have control over the user exception printer (in particular,
  11. # we cannot assume the linecache trick will work, c.f.
  12. # https://stackoverflow.com/q/50515651/23845 )
  13. #
  14. # - We don't want to create temporary files every time we compile()
  15. # some code; file creation should happen lazily only at exception
  16. # time. Arguably, you *should* be willing to write out your
  17. # generated Python code to file system, but in some situations
  18. # (esp. library code) it would violate user expectation to write
  19. # to the file system, so we try to avoid it. In particular, we'd
  20. # like to keep the files around, so users can open up the files
  21. # mentioned in the trace; if the file is invisible, we want to
  22. # avoid clogging up the filesystem.
  23. #
  24. # - You have control over a context where the compiled code will get
  25. # executed, so that we can interpose while the stack is unwinding
  26. # (otherwise, we have no way to interpose on the exception printing
  27. # process.)
  28. #
  29. # There are two things you have to do to make use of the utilities here:
  30. #
  31. # - When you compile your source code, you must save its string source
  32. # in its f_globals under the magic name "__compile_source__"
  33. #
  34. # - Before running the compiled code, enter the
  35. # report_compile_source_on_error() context manager.
  36. @contextlib.contextmanager
  37. def report_compile_source_on_error():
  38. try:
  39. yield
  40. except Exception as exc:
  41. tb = exc.__traceback__
  42. # Walk the traceback, looking for frames that have
  43. # source attached
  44. stack = []
  45. while tb is not None:
  46. filename = tb.tb_frame.f_code.co_filename
  47. source = tb.tb_frame.f_globals.get("__compile_source__")
  48. if filename == "<string>" and source is not None:
  49. # Don't delete the temporary file so the user can inspect it
  50. with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".py") as f:
  51. f.write(source)
  52. # Create a frame. Python doesn't let you construct
  53. # FrameType directly, so just make one with compile
  54. frame = tb.tb_frame
  55. code = compile('__inspect_currentframe()', f.name, 'eval')
  56. # Python 3.8 only. In earlier versions of Python
  57. # just have less accurate name info
  58. if hasattr(code, 'replace'):
  59. code = code.replace(co_name=frame.f_code.co_name)
  60. fake_frame = eval(
  61. code,
  62. frame.f_globals,
  63. {
  64. **frame.f_locals,
  65. '__inspect_currentframe': inspect.currentframe
  66. }
  67. )
  68. fake_tb = TracebackType(
  69. None, fake_frame, tb.tb_lasti, tb.tb_lineno
  70. )
  71. stack.append(fake_tb)
  72. else:
  73. stack.append(tb)
  74. tb = tb.tb_next
  75. # Reconstruct the linked list
  76. tb_next = None
  77. for tb in reversed(stack):
  78. tb.tb_next = tb_next
  79. tb_next = tb
  80. raise exc.with_traceback(tb_next)