jupyter-python-tutor 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/PKG-INFO +12 -11
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/README.md +11 -10
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor/__init__.py +1 -1
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor/magics.py +24 -10
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor/renderer.py +229 -34
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor/tracer.py +68 -4
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/PKG-INFO +12 -11
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/pyproject.toml +1 -1
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/tests/test_magics.py +11 -11
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/tests/test_tracer.py +1 -1
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/LICENSE +0 -0
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/SOURCES.txt +0 -0
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/dependency_links.txt +0 -0
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/requires.txt +0 -0
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/top_level.txt +0 -0
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/setup.cfg +0 -0
- {jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/tests/test_renderer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jupyter-python-tutor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python Tutor-style step-by-step code visualization for JupyterLab 4.x and Notebook 7
|
|
5
5
|
Author: dive4dec
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,10 +34,10 @@ In a Jupyter cell:
|
|
|
34
34
|
%load_ext jupyter_python_tutor
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Then use the `%%
|
|
37
|
+
Then use the `%%pytutor` cell magic:
|
|
38
38
|
|
|
39
39
|
```python
|
|
40
|
-
%%
|
|
40
|
+
%%pytutor
|
|
41
41
|
x = 3
|
|
42
42
|
y = x + 4
|
|
43
43
|
print(y)
|
|
@@ -66,9 +66,10 @@ from frame variables to heap objects (like [Python Tutor](https://pythontutor.co
|
|
|
66
66
|
## Options
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
%%
|
|
70
|
-
%%
|
|
71
|
-
%%
|
|
69
|
+
%%pytutor --max-steps 1000 # Limit trace steps (default: 5000)
|
|
70
|
+
%%pytutor --raw # Output trace as JSON instead of HTML
|
|
71
|
+
%%pytutor --height 400 # Set minimum widget height (default: 500)
|
|
72
|
+
%%pytutor --input 2026 # Pre-collected input for input() calls
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
## Examples
|
|
@@ -76,7 +77,7 @@ from frame variables to heap objects (like [Python Tutor](https://pythontutor.co
|
|
|
76
77
|
### Loops
|
|
77
78
|
|
|
78
79
|
```python
|
|
79
|
-
%%
|
|
80
|
+
%%pytutor
|
|
80
81
|
total = 0
|
|
81
82
|
for i in range(5):
|
|
82
83
|
total += i
|
|
@@ -86,7 +87,7 @@ print(f"Sum: {total}")
|
|
|
86
87
|
### Functions with recursion
|
|
87
88
|
|
|
88
89
|
```python
|
|
89
|
-
%%
|
|
90
|
+
%%pytutor
|
|
90
91
|
def factorial(n):
|
|
91
92
|
if n <= 1:
|
|
92
93
|
return 1
|
|
@@ -99,7 +100,7 @@ print(result)
|
|
|
99
100
|
### Classes and inheritance
|
|
100
101
|
|
|
101
102
|
```python
|
|
102
|
-
%%
|
|
103
|
+
%%pytutor
|
|
103
104
|
class Animal:
|
|
104
105
|
def __init__(self, name):
|
|
105
106
|
self.name = name
|
|
@@ -116,7 +117,7 @@ print(d.speak())
|
|
|
116
117
|
|
|
117
118
|
## How It Works
|
|
118
119
|
|
|
119
|
-
1. **Tracing**: The `%%
|
|
120
|
+
1. **Tracing**: The `%%pytutor` magic compiles the cell code and executes it under
|
|
120
121
|
`sys.settrace()`. At each line/call/return event, a snapshot of all local and
|
|
121
122
|
global variables is captured. AST analysis (`ast.parse`) extracts the set of
|
|
122
123
|
names the traced code touches, so only code-relevant variables are shown
|
|
@@ -136,7 +137,7 @@ print(d.speak())
|
|
|
136
137
|
4. **JupyterLab 4 compatibility**: JupyterLab 4's HTML sanitizer (DOMPurify)
|
|
137
138
|
strips JavaScript and external URLs from untrusted notebooks. The iframe
|
|
138
139
|
`srcdoc` approach bypasses this entirely in trusted notebooks. Run
|
|
139
|
-
`jupyter trust your_notebook.ipynb` after executing cells with `%%
|
|
140
|
+
`jupyter trust your_notebook.ipynb` after executing cells with `%%pytutor`
|
|
140
141
|
to ensure the visualization renders correctly.
|
|
141
142
|
|
|
142
143
|
## Limitations
|
|
@@ -14,10 +14,10 @@ In a Jupyter cell:
|
|
|
14
14
|
%load_ext jupyter_python_tutor
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Then use the `%%
|
|
17
|
+
Then use the `%%pytutor` cell magic:
|
|
18
18
|
|
|
19
19
|
```python
|
|
20
|
-
%%
|
|
20
|
+
%%pytutor
|
|
21
21
|
x = 3
|
|
22
22
|
y = x + 4
|
|
23
23
|
print(y)
|
|
@@ -46,9 +46,10 @@ from frame variables to heap objects (like [Python Tutor](https://pythontutor.co
|
|
|
46
46
|
## Options
|
|
47
47
|
|
|
48
48
|
```
|
|
49
|
-
%%
|
|
50
|
-
%%
|
|
51
|
-
%%
|
|
49
|
+
%%pytutor --max-steps 1000 # Limit trace steps (default: 5000)
|
|
50
|
+
%%pytutor --raw # Output trace as JSON instead of HTML
|
|
51
|
+
%%pytutor --height 400 # Set minimum widget height (default: 500)
|
|
52
|
+
%%pytutor --input 2026 # Pre-collected input for input() calls
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
## Examples
|
|
@@ -56,7 +57,7 @@ from frame variables to heap objects (like [Python Tutor](https://pythontutor.co
|
|
|
56
57
|
### Loops
|
|
57
58
|
|
|
58
59
|
```python
|
|
59
|
-
%%
|
|
60
|
+
%%pytutor
|
|
60
61
|
total = 0
|
|
61
62
|
for i in range(5):
|
|
62
63
|
total += i
|
|
@@ -66,7 +67,7 @@ print(f"Sum: {total}")
|
|
|
66
67
|
### Functions with recursion
|
|
67
68
|
|
|
68
69
|
```python
|
|
69
|
-
%%
|
|
70
|
+
%%pytutor
|
|
70
71
|
def factorial(n):
|
|
71
72
|
if n <= 1:
|
|
72
73
|
return 1
|
|
@@ -79,7 +80,7 @@ print(result)
|
|
|
79
80
|
### Classes and inheritance
|
|
80
81
|
|
|
81
82
|
```python
|
|
82
|
-
%%
|
|
83
|
+
%%pytutor
|
|
83
84
|
class Animal:
|
|
84
85
|
def __init__(self, name):
|
|
85
86
|
self.name = name
|
|
@@ -96,7 +97,7 @@ print(d.speak())
|
|
|
96
97
|
|
|
97
98
|
## How It Works
|
|
98
99
|
|
|
99
|
-
1. **Tracing**: The `%%
|
|
100
|
+
1. **Tracing**: The `%%pytutor` magic compiles the cell code and executes it under
|
|
100
101
|
`sys.settrace()`. At each line/call/return event, a snapshot of all local and
|
|
101
102
|
global variables is captured. AST analysis (`ast.parse`) extracts the set of
|
|
102
103
|
names the traced code touches, so only code-relevant variables are shown
|
|
@@ -116,7 +117,7 @@ print(d.speak())
|
|
|
116
117
|
4. **JupyterLab 4 compatibility**: JupyterLab 4's HTML sanitizer (DOMPurify)
|
|
117
118
|
strips JavaScript and external URLs from untrusted notebooks. The iframe
|
|
118
119
|
`srcdoc` approach bypasses this entirely in trusted notebooks. Run
|
|
119
|
-
`jupyter trust your_notebook.ipynb` after executing cells with `%%
|
|
120
|
+
`jupyter trust your_notebook.ipynb` after executing cells with `%%pytutor`
|
|
120
121
|
to ensure the visualization renders correctly.
|
|
121
122
|
|
|
122
123
|
## Limitations
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Usage in a Jupyter cell:
|
|
4
4
|
|
|
5
|
-
%%
|
|
5
|
+
%%pytutor
|
|
6
6
|
x = 3
|
|
7
7
|
y = x + 4
|
|
8
8
|
print(y)
|
|
9
9
|
|
|
10
10
|
Options:
|
|
11
|
-
%%
|
|
12
|
-
%%
|
|
13
|
-
%%
|
|
11
|
+
%%pytutor --max-steps 1000 — limit the number of trace steps
|
|
12
|
+
%%pytutor --raw — output the trace as JSON instead of HTML
|
|
13
|
+
%%pytutor --height 400 — set max-height of the variables panel (px)
|
|
14
|
+
%%pytutor --input 2026 — pre-collected input for input() calls
|
|
14
15
|
"""
|
|
15
16
|
from __future__ import annotations
|
|
16
17
|
|
|
@@ -28,7 +29,7 @@ __all__ = ["TutorMagics", "load_ipython_extension", "unload_ipython_extension"]
|
|
|
28
29
|
|
|
29
30
|
def _make_parser() -> argparse.ArgumentParser:
|
|
30
31
|
parser = argparse.ArgumentParser(
|
|
31
|
-
prog="%%
|
|
32
|
+
prog="%%pytutor",
|
|
32
33
|
description="Trace Python code and display an interactive visualization.",
|
|
33
34
|
)
|
|
34
35
|
parser.add_argument(
|
|
@@ -43,15 +44,21 @@ def _make_parser() -> argparse.ArgumentParser:
|
|
|
43
44
|
"--height", type=int, default=300,
|
|
44
45
|
help="Max height of the variables panel in pixels (default: 300)",
|
|
45
46
|
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--input", action="append", default=[], metavar="VALUE",
|
|
49
|
+
help="Pre-collected input for input() calls. "
|
|
50
|
+
"Repeat --input for multiple values: --input 2026 --input John. "
|
|
51
|
+
"If omitted and code contains input(), a dialog prompts for values.",
|
|
52
|
+
)
|
|
46
53
|
return parser
|
|
47
54
|
|
|
48
55
|
|
|
49
56
|
@magics_class
|
|
50
57
|
class TutorMagics(Magics):
|
|
51
|
-
"""Register the %%
|
|
58
|
+
"""Register the %%pytutor cell magic."""
|
|
52
59
|
|
|
53
|
-
@cell_magic("
|
|
54
|
-
def
|
|
60
|
+
@cell_magic("pytutor")
|
|
61
|
+
def pytutor(self, line: str, cell: str):
|
|
55
62
|
"""Trace the cell code and display an interactive visualization."""
|
|
56
63
|
parser = _make_parser()
|
|
57
64
|
try:
|
|
@@ -67,10 +74,17 @@ class TutorMagics(Magics):
|
|
|
67
74
|
# debugger operates.
|
|
68
75
|
user_ns = self.shell.user_ns
|
|
69
76
|
|
|
77
|
+
# Pre-collected inputs for input() calls.
|
|
78
|
+
# If --input is provided, use those values.
|
|
79
|
+
# If not provided but code contains input(), use empty list
|
|
80
|
+
# (input() returns empty string instead of blocking).
|
|
81
|
+
inputs = args.input if args.input else None
|
|
82
|
+
|
|
70
83
|
steps = trace_code(
|
|
71
84
|
cell,
|
|
72
85
|
max_steps=args.max_steps,
|
|
73
86
|
user_globals=user_ns,
|
|
87
|
+
inputs=inputs,
|
|
74
88
|
)
|
|
75
89
|
|
|
76
90
|
if args.raw:
|
|
@@ -105,5 +119,5 @@ def load_ipython_extension(ipython):
|
|
|
105
119
|
|
|
106
120
|
def unload_ipython_extension(ipython):
|
|
107
121
|
"""Unload the extension."""
|
|
108
|
-
if "
|
|
109
|
-
ipython.magics_manager.magics["cell"].pop("
|
|
122
|
+
if "pytutor" in ipython.magics_manager.magics["cell"]:
|
|
123
|
+
ipython.magics_manager.magics["cell"].pop("pytutor", None)
|
|
@@ -68,13 +68,16 @@ def _generate_inner_html(steps: list[dict], source_code: str, height: int = 500)
|
|
|
68
68
|
|
|
69
69
|
# Prepare step data as JSON — strip any non-serializable fields
|
|
70
70
|
clean_steps = []
|
|
71
|
-
for step in steps:
|
|
71
|
+
for i, step in enumerate(steps):
|
|
72
72
|
clean_step = {
|
|
73
73
|
"line": step.get("line", 0),
|
|
74
74
|
"stdout": step.get("stdout", ""),
|
|
75
75
|
"call_stack": [],
|
|
76
76
|
"return_value": step.get("return_value"),
|
|
77
77
|
"event": step.get("event", ""),
|
|
78
|
+
"input_prompt": step.get("input_prompt", ""),
|
|
79
|
+
"input_index": step.get("input_index", -1),
|
|
80
|
+
"prev_line": steps[i - 1].get("line", 0) if i > 0 else 0,
|
|
78
81
|
}
|
|
79
82
|
for frame_info in step.get("call_stack", []):
|
|
80
83
|
clean_frame = {
|
|
@@ -161,19 +164,19 @@ body {{
|
|
|
161
164
|
/* Print output */
|
|
162
165
|
.jpt-output-section {{
|
|
163
166
|
border-bottom: 1px solid #d4d4d4;
|
|
164
|
-
background: #
|
|
167
|
+
background: #dcfce7;
|
|
165
168
|
flex-shrink: 0;
|
|
166
169
|
}}
|
|
167
170
|
.jpt-output-label {{
|
|
168
171
|
font-size: 10px;
|
|
169
|
-
color: #
|
|
172
|
+
color: #166534;
|
|
170
173
|
padding: 3px 10px 1px;
|
|
171
174
|
font-weight: 600;
|
|
172
175
|
text-transform: uppercase;
|
|
173
176
|
}}
|
|
174
177
|
.jpt-stdout {{
|
|
175
|
-
background: #
|
|
176
|
-
color: #
|
|
178
|
+
background: #f0fdf4;
|
|
179
|
+
color: #166534;
|
|
177
180
|
font-family: 'DejaVu Sans Mono', monospace;
|
|
178
181
|
font-size: 11px;
|
|
179
182
|
padding: 4px 10px;
|
|
@@ -182,7 +185,7 @@ body {{
|
|
|
182
185
|
white-space: pre-wrap;
|
|
183
186
|
max-height: 80px;
|
|
184
187
|
overflow-y: auto;
|
|
185
|
-
min-height:
|
|
188
|
+
min-height: 20px;
|
|
186
189
|
}}
|
|
187
190
|
/* Main area */
|
|
188
191
|
.jpt-main {{
|
|
@@ -242,20 +245,31 @@ body {{
|
|
|
242
245
|
min-height: 18px;
|
|
243
246
|
border-left: 3px solid transparent;
|
|
244
247
|
}}
|
|
248
|
+
.jpt-code-line.jpt-executed {{
|
|
249
|
+
background: #dcfce7;
|
|
250
|
+
border-left: 3px solid #22c55e;
|
|
251
|
+
}}
|
|
245
252
|
.jpt-code-line.jpt-current {{
|
|
246
|
-
background: #
|
|
247
|
-
border-left: 3px solid #
|
|
253
|
+
background: #fce7f3;
|
|
254
|
+
border-left: 3px solid #e10c65;
|
|
248
255
|
}}
|
|
249
256
|
.jpt-arrow {{
|
|
250
257
|
width: 16px; flex: 0 0 16px; text-align: center;
|
|
251
258
|
color: transparent; font-weight: bold; user-select: none;
|
|
252
259
|
}}
|
|
253
|
-
.jpt-current .jpt-arrow {{ color: #
|
|
260
|
+
.jpt-current .jpt-arrow {{ color: #e10c65; }}
|
|
254
261
|
.jpt-lineno {{
|
|
255
262
|
width: 28px; flex: 0 0 28px; text-align: right;
|
|
256
263
|
padding-right: 8px; color: #6b7280; font-size: 11px; user-select: none;
|
|
257
264
|
}}
|
|
258
265
|
.jpt-code-text {{ flex: 1; padding-left: 4px; }}
|
|
266
|
+
/* Python syntax highlighting */
|
|
267
|
+
.jpt-tok-kw {{ color: #c678dd; font-weight: 600; }} /* keywords: def, if, for... */
|
|
268
|
+
.jpt-tok-str {{ color: #98c379; }} /* strings */
|
|
269
|
+
.jpt-tok-num {{ color: #d19a66; }} /* numbers */
|
|
270
|
+
.jpt-tok-com {{ color: #7c7c7c; font-style: italic; }} /* comments */
|
|
271
|
+
.jpt-tok-fn {{ color: #61afef; }} /* function names */
|
|
272
|
+
.jpt-tok-builtin {{ color: #56b6c2; }} /* builtins: print, len... */
|
|
259
273
|
/* Frames */
|
|
260
274
|
.jpt-frame {{
|
|
261
275
|
border: 1px solid #bae6fd; border-radius: 4px; background: #f0f9ff;
|
|
@@ -288,8 +302,7 @@ body {{
|
|
|
288
302
|
cursor: pointer; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
289
303
|
white-space: nowrap; position: relative;
|
|
290
304
|
}}
|
|
291
|
-
.jpt-pointer-arrow {{ color: #4f46e5; font-weight: bold;
|
|
292
|
-
.jpt-pointer-label {{ color: #3730a3; overflow: hidden; text-overflow: ellipsis; }}
|
|
305
|
+
.jpt-pointer-arrow {{ color: #4f46e5; font-weight: bold; }}
|
|
293
306
|
.jpt-frame.jpt-global {{
|
|
294
307
|
border-color: #fde047; background: #fefce8;
|
|
295
308
|
}}
|
|
@@ -336,10 +349,50 @@ body {{
|
|
|
336
349
|
}}
|
|
337
350
|
.jpt-arrow-path {{ stroke: #6b7280; stroke-width: 1.5; fill: none; marker-end: url(#arrowhead); }}
|
|
338
351
|
.jpt-arrow-path-highlight {{ stroke: #3b82f6; stroke-width: 2; }}
|
|
352
|
+
/* Input dialog overlay */
|
|
353
|
+
.jpt-input-overlay {{
|
|
354
|
+
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
355
|
+
background: rgba(0,0,0,0.3); z-index: 20;
|
|
356
|
+
display: flex; align-items: center; justify-content: center;
|
|
357
|
+
}}
|
|
358
|
+
.jpt-input-dialog {{
|
|
359
|
+
background: #fff; border-radius: 8px; padding: 16px 20px;
|
|
360
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.15); max-width: 420px; width: 90%;
|
|
361
|
+
font-size: 13px;
|
|
362
|
+
}}
|
|
363
|
+
.jpt-input-dialog-label {{
|
|
364
|
+
font-weight: 600; color: #1f2937; margin-bottom: 8px;
|
|
365
|
+
font-family: 'DejaVu Sans Mono', monospace; font-size: 12px;
|
|
366
|
+
}}
|
|
367
|
+
.jpt-input-dialog-input {{
|
|
368
|
+
width: 100%; padding: 8px 10px; border: 1px solid #d4d4d4;
|
|
369
|
+
border-radius: 4px; font-size: 13px; font-family: 'DejaVu Sans Mono', monospace;
|
|
370
|
+
box-sizing: border-box;
|
|
371
|
+
}}
|
|
372
|
+
.jpt-input-dialog-input:focus {{ outline: none; border-color: #3b82f6; }}
|
|
373
|
+
.jpt-input-dialog-btn {{
|
|
374
|
+
margin-top: 10px; padding: 6px 16px; background: #3b82f6; color: #fff;
|
|
375
|
+
border: none; border-radius: 4px; font-size: 13px; cursor: pointer;
|
|
376
|
+
font-weight: 500;
|
|
377
|
+
}}
|
|
378
|
+
.jpt-input-dialog-btn:hover {{ background: #2563eb; }}
|
|
379
|
+
.jpt-input-badge {{
|
|
380
|
+
display: inline-block; background: #fef3c7; color: #92400e;
|
|
381
|
+
padding: 1px 6px; border-radius: 3px; font-size: 11px;
|
|
382
|
+
font-family: 'DejaVu Sans Mono', monospace; margin-top: 2px;
|
|
383
|
+
}}
|
|
339
384
|
</style>
|
|
340
385
|
</head>
|
|
341
386
|
<body>
|
|
342
387
|
<div class="jpt-widget" id="jptWidget">
|
|
388
|
+
<!-- Input dialog overlay (hidden by default) -->
|
|
389
|
+
<div class="jpt-input-overlay" id="inputOverlay" style="display:none;">
|
|
390
|
+
<div class="jpt-input-dialog">
|
|
391
|
+
<div class="jpt-input-dialog-label" id="inputDialogLabel">Enter input:</div>
|
|
392
|
+
<input type="text" class="jpt-input-dialog-input" id="inputDialogInput" autofocus />
|
|
393
|
+
<button class="jpt-input-dialog-btn" id="inputDialogBtn">Submit</button>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
343
396
|
<div class="jpt-navbar">
|
|
344
397
|
<button id="btnFirst" title="First">⏮</button>
|
|
345
398
|
<button id="btnPrev" title="Previous">◀</button>
|
|
@@ -351,6 +404,7 @@ body {{
|
|
|
351
404
|
<div class="jpt-output-section">
|
|
352
405
|
<div class="jpt-output-label">Program Output</div>
|
|
353
406
|
<div class="jpt-stdout" id="stdout"></div>
|
|
407
|
+
<div class="jpt-input-badge" id="inputBadge" style="display:none;"></div>
|
|
354
408
|
</div>
|
|
355
409
|
<div class="jpt-main">
|
|
356
410
|
<div class="jpt-code-section" id="codeSection">
|
|
@@ -404,18 +458,125 @@ body {{
|
|
|
404
458
|
return ref.name || 'func';
|
|
405
459
|
}}
|
|
406
460
|
|
|
407
|
-
|
|
461
|
+
// ─── Python syntax highlighting ──────────────────────────────────
|
|
462
|
+
var PY_KEYWORDS = ['def','return','if','elif','else','for','while','break','continue',
|
|
463
|
+
'import','from','as','class','try','except','finally','with','lambda','yield',
|
|
464
|
+
'global','nonlocal','pass','del','raise','assert','in','not','and','or','is','None',
|
|
465
|
+
'True','False','async','await'];
|
|
466
|
+
var PY_BUILTINS = ['print','len','range','input','int','float','str','list','dict',
|
|
467
|
+
'set','tuple','bool','type','isinstance','enumerate','zip','map','filter','sorted',
|
|
468
|
+
'sum','min','max','abs','round','open','format','repr','chr','ord','hex','oct','bin',
|
|
469
|
+
'super','property','staticmethod','classmethod','iter','next','reversed','any','all',
|
|
470
|
+
'getattr','setattr','hasattr','delattr','id','hash','dir','vars','globals','locals'];
|
|
471
|
+
var kwSet = {{}}; PY_KEYWORDS.forEach(function(k){{ kwSet[k] = 1; }});
|
|
472
|
+
var builtinSet = {{}}; PY_BUILTINS.forEach(function(b){{ builtinSet[b] = 1; }});
|
|
473
|
+
|
|
474
|
+
function highlightPython(line) {{
|
|
475
|
+
// Tokenize: comments, strings, numbers, identifiers, operators
|
|
476
|
+
var result = '';
|
|
477
|
+
var i = 0;
|
|
478
|
+
while (i < line.length) {{
|
|
479
|
+
var ch = line[i];
|
|
480
|
+
// Comment
|
|
481
|
+
if (ch === '#') {{
|
|
482
|
+
result += '<span class="jpt-tok-com">' + escapeHtml(line.substring(i)) + '</span>';
|
|
483
|
+
break;
|
|
484
|
+
}}
|
|
485
|
+
// String (single, double, triple)
|
|
486
|
+
if (ch === '"' || ch === "'") {{
|
|
487
|
+
var quote = ch;
|
|
488
|
+
// Check triple-quote
|
|
489
|
+
if (line.substring(i, i+3) === quote+quote+quote) {{
|
|
490
|
+
var end = line.indexOf(quote+quote+quote, i+3);
|
|
491
|
+
if (end < 0) end = line.length - 3;
|
|
492
|
+
var s = line.substring(i, end+3);
|
|
493
|
+
result += '<span class="jpt-tok-str">' + escapeHtml(s) + '</span>';
|
|
494
|
+
i = end + 3;
|
|
495
|
+
continue;
|
|
496
|
+
}}
|
|
497
|
+
// Single/double quote — find matching close (handle escapes)
|
|
498
|
+
var j = i + 1;
|
|
499
|
+
while (j < line.length) {{
|
|
500
|
+
if (line[j] === '\\\\') {{ j += 2; continue; }}
|
|
501
|
+
if (line[j] === quote) {{ j++; break; }}
|
|
502
|
+
j++;
|
|
503
|
+
}}
|
|
504
|
+
var s = line.substring(i, j);
|
|
505
|
+
result += '<span class="jpt-tok-str">' + escapeHtml(s) + '</span>';
|
|
506
|
+
i = j;
|
|
507
|
+
continue;
|
|
508
|
+
}}
|
|
509
|
+
// f-string prefix (f, r, b, fr, rf, etc. before quote)
|
|
510
|
+
if (/[frbFRB]/.test(ch) && i+1 < line.length && (line[i+1] === '"' || line[i+1] === "'")) {{
|
|
511
|
+
var j = i;
|
|
512
|
+
while (j < line.length && /[frbFRB]/.test(line[j])) j++;
|
|
513
|
+
if (j < line.length && (line[j] === '"' || line[j] === "'")) {{
|
|
514
|
+
var quote = line[j];
|
|
515
|
+
var k = j + 1;
|
|
516
|
+
while (k < line.length) {{
|
|
517
|
+
if (line[k] === '\\\\') {{ k += 2; continue; }}
|
|
518
|
+
if (line[k] === quote) {{ k++; break; }}
|
|
519
|
+
k++;
|
|
520
|
+
}}
|
|
521
|
+
var s = line.substring(i, k);
|
|
522
|
+
result += '<span class="jpt-tok-str">' + escapeHtml(s) + '</span>';
|
|
523
|
+
i = k;
|
|
524
|
+
continue;
|
|
525
|
+
}}
|
|
526
|
+
}}
|
|
527
|
+
// Number
|
|
528
|
+
if (/[0-9]/.test(ch) || (ch === '.' && i+1 < line.length && /[0-9]/.test(line[i+1]))) {{
|
|
529
|
+
var j = i;
|
|
530
|
+
while (j < line.length && /[0-9._xXeE+\-a-fA-F]/.test(line[j])) {{
|
|
531
|
+
// stop on +/- if not part of exponent
|
|
532
|
+
if ((line[j] === '+' || line[j] === '-') && !/[eE]/.test(line[j-1])) break;
|
|
533
|
+
j++;
|
|
534
|
+
}}
|
|
535
|
+
result += '<span class="jpt-tok-num">' + escapeHtml(line.substring(i, j)) + '</span>';
|
|
536
|
+
i = j;
|
|
537
|
+
continue;
|
|
538
|
+
}}
|
|
539
|
+
// Identifier / keyword / builtin
|
|
540
|
+
if (/[a-zA-Z_]/.test(ch)) {{
|
|
541
|
+
var j = i;
|
|
542
|
+
while (j < line.length && /[a-zA-Z0-9_]/.test(line[j])) j++;
|
|
543
|
+
var word = line.substring(i, j);
|
|
544
|
+
// Check if followed by ( → function call
|
|
545
|
+
var afterWord = line.substring(j).trim();
|
|
546
|
+
if (kwSet[word]) {{
|
|
547
|
+
result += '<span class="jpt-tok-kw">' + escapeHtml(word) + '</span>';
|
|
548
|
+
}} else if (builtinSet[word]) {{
|
|
549
|
+
result += '<span class="jpt-tok-builtin">' + escapeHtml(word) + '</span>';
|
|
550
|
+
}} else if (afterWord.charAt(0) === '(') {{
|
|
551
|
+
result += '<span class="jpt-tok-fn">' + escapeHtml(word) + '</span>';
|
|
552
|
+
}} else {{
|
|
553
|
+
result += escapeHtml(word);
|
|
554
|
+
}}
|
|
555
|
+
i = j;
|
|
556
|
+
continue;
|
|
557
|
+
}}
|
|
558
|
+
// Default: escape and output
|
|
559
|
+
result += escapeHtml(ch);
|
|
560
|
+
i++;
|
|
561
|
+
}}
|
|
562
|
+
return result;
|
|
563
|
+
}}
|
|
564
|
+
|
|
565
|
+
function renderCode(lineNo, prevLineNo) {{
|
|
408
566
|
if (!lineNo || lineNo < 1) lineNo = 1;
|
|
567
|
+
if (!prevLineNo) prevLineNo = 0;
|
|
409
568
|
var html = '';
|
|
410
569
|
for (var i = 0; i < sourceLines.length; i++) {{
|
|
411
570
|
var ln = i + 1;
|
|
412
571
|
var line = sourceLines[i] || '';
|
|
413
|
-
var
|
|
414
|
-
var
|
|
415
|
-
|
|
572
|
+
var highlighted = line.trim() ? highlightPython(line) : ' ';
|
|
573
|
+
var cls = '';
|
|
574
|
+
if (ln === lineNo) cls = ' jpt-current';
|
|
575
|
+
else if (ln === prevLineNo) cls = ' jpt-executed';
|
|
576
|
+
html += '<div class="jpt-code-line' + cls + '">' +
|
|
416
577
|
'<span class="jpt-arrow">▶</span>' +
|
|
417
578
|
'<span class="jpt-lineno">' + ln + '</span>' +
|
|
418
|
-
'<span class="jpt-code-text">' +
|
|
579
|
+
'<span class="jpt-code-text">' + highlighted + '</span>' +
|
|
419
580
|
'</div>';
|
|
420
581
|
}}
|
|
421
582
|
codeDisplay.innerHTML = html;
|
|
@@ -450,24 +611,10 @@ body {{
|
|
|
450
611
|
for (var ri = 0; ri < refs.length; ri++) {{
|
|
451
612
|
var ref = refs[ri];
|
|
452
613
|
var heapId = escapeHtml(ref.heap_id || '');
|
|
453
|
-
var typeLabel;
|
|
454
|
-
if (ref.is_function) {{
|
|
455
|
-
typeLabel = 'func ' + escapeHtml(funcDisplayName(ref));
|
|
456
|
-
}} else if (ref.is_class) {{
|
|
457
|
-
typeLabel = 'class ' + escapeHtml(ref.name);
|
|
458
|
-
}} else if (ref.is_module) {{
|
|
459
|
-
typeLabel = 'module ' + escapeHtml(ref.name);
|
|
460
|
-
}} else if (ref.is_instance) {{
|
|
461
|
-
typeLabel = escapeHtml(ref.type || 'instance');
|
|
462
|
-
}} else {{
|
|
463
|
-
typeLabel = escapeHtml(ref.type || 'object');
|
|
464
|
-
}}
|
|
465
614
|
html += '<div class="jpt-var-row">' +
|
|
466
615
|
'<span class="jpt-var-name">' + escapeHtml(ref.name) + '</span>' +
|
|
467
616
|
'<span class="jpt-var-eq">=</span>' +
|
|
468
|
-
'<span class="jpt-var-pointer" data-heap-id="' + heapId + '" title="' + escapeHtml(ref.repr) + '">'
|
|
469
|
-
'<span class="jpt-pointer-arrow">→</span>' +
|
|
470
|
-
'<span class="jpt-pointer-label">' + typeLabel + '</span></span></div>';
|
|
617
|
+
'<span class="jpt-var-pointer" data-heap-id="' + heapId + '" title="' + escapeHtml(ref.repr) + '"><span class="jpt-pointer-arrow">→</span></span></div>';
|
|
471
618
|
}}
|
|
472
619
|
if (keys.length === 0 && refs.length === 0) {{
|
|
473
620
|
html += '<div class="jpt-var-row" style="color:#9ca3af;font-style:italic;">(no variables)</div>';
|
|
@@ -611,17 +758,65 @@ body {{
|
|
|
611
758
|
document.getElementById('jptWidget').style.height = totalH + 'px';
|
|
612
759
|
}}
|
|
613
760
|
|
|
761
|
+
// ─── Input handling ──────────────────────────────────────────────
|
|
762
|
+
// inputsCollected: cached values for each input_index
|
|
763
|
+
// Once collected, navigation back/forth never re-prompts.
|
|
764
|
+
var inputsCollected = {{}}; // input_index → value string
|
|
765
|
+
var inputOverlay = document.getElementById('inputOverlay');
|
|
766
|
+
var inputDialogLabel = document.getElementById('inputDialogLabel');
|
|
767
|
+
var inputDialogInput = document.getElementById('inputDialogInput');
|
|
768
|
+
var inputDialogBtn = document.getElementById('inputDialogBtn');
|
|
769
|
+
|
|
770
|
+
// Pre-populate from step data (inputs already provided during tracing)
|
|
771
|
+
for (var si = 0; si < nSteps; si++) {{
|
|
772
|
+
var ss = steps[si];
|
|
773
|
+
if (ss.input_index >= 0 && ss.input_index !== undefined) {{
|
|
774
|
+
// Extract the input value from stdout: prompt + value + newline
|
|
775
|
+
// The value is what the user typed (echoed after the prompt)
|
|
776
|
+
if (!(ss.input_index in inputsCollected)) {{
|
|
777
|
+
var prompt = ss.input_prompt || '';
|
|
778
|
+
var out = ss.stdout || '';
|
|
779
|
+
// stdout contains prompt + value + newline, find the value
|
|
780
|
+
if (out.includes(prompt)) {{
|
|
781
|
+
var afterPrompt = out.substring(out.indexOf(prompt) + prompt.length);
|
|
782
|
+
// The value is everything up to the first newline
|
|
783
|
+
var nl = afterPrompt.indexOf(String.fromCharCode(10));
|
|
784
|
+
if (nl >= 0) {{
|
|
785
|
+
inputsCollected[ss.input_index] = afterPrompt.substring(0, nl);
|
|
786
|
+
}} else {{
|
|
787
|
+
inputsCollected[ss.input_index] = afterPrompt;
|
|
788
|
+
}}
|
|
789
|
+
}}
|
|
790
|
+
}}
|
|
791
|
+
}}
|
|
792
|
+
}}
|
|
793
|
+
|
|
794
|
+
// Show input badge in output section if this step has input
|
|
795
|
+
function updateInputBadge(step) {{
|
|
796
|
+
var badge = document.getElementById('inputBadge');
|
|
797
|
+
if (!badge) return;
|
|
798
|
+
if (step.input_index >= 0 && step.input_index !== undefined && step.input_prompt) {{
|
|
799
|
+
var val = inputsCollected[step.input_index] || '';
|
|
800
|
+
badge.textContent = 'input() → ' + val;
|
|
801
|
+
badge.style.display = 'inline-block';
|
|
802
|
+
}} else {{
|
|
803
|
+
badge.style.display = 'none';
|
|
804
|
+
}}
|
|
805
|
+
}}
|
|
806
|
+
|
|
614
807
|
function renderStep(idx) {{
|
|
615
808
|
if (idx < 0) idx = 0;
|
|
616
809
|
if (idx >= nSteps) idx = nSteps - 1;
|
|
617
810
|
curStep = idx;
|
|
618
811
|
var step = steps[idx];
|
|
619
|
-
|
|
812
|
+
var prevLine = (idx > 0) ? steps[idx - 1].line : 0;
|
|
813
|
+
renderCode(step.line || 0, prevLine);
|
|
620
814
|
renderFrames(step);
|
|
621
815
|
renderHeap(step);
|
|
622
816
|
stdout.textContent = step.stdout || '';
|
|
623
|
-
if (!step.stdout) stdout.style.color = '#
|
|
624
|
-
else stdout.style.color = '#
|
|
817
|
+
if (!step.stdout) stdout.style.color = '#86efac', stdout.style.fontStyle = 'italic', stdout.textContent = '(no output yet)';
|
|
818
|
+
else stdout.style.color = '#166534', stdout.style.fontStyle = 'normal';
|
|
819
|
+
updateInputBadge(step);
|
|
625
820
|
stepCounter.textContent = 'Step ' + (idx + 1) + ' of ' + nSteps;
|
|
626
821
|
slider.value = idx;
|
|
627
822
|
btnFirst.disabled = (idx === 0);
|
|
@@ -401,6 +401,7 @@ def _trace_code_internal(
|
|
|
401
401
|
code_str: str,
|
|
402
402
|
user_globals: dict[str, typing.Any] | None = None,
|
|
403
403
|
max_steps: int = 5000,
|
|
404
|
+
inputs: list[str] | None = None,
|
|
404
405
|
) -> list[dict]:
|
|
405
406
|
"""Execute *code_str* under a trace and return the list of steps.
|
|
406
407
|
|
|
@@ -411,12 +412,17 @@ def _trace_code_internal(
|
|
|
411
412
|
event: str — "call", "line", or "return"
|
|
412
413
|
func: str — current function name
|
|
413
414
|
stdout: str — output printed up to THIS step (incremental)
|
|
415
|
+
input_prompt: str — prompt text if this step calls input(), else ""
|
|
416
|
+
input_index: int — index into inputs list for this input() call, or -1
|
|
417
|
+
|
|
418
|
+
If *inputs* is provided, input() calls during tracing return successive
|
|
419
|
+
values from the list instead of reading stdin.
|
|
414
420
|
"""
|
|
415
421
|
steps: list[dict] = []
|
|
416
422
|
step_count = 0
|
|
417
423
|
# Execute directly in the user's namespace so that classes, functions,
|
|
418
424
|
# and imports from previous cells are available. This is how a normal
|
|
419
|
-
# Jupyter cell works — the %%
|
|
425
|
+
# Jupyter cell works — the %%pytutor magic should behave the same way,
|
|
420
426
|
# just with tracing enabled. Variables defined by the traced code WILL
|
|
421
427
|
# persist in the kernel (like a normal cell), but we must not overwrite
|
|
422
428
|
# __name__ or __builtins__.
|
|
@@ -474,18 +480,59 @@ def _trace_code_internal(
|
|
|
474
480
|
|
|
475
481
|
tracking_stream = _StepTrackingStream(captured)
|
|
476
482
|
|
|
483
|
+
# ─── input() override ─────────────────────────────────────────────
|
|
484
|
+
# During tracing, input() should return pre-collected values from the
|
|
485
|
+
# *inputs* list instead of prompting stdin. The prompt text is still
|
|
486
|
+
# written to stdout (matching Python's built-in input() behavior).
|
|
487
|
+
# We also record which step triggered input() so the renderer can show
|
|
488
|
+
# an input UI at the right point.
|
|
489
|
+
import builtins
|
|
490
|
+
_real_input = builtins.input
|
|
491
|
+
_input_queue = list(inputs) if inputs else []
|
|
492
|
+
_input_idx = 0 # how many inputs have been consumed
|
|
493
|
+
# Track the input prompt for the CURRENT step being traced
|
|
494
|
+
_current_input_prompt: str | None = None
|
|
495
|
+
_current_input_index: int = -1
|
|
496
|
+
|
|
497
|
+
def _traced_input(prompt: str = "") -> str:
|
|
498
|
+
nonlocal _input_idx, _current_input_prompt, _current_input_index
|
|
499
|
+
# Write prompt to stdout (like real input() does)
|
|
500
|
+
if prompt:
|
|
501
|
+
tracking_stream.write(prompt)
|
|
502
|
+
_current_input_prompt = prompt
|
|
503
|
+
_current_input_index = _input_idx
|
|
504
|
+
if _input_idx < len(_input_queue):
|
|
505
|
+
val = _input_queue[_input_idx]
|
|
506
|
+
else:
|
|
507
|
+
val = "" # empty string if no more inputs
|
|
508
|
+
_input_idx += 1
|
|
509
|
+
# Echo the input value to stdout (like a terminal would)
|
|
510
|
+
tracking_stream.write(val + "\n")
|
|
511
|
+
return val
|
|
512
|
+
|
|
477
513
|
# Shared ID map: maps id(obj) → "id1", "id2", etc.
|
|
478
514
|
# Same object always gets the same label across all steps.
|
|
479
515
|
id_map: dict[int, str] = {}
|
|
480
516
|
|
|
481
517
|
def _trace(frame, event, arg):
|
|
482
|
-
nonlocal step_count
|
|
518
|
+
nonlocal step_count, _current_input_prompt, _current_input_index
|
|
483
519
|
if frame.f_code.co_filename != filename:
|
|
484
520
|
return _trace # skip frames from other files
|
|
485
521
|
if step_count >= max_steps:
|
|
486
522
|
raise _StepLimitExceeded() # stop execution, not just tracing
|
|
487
523
|
step_count += 1
|
|
488
524
|
|
|
525
|
+
# Capture input info from the PREVIOUS step's execution.
|
|
526
|
+
# input() runs during line execution (between trace callbacks),
|
|
527
|
+
# so _current_input_prompt was set by the line that just ran.
|
|
528
|
+
# We record it on THIS step (the next line) so the renderer knows
|
|
529
|
+
# input happened during the previous step.
|
|
530
|
+
step_input_prompt = _current_input_prompt
|
|
531
|
+
step_input_index = _current_input_index
|
|
532
|
+
# Reset for the next execution phase
|
|
533
|
+
_current_input_prompt = None
|
|
534
|
+
_current_input_index = -1
|
|
535
|
+
|
|
489
536
|
lineno = frame.f_lineno
|
|
490
537
|
source = source_lines[lineno - 1].strip() if 1 <= lineno <= len(source_lines) else ""
|
|
491
538
|
call_stack = _snapshot_call_stack(frame, id_map, code_names)
|
|
@@ -499,6 +546,8 @@ def _trace_code_internal(
|
|
|
499
546
|
"event": "line",
|
|
500
547
|
"func": frame.f_code.co_name,
|
|
501
548
|
"stdout": current_stdout,
|
|
549
|
+
"input_prompt": step_input_prompt or "",
|
|
550
|
+
"input_index": step_input_index,
|
|
502
551
|
})
|
|
503
552
|
elif event == "call":
|
|
504
553
|
steps.append({
|
|
@@ -508,6 +557,8 @@ def _trace_code_internal(
|
|
|
508
557
|
"event": "call",
|
|
509
558
|
"func": frame.f_code.co_name,
|
|
510
559
|
"stdout": current_stdout,
|
|
560
|
+
"input_prompt": step_input_prompt or "",
|
|
561
|
+
"input_index": step_input_index,
|
|
511
562
|
})
|
|
512
563
|
elif event == "return":
|
|
513
564
|
ret_repr = _safe_repr(arg) if arg is not None else "None"
|
|
@@ -519,6 +570,8 @@ def _trace_code_internal(
|
|
|
519
570
|
"func": frame.f_code.co_name,
|
|
520
571
|
"return_value": ret_repr,
|
|
521
572
|
"stdout": current_stdout,
|
|
573
|
+
"input_prompt": step_input_prompt or "",
|
|
574
|
+
"input_index": step_input_index,
|
|
522
575
|
})
|
|
523
576
|
elif event == "exception":
|
|
524
577
|
exc_type, exc_val, _ = arg
|
|
@@ -533,6 +586,8 @@ def _trace_code_internal(
|
|
|
533
586
|
"func": frame.f_code.co_name,
|
|
534
587
|
"error": f"{exc_type.__name__}: {exc_val}",
|
|
535
588
|
"stdout": current_stdout,
|
|
589
|
+
"input_prompt": step_input_prompt or "",
|
|
590
|
+
"input_index": step_input_index,
|
|
536
591
|
})
|
|
537
592
|
|
|
538
593
|
return _trace
|
|
@@ -542,6 +597,7 @@ def _trace_code_internal(
|
|
|
542
597
|
|
|
543
598
|
try:
|
|
544
599
|
sys.stdout = tracking_stream
|
|
600
|
+
builtins.input = _traced_input # override input() during tracing
|
|
545
601
|
sys.settrace(_trace)
|
|
546
602
|
exec(compiled, g)
|
|
547
603
|
except _StepLimitExceeded:
|
|
@@ -555,10 +611,13 @@ def _trace_code_internal(
|
|
|
555
611
|
"func": "<module>",
|
|
556
612
|
"error": f"{type(e).__name__}: {e}",
|
|
557
613
|
"stdout": captured.getvalue(),
|
|
614
|
+
"input_prompt": "",
|
|
615
|
+
"input_index": -1,
|
|
558
616
|
})
|
|
559
617
|
finally:
|
|
560
618
|
sys.settrace(old_trace)
|
|
561
619
|
sys.stdout = old_stdout
|
|
620
|
+
builtins.input = _real_input # restore real input()
|
|
562
621
|
|
|
563
622
|
return steps
|
|
564
623
|
|
|
@@ -567,6 +626,7 @@ def trace_code(
|
|
|
567
626
|
code_str: str,
|
|
568
627
|
max_steps: int = 5000,
|
|
569
628
|
user_globals: dict[str, typing.Any] | None = None,
|
|
629
|
+
inputs: list[str] | None = None,
|
|
570
630
|
) -> list[dict]:
|
|
571
631
|
"""Trace Python code and return step-by-step execution state.
|
|
572
632
|
|
|
@@ -581,11 +641,15 @@ def trace_code(
|
|
|
581
641
|
variables are available to the traced code). The code is executed
|
|
582
642
|
directly in this namespace — variables defined by the traced code
|
|
583
643
|
persist in the kernel (like a normal Jupyter cell).
|
|
644
|
+
inputs : list[str], optional
|
|
645
|
+
Pre-collected user inputs for input() calls. Each input() call
|
|
646
|
+
during tracing returns the next value from this list, instead of
|
|
647
|
+
reading from stdin.
|
|
584
648
|
|
|
585
649
|
Returns
|
|
586
650
|
-------
|
|
587
651
|
list[dict]
|
|
588
652
|
Each dict has keys: line, source, call_stack, event, func.
|
|
589
|
-
May also include: return_value, error, stdout.
|
|
653
|
+
May also include: return_value, error, stdout, input_prompt, input_index.
|
|
590
654
|
"""
|
|
591
|
-
return _trace_code_internal(code_str, user_globals, max_steps)
|
|
655
|
+
return _trace_code_internal(code_str, user_globals, max_steps, inputs)
|
{jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jupyter-python-tutor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python Tutor-style step-by-step code visualization for JupyterLab 4.x and Notebook 7
|
|
5
5
|
Author: dive4dec
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,10 +34,10 @@ In a Jupyter cell:
|
|
|
34
34
|
%load_ext jupyter_python_tutor
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Then use the `%%
|
|
37
|
+
Then use the `%%pytutor` cell magic:
|
|
38
38
|
|
|
39
39
|
```python
|
|
40
|
-
%%
|
|
40
|
+
%%pytutor
|
|
41
41
|
x = 3
|
|
42
42
|
y = x + 4
|
|
43
43
|
print(y)
|
|
@@ -66,9 +66,10 @@ from frame variables to heap objects (like [Python Tutor](https://pythontutor.co
|
|
|
66
66
|
## Options
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
%%
|
|
70
|
-
%%
|
|
71
|
-
%%
|
|
69
|
+
%%pytutor --max-steps 1000 # Limit trace steps (default: 5000)
|
|
70
|
+
%%pytutor --raw # Output trace as JSON instead of HTML
|
|
71
|
+
%%pytutor --height 400 # Set minimum widget height (default: 500)
|
|
72
|
+
%%pytutor --input 2026 # Pre-collected input for input() calls
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
## Examples
|
|
@@ -76,7 +77,7 @@ from frame variables to heap objects (like [Python Tutor](https://pythontutor.co
|
|
|
76
77
|
### Loops
|
|
77
78
|
|
|
78
79
|
```python
|
|
79
|
-
%%
|
|
80
|
+
%%pytutor
|
|
80
81
|
total = 0
|
|
81
82
|
for i in range(5):
|
|
82
83
|
total += i
|
|
@@ -86,7 +87,7 @@ print(f"Sum: {total}")
|
|
|
86
87
|
### Functions with recursion
|
|
87
88
|
|
|
88
89
|
```python
|
|
89
|
-
%%
|
|
90
|
+
%%pytutor
|
|
90
91
|
def factorial(n):
|
|
91
92
|
if n <= 1:
|
|
92
93
|
return 1
|
|
@@ -99,7 +100,7 @@ print(result)
|
|
|
99
100
|
### Classes and inheritance
|
|
100
101
|
|
|
101
102
|
```python
|
|
102
|
-
%%
|
|
103
|
+
%%pytutor
|
|
103
104
|
class Animal:
|
|
104
105
|
def __init__(self, name):
|
|
105
106
|
self.name = name
|
|
@@ -116,7 +117,7 @@ print(d.speak())
|
|
|
116
117
|
|
|
117
118
|
## How It Works
|
|
118
119
|
|
|
119
|
-
1. **Tracing**: The `%%
|
|
120
|
+
1. **Tracing**: The `%%pytutor` magic compiles the cell code and executes it under
|
|
120
121
|
`sys.settrace()`. At each line/call/return event, a snapshot of all local and
|
|
121
122
|
global variables is captured. AST analysis (`ast.parse`) extracts the set of
|
|
122
123
|
names the traced code touches, so only code-relevant variables are shown
|
|
@@ -136,7 +137,7 @@ print(d.speak())
|
|
|
136
137
|
4. **JupyterLab 4 compatibility**: JupyterLab 4's HTML sanitizer (DOMPurify)
|
|
137
138
|
strips JavaScript and external URLs from untrusted notebooks. The iframe
|
|
138
139
|
`srcdoc` approach bypasses this entirely in trusted notebooks. Run
|
|
139
|
-
`jupyter trust your_notebook.ipynb` after executing cells with `%%
|
|
140
|
+
`jupyter trust your_notebook.ipynb` after executing cells with `%%pytutor`
|
|
140
141
|
to ensure the visualization renders correctly.
|
|
141
142
|
|
|
142
143
|
## Limitations
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jupyter-python-tutor"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python Tutor-style step-by-step code visualization for JupyterLab 4.x and Notebook 7"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -13,28 +13,28 @@ def ipython():
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class TestTutorMagic:
|
|
16
|
-
"""Test the %%
|
|
16
|
+
"""Test the %%pytutor cell magic."""
|
|
17
17
|
|
|
18
18
|
def test_tutor_magic_registered(self, ipython):
|
|
19
|
-
assert "
|
|
19
|
+
assert "pytutor" in ipython.magics_manager.magics["cell"]
|
|
20
20
|
|
|
21
21
|
def test_simple_code(self, ipython):
|
|
22
|
-
result = ipython.run_cell_magic("
|
|
22
|
+
result = ipython.run_cell_magic("pytutor", "", "x = 3\ny = 4")
|
|
23
23
|
# Should return None (display is called) or a widget/HTML
|
|
24
24
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
25
25
|
|
|
26
26
|
def test_print_output(self, ipython):
|
|
27
|
-
result = ipython.run_cell_magic("
|
|
27
|
+
result = ipython.run_cell_magic("pytutor", "", "print('hello world')")
|
|
28
28
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
29
29
|
|
|
30
30
|
def test_raw_option(self, ipython):
|
|
31
|
-
result = ipython.run_cell_magic("
|
|
31
|
+
result = ipython.run_cell_magic("pytutor", "--raw", "x = 3")
|
|
32
32
|
# --raw returns JSON
|
|
33
33
|
assert result is not None
|
|
34
34
|
assert hasattr(result, 'data') # IPython.display.JSON
|
|
35
35
|
|
|
36
36
|
def test_max_steps_option(self, ipython):
|
|
37
|
-
result = ipython.run_cell_magic("
|
|
37
|
+
result = ipython.run_cell_magic("pytutor", "--max-steps 10", "x = 3")
|
|
38
38
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
39
39
|
|
|
40
40
|
def test_recursion(self, ipython):
|
|
@@ -46,7 +46,7 @@ def factorial(n):
|
|
|
46
46
|
|
|
47
47
|
result = factorial(3)
|
|
48
48
|
"""
|
|
49
|
-
result = ipython.run_cell_magic("
|
|
49
|
+
result = ipython.run_cell_magic("pytutor", "", code)
|
|
50
50
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
51
51
|
|
|
52
52
|
def test_inheritance(self, ipython):
|
|
@@ -62,18 +62,18 @@ class Circle(Shape):
|
|
|
62
62
|
|
|
63
63
|
c = Circle('red', 5)
|
|
64
64
|
"""
|
|
65
|
-
result = ipython.run_cell_magic("
|
|
65
|
+
result = ipython.run_cell_magic("pytutor", "", code)
|
|
66
66
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
67
67
|
|
|
68
68
|
def test_syntax_error_handled(self, ipython):
|
|
69
|
-
result = ipython.run_cell_magic("
|
|
69
|
+
result = ipython.run_cell_magic("pytutor", "", "x = )")
|
|
70
70
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
71
71
|
|
|
72
72
|
def test_imports(self, ipython):
|
|
73
73
|
code = "import math\nresult = math.pi"
|
|
74
|
-
result = ipython.run_cell_magic("
|
|
74
|
+
result = ipython.run_cell_magic("pytutor", "", code)
|
|
75
75
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
76
76
|
|
|
77
77
|
def test_height_option(self, ipython):
|
|
78
|
-
result = ipython.run_cell_magic("
|
|
78
|
+
result = ipython.run_cell_magic("pytutor", "--height 500", "x = 3")
|
|
79
79
|
assert result is None or hasattr(result, 'children') or "jpt" in str(result)
|
|
@@ -146,7 +146,7 @@ print(c.describe())
|
|
|
146
146
|
"""Traced code should execute in the user's namespace so variables persist.
|
|
147
147
|
|
|
148
148
|
This is like a normal Jupyter cell — classes/functions defined in one
|
|
149
|
-
cell are available in the next. The %%
|
|
149
|
+
cell are available in the next. The %%pytutor magic should work the same way.
|
|
150
150
|
"""
|
|
151
151
|
user_globals = {"existing": 42}
|
|
152
152
|
# Trace code that defines a new variable
|
|
File without changes
|
{jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_python_tutor-0.2.0 → jupyter_python_tutor-0.3.0}/jupyter_python_tutor.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|