-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender.py
More file actions
170 lines (140 loc) · 4.76 KB
/
render.py
File metadata and controls
170 lines (140 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# render.py (render the code, stack and stdout onto the terminal using Rich)
from rich.console import Console, Group
from rich.layout import Layout
from rich.panel import Panel
from rich.syntax import Syntax
from rich.live import Live
from rich.text import Text
from rich.align import Align
import os
import types
import sys
layout = None
live = None
user_program_path = None
output = ""
console = Console(file=sys.__stdout__) # force Rich to render into original stdout so it doesn't conflict with stdout_buffer (io.StringIO() from stack_tracer.py)
# called by stack_tracer.py to render stuff to terminal
def start_ui(program_path):
global layout, live, user_program_path
user_program_path = program_path
layout = make_layout()
# initialize our panels so they don't start empty
layout["left"].update(make_source_panel(user_program_path, 1))
layout["right"].update(Panel("Waiting for first frame...", title="Stack"))
layout["bottom"].update(make_output_panel(""))
live = Live(layout, console=console, screen=True)
live.start()
def update_ui(frame, stdout_buf=""):
global layout, user_program_path
# get latest frame data and line of code we're on
lineno = frame.f_lineno
# re-paint our layouts
layout["left"].update(
make_source_panel(user_program_path, lineno)
)
layout["right"].update(
make_stack_panel(frame)
)
layout["bottom"].update(make_output_panel(stdout_buf))
if live:
live.refresh()
def stop_ui():
global live
if live:
live.stop()
# =====================================================================
# HELPER FUNCTIONS
def make_layout(): # create our screen layout into 3 chunks
layout = Layout() # the whole screen
# Split into top and bottom
layout.split_column(
Layout(name="top", ratio=4),
Layout(name="bottom", ratio=1)
)
# Split top into left and right
layout["top"].split_row(
Layout(name="left"),
Layout(name="right")
)
return layout
# left chunk: display user's program code
def make_source_panel(filename: str, curr_line: int):
code = open(filename).read()
syntax = Syntax(
code,
"python",
line_numbers=True,
highlight_lines={curr_line},
)
return Panel(syntax, title="Source Code", border_style="cyan")
# right chunk: display the call stack
def make_stack_panel(frame):
current = frame
stack = []
top_label = Align.center(Text("▼ TOP ▼", style="bold red dim"))
bottom_label = Align.center(Text("▲ BOTTOM ▲", style="bold green dim"))
# collect stack frames
while current:
filename = os.path.abspath(current.f_code.co_filename)
if filename == user_program_path:
stack.append(current)
current = current.f_back
panels = []
for index, stack_frame in enumerate(stack):
is_top = (index == 0)
panels.append(make_frame_panel(stack_frame, is_top))
if not panels:
return Panel("No stack frames", title="Call Stack")
content = Group(
top_label,
Group(*panels),
bottom_label
)
return Panel(
content,
title="Call Stack",
border_style="white",
# expand=True
)
def make_frame_panel(frame, is_top=False):
func_name = frame.f_code.co_name
lineno = frame.f_lineno
items = []
# header
items.append(Text(f"{func_name} (line {lineno})", style="bold cyan"))
arg_count = frame.f_code.co_argcount
arg_names = frame.f_code.co_varnames[:arg_count]
# ARGS
if arg_names:
items.append(Text("ARGS:", style="bold magenta"))
for name in arg_names:
if name in frame.f_locals:
val = frame.f_locals[name]
items.append(Text(f" {name} = {repr(val)}"))
# LOCALS
items.append(Text("LOCALS:", style="bold yellow"))
for name, val in reversed(list(frame.f_locals.items())):
if name.startswith("__") and name.endswith("__"):
continue
if name in arg_names:
continue
if isinstance(val, types.ModuleType):
continue
if callable(val):
continue
items.append(Text(f" {name} = {repr(val)}"))
border = "red" if is_top else "bright_black"
title = "ACTIVE FRAME" if is_top else "Frame"
return Panel(
Group(*items),
title=title,
border_style=border,
expand=True # s.t. panel takes up full width of stack
)
MAX_LINES = 5
# bottom chunk: display stdout
def make_output_panel(output_text: str):
lines = output_text.splitlines() # list of lines from stdout
visible = lines[-MAX_LINES:] # print just the latest 5 lines of output
return Panel("\n".join(visible), title="Program Output", border_style="yellow")