runnable_code_cell.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """show_runnable_code_cell: a Jupyter notebook like code cell
  2. * Displays an editable code cell
  3. * A Run button
  4. * An indication (*) whether the code was modified since it was last run.
  5. * The result of the last run is displayed below the code cell
  6. (you can provide a custom renderer for the result)
  7. This is very much a work in progress.
  8. """
  9. import sys
  10. import io
  11. from typing import Any, Callable
  12. from imgui_bundle import immapp, imgui_md, imgui, imgui_ctx, hello_imgui, ImVec2
  13. import textwrap
  14. from dataclasses import dataclass
  15. class _NoResult:
  16. pass
  17. _NoResultValue = _NoResult()
  18. ResultRenderer = Callable[[Any], None]
  19. def _default_result_renderer(result: Any) -> None:
  20. as_string = str(result)
  21. md_string = "```\n" + as_string + "\n```"
  22. imgui_md.render(md_string)
  23. class _CaptureStdout(list[str]):
  24. def __enter__(self) -> "_CaptureStdout":
  25. self._stdout = sys.stdout
  26. sys.stdout = self._stringio = io.StringIO()
  27. return self
  28. def __exit__(self, *args: Any) -> None:
  29. self.extend(self._stringio.getvalue().splitlines())
  30. sys.stdout = self._stdout
  31. def _execute_and_capture_last_expr(code: str) -> Any | _NoResult:
  32. with _CaptureStdout() as output:
  33. code = textwrap.dedent(code).strip() # De-indent and strip code
  34. lines = code.splitlines()
  35. if not lines:
  36. return None
  37. last_line = lines[-1]
  38. try:
  39. exec(code, globals(), locals()) # Execute the entire code block
  40. except Exception as e:
  41. return f"Error:\n{e}"
  42. try:
  43. last_line_result = eval(last_line, globals(), locals()) # Evaluate the last line
  44. except Exception:
  45. last_line_result = _NoResult()
  46. return last_line_result
  47. @dataclass
  48. class RunnableCodeCellResult:
  49. code: str = ""
  50. last_run_code: str = ""
  51. was_last_edited_code_ran: bool = False # simply indicates if last_run_code == code
  52. result: Any = _NoResultValue
  53. was_just_run: bool = False
  54. def show_runnable_code_cell(label_id: str, code: str = "", result_renderer: ResultRenderer | None = None) -> RunnableCodeCellResult:
  55. statics = show_runnable_code_cell
  56. @dataclass
  57. class CodeAndResult:
  58. snippet_data: immapp.snippets.SnippetData
  59. result: Any
  60. last_ran_code: str
  61. if not hasattr(statics, "s_code_cells"):
  62. statics.s_code_cells: dict[str, CodeAndResult] = {} # type: ignore
  63. if label_id not in statics.s_code_cells:
  64. snippet_data = immapp.snippets.SnippetData()
  65. snippet_data.code = code
  66. snippet_data.height_in_lines = code.count("\n")
  67. snippet_data.palette = immapp.snippets.SnippetTheme.dark
  68. statics.s_code_cells[label_id] = CodeAndResult(snippet_data, _NoResult(), "")
  69. code_and_result = statics.s_code_cells[label_id]
  70. runnnable_code_cell_result = RunnableCodeCellResult()
  71. with imgui_ctx.push_id(label_id):
  72. original_cur_pos = imgui.get_cursor_screen_pos()
  73. visible_label = label_id if "##" not in label_id else label_id.split("##")[0]
  74. imgui.separator_text(visible_label)
  75. # Add "Run" button at the end of the separator line
  76. cur_pos_after_separator = imgui.get_cursor_screen_pos()
  77. x_end = imgui.get_item_rect_max().x
  78. x_end = x_end - hello_imgui.em_size(3.5)
  79. imgui.set_cursor_screen_pos(ImVec2(x_end, original_cur_pos.y))
  80. if imgui.button("Run"):
  81. code_and_result.result = _execute_and_capture_last_expr(code_and_result.snippet_data.code)
  82. code_and_result.last_ran_code = code_and_result.snippet_data.code
  83. runnnable_code_cell_result.was_just_run = True
  84. if code_and_result.last_ran_code != code_and_result.snippet_data.code:
  85. imgui.same_line()
  86. imgui.text("*")
  87. imgui.set_cursor_screen_pos(cur_pos_after_separator)
  88. immapp.snippets.show_editable_code_snippet(label_id, code_and_result.snippet_data)
  89. runnnable_code_cell_result.code = code_and_result.snippet_data.code
  90. runnnable_code_cell_result.last_run_code = code_and_result.last_ran_code
  91. runnnable_code_cell_result.result = code_and_result.result
  92. runnnable_code_cell_result.was_last_edited_code_ran = code_and_result.last_ran_code == code_and_result.snippet_data.code
  93. if not isinstance(code_and_result.result, _NoResult):
  94. if result_renderer is None:
  95. result_renderer = _default_result_renderer
  96. result_renderer(code_and_result.result)
  97. imgui.new_line()
  98. return runnnable_code_cell_result