jupyter-python-tutor 0.3.0__tar.gz → 0.3.1__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.3.0 → jupyter_python_tutor-0.3.1}/PKG-INFO +1 -1
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/__init__.py +1 -1
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/renderer.py +73 -143
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/PKG-INFO +1 -1
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/pyproject.toml +1 -1
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/LICENSE +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/README.md +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/magics.py +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/tracer.py +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/SOURCES.txt +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/dependency_links.txt +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/requires.txt +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/top_level.txt +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/setup.cfg +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/tests/test_magics.py +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/tests/test_renderer.py +0 -0
- {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/tests/test_tracer.py +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
"""Render a trace as interactive HTML inside an iframe srcdoc.
|
|
1
|
+
"""Render a Python trace as interactive HTML inside an iframe srcdoc.
|
|
2
2
|
|
|
3
3
|
JupyterLab 4 sanitizes HTML output from untrusted notebooks, stripping
|
|
4
4
|
``<style>``, ``<script>``, ``<iframe>``, ``<button>``, ``<input>``, etc.
|
|
5
5
|
However, when a notebook is **trusted** (either run live by the user, or
|
|
6
6
|
signed with ``jupyter trust``), all HTML passes through unsanitized.
|
|
7
7
|
|
|
8
|
-
This renderer wraps the entire visualization in ``<iframe srcdoc="...">``,
|
|
9
|
-
which:
|
|
8
|
+
This renderer wraps the entire visualization in ``<iframe srcdoc="...">``, which:
|
|
10
9
|
- Works in trusted notebooks (live execution + signed notebooks)
|
|
11
10
|
- Contains full HTML with ``<style>``, ``<script>``, ``<input type="range">``
|
|
12
11
|
- Is fully isolated from JupyterLab's CSS/JS
|
|
@@ -26,11 +25,12 @@ Layout (OPT_Mentor / Python Tutor style):
|
|
|
26
25
|
│ │ └──┘ │ └──┘ │
|
|
27
26
|
└──────────────┴───────────────────────────────────────────┘
|
|
28
27
|
|
|
29
|
-
Key design:
|
|
30
|
-
-
|
|
31
|
-
-
|
|
28
|
+
Key design (matches jupyter-cpp-tutor):
|
|
29
|
+
- Fixed widget height with internal scrolling — no "dancing" cell
|
|
30
|
+
- Slider navigation (``<input type="range">``)
|
|
31
|
+
- SVG arrows from frame variables to heap objects
|
|
32
32
|
- Single-step view — only current step shown, JS swaps content
|
|
33
|
-
-
|
|
33
|
+
- Draggable dividers between columns (double-click to collapse)
|
|
34
34
|
- Functions show TRUE name (co_name), not __name__ (functools.wraps-safe)
|
|
35
35
|
- Heap only shows objects reachable from the current call stack
|
|
36
36
|
"""
|
|
@@ -39,17 +39,11 @@ from __future__ import annotations
|
|
|
39
39
|
import html
|
|
40
40
|
import json
|
|
41
41
|
|
|
42
|
-
__all__ = ["render_trace"
|
|
42
|
+
__all__ = ["render_trace"]
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def _generate_inner_html(steps: list[dict], source_code: str, height: int = 500) -> str:
|
|
46
|
-
"""Generate the full HTML page that goes inside the iframe srcdoc.
|
|
47
|
-
|
|
48
|
-
This is a self-contained HTML page with:
|
|
49
|
-
- CSS for the visualization layout
|
|
50
|
-
- All step data embedded as JSON
|
|
51
|
-
- JavaScript for step navigation, SVG arrows, resizing
|
|
52
|
-
"""
|
|
46
|
+
"""Generate the full HTML page that goes inside the iframe srcdoc."""
|
|
53
47
|
if not steps:
|
|
54
48
|
return '<!DOCTYPE html><html><body><p style="color:red;">No trace steps to display.</p></body></html>'
|
|
55
49
|
|
|
@@ -107,7 +101,7 @@ def _generate_inner_html(steps: list[dict], source_code: str, height: int = 500)
|
|
|
107
101
|
steps_json = json.dumps(clean_steps, ensure_ascii=False)
|
|
108
102
|
source_json = json.dumps(source_lines, ensure_ascii=False)
|
|
109
103
|
|
|
110
|
-
return
|
|
104
|
+
return rf"""<!DOCTYPE html>
|
|
111
105
|
<html lang="en">
|
|
112
106
|
<head>
|
|
113
107
|
<meta charset="utf-8">
|
|
@@ -187,55 +181,58 @@ body {{
|
|
|
187
181
|
overflow-y: auto;
|
|
188
182
|
min-height: 20px;
|
|
189
183
|
}}
|
|
190
|
-
/* Main area */
|
|
184
|
+
/* Main content area — flex, internal scroll, fixed height */
|
|
191
185
|
.jpt-main {{
|
|
192
186
|
display: flex;
|
|
193
|
-
|
|
187
|
+
flex: 1;
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
min-height: 0;
|
|
194
190
|
}}
|
|
195
191
|
.jpt-code-section {{
|
|
196
192
|
flex: 0 0 40%;
|
|
197
193
|
min-width: 30px;
|
|
198
194
|
overflow: auto;
|
|
199
195
|
background: #fafafa;
|
|
200
|
-
|
|
196
|
+
border-right: 1px solid #d4d4d4;
|
|
201
197
|
}}
|
|
202
198
|
.jpt-code-section::-webkit-scrollbar {{ width: 8px; height: 8px; }}
|
|
203
199
|
.jpt-code-section::-webkit-scrollbar-thumb {{ background: #cbd5e1; border-radius: 4px; }}
|
|
204
|
-
/* Draggable
|
|
200
|
+
/* Draggable dividers */
|
|
205
201
|
.jpt-divider {{
|
|
206
202
|
flex: 0 0 6px;
|
|
207
203
|
cursor: col-resize;
|
|
208
204
|
background: #d4d4d4;
|
|
209
|
-
position: relative;
|
|
210
205
|
user-select: none;
|
|
211
206
|
-webkit-user-select: none;
|
|
212
207
|
transition: background 0.15s;
|
|
208
|
+
flex-shrink: 0;
|
|
213
209
|
}}
|
|
214
210
|
.jpt-divider:hover {{ background: #3b82f6; }}
|
|
215
211
|
.jpt-divider.jpt-dragging {{ background: #2563eb; }}
|
|
216
212
|
.jpt-divider.jpt-collapsed {{ background: #9ca3af; cursor: pointer; }}
|
|
217
|
-
|
|
218
|
-
/* Right panel holds frames-divider-heap, no overflow so SVG arrows stay aligned */
|
|
213
|
+
/* Right panel holds frames + heap, with SVG overlay */
|
|
219
214
|
.jpt-right-panel {{
|
|
220
215
|
flex: 1;
|
|
221
216
|
display: flex;
|
|
222
217
|
min-width: 0;
|
|
223
218
|
position: relative;
|
|
219
|
+
overflow: hidden;
|
|
224
220
|
}}
|
|
225
221
|
.jpt-frames-section {{
|
|
226
222
|
flex: 0 0 35%;
|
|
227
223
|
min-width: 30px;
|
|
224
|
+
overflow: auto;
|
|
228
225
|
padding: 6px;
|
|
229
226
|
background: #fafafa;
|
|
230
227
|
}}
|
|
231
228
|
.jpt-heap-section {{
|
|
232
229
|
flex: 1;
|
|
230
|
+
overflow: auto;
|
|
233
231
|
padding: 6px;
|
|
234
232
|
background: #f5f5f5;
|
|
235
|
-
position: relative;
|
|
236
233
|
min-width: 30px;
|
|
237
234
|
}}
|
|
238
|
-
/* Code */
|
|
235
|
+
/* Code lines */
|
|
239
236
|
.jpt-code-line {{
|
|
240
237
|
display: flex;
|
|
241
238
|
align-items: baseline;
|
|
@@ -264,12 +261,12 @@ body {{
|
|
|
264
261
|
}}
|
|
265
262
|
.jpt-code-text {{ flex: 1; padding-left: 4px; }}
|
|
266
263
|
/* Python syntax highlighting */
|
|
267
|
-
.jpt-tok-kw {{ color: #c678dd; font-weight: 600; }}
|
|
268
|
-
.jpt-tok-str {{ color: #98c379; }}
|
|
269
|
-
.jpt-tok-num {{ color: #d19a66; }}
|
|
270
|
-
.jpt-tok-com {{ color: #7c7c7c; font-style: italic; }}
|
|
271
|
-
.jpt-tok-fn {{ color: #61afef; }}
|
|
272
|
-
.jpt-tok-builtin {{ color: #56b6c2; }}
|
|
264
|
+
.jpt-tok-kw {{ color: #c678dd; font-weight: 600; }}
|
|
265
|
+
.jpt-tok-str {{ color: #98c379; }}
|
|
266
|
+
.jpt-tok-num {{ color: #d19a66; }}
|
|
267
|
+
.jpt-tok-com {{ color: #7c7c7c; font-style: italic; }}
|
|
268
|
+
.jpt-tok-fn {{ color: #61afef; }}
|
|
269
|
+
.jpt-tok-builtin {{ color: #56b6c2; }}
|
|
273
270
|
/* Frames */
|
|
274
271
|
.jpt-frame {{
|
|
275
272
|
border: 1px solid #bae6fd; border-radius: 4px; background: #f0f9ff;
|
|
@@ -348,7 +345,6 @@ body {{
|
|
|
348
345
|
pointer-events: none; z-index: 10;
|
|
349
346
|
}}
|
|
350
347
|
.jpt-arrow-path {{ stroke: #6b7280; stroke-width: 1.5; fill: none; marker-end: url(#arrowhead); }}
|
|
351
|
-
.jpt-arrow-path-highlight {{ stroke: #3b82f6; stroke-width: 2; }}
|
|
352
348
|
/* Input dialog overlay */
|
|
353
349
|
.jpt-input-overlay {{
|
|
354
350
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
@@ -384,7 +380,7 @@ body {{
|
|
|
384
380
|
</style>
|
|
385
381
|
</head>
|
|
386
382
|
<body>
|
|
387
|
-
<div class="jpt-widget" id="jptWidget">
|
|
383
|
+
<div class="jpt-widget" id="jptWidget" style="height:{height}px;position:relative;">
|
|
388
384
|
<!-- Input dialog overlay (hidden by default) -->
|
|
389
385
|
<div class="jpt-input-overlay" id="inputOverlay" style="display:none;">
|
|
390
386
|
<div class="jpt-input-dialog">
|
|
@@ -454,8 +450,7 @@ body {{
|
|
|
454
450
|
}}
|
|
455
451
|
|
|
456
452
|
function funcDisplayName(ref) {{
|
|
457
|
-
|
|
458
|
-
return ref.name || 'func';
|
|
453
|
+
return ref.true_name || ref.name;
|
|
459
454
|
}}
|
|
460
455
|
|
|
461
456
|
// ─── Python syntax highlighting ──────────────────────────────────
|
|
@@ -472,41 +467,33 @@ body {{
|
|
|
472
467
|
var builtinSet = {{}}; PY_BUILTINS.forEach(function(b){{ builtinSet[b] = 1; }});
|
|
473
468
|
|
|
474
469
|
function highlightPython(line) {{
|
|
475
|
-
// Tokenize: comments, strings, numbers, identifiers, operators
|
|
476
470
|
var result = '';
|
|
477
471
|
var i = 0;
|
|
478
472
|
while (i < line.length) {{
|
|
479
473
|
var ch = line[i];
|
|
480
|
-
// Comment
|
|
481
474
|
if (ch === '#') {{
|
|
482
475
|
result += '<span class="jpt-tok-com">' + escapeHtml(line.substring(i)) + '</span>';
|
|
483
476
|
break;
|
|
484
477
|
}}
|
|
485
|
-
// String (single, double, triple)
|
|
486
478
|
if (ch === '"' || ch === "'") {{
|
|
487
479
|
var quote = ch;
|
|
488
|
-
// Check triple-quote
|
|
489
480
|
if (line.substring(i, i+3) === quote+quote+quote) {{
|
|
490
481
|
var end = line.indexOf(quote+quote+quote, i+3);
|
|
491
482
|
if (end < 0) end = line.length - 3;
|
|
492
|
-
|
|
493
|
-
result += '<span class="jpt-tok-str">' + escapeHtml(s) + '</span>';
|
|
483
|
+
result += '<span class="jpt-tok-str">' + escapeHtml(line.substring(i, end+3)) + '</span>';
|
|
494
484
|
i = end + 3;
|
|
495
485
|
continue;
|
|
496
486
|
}}
|
|
497
|
-
// Single/double quote — find matching close (handle escapes)
|
|
498
487
|
var j = i + 1;
|
|
499
488
|
while (j < line.length) {{
|
|
500
489
|
if (line[j] === '\\\\') {{ j += 2; continue; }}
|
|
501
490
|
if (line[j] === quote) {{ j++; break; }}
|
|
502
491
|
j++;
|
|
503
492
|
}}
|
|
504
|
-
|
|
505
|
-
result += '<span class="jpt-tok-str">' + escapeHtml(s) + '</span>';
|
|
493
|
+
result += '<span class="jpt-tok-str">' + escapeHtml(line.substring(i, j)) + '</span>';
|
|
506
494
|
i = j;
|
|
507
495
|
continue;
|
|
508
496
|
}}
|
|
509
|
-
// f-string prefix (f, r, b, fr, rf, etc. before quote)
|
|
510
497
|
if (/[frbFRB]/.test(ch) && i+1 < line.length && (line[i+1] === '"' || line[i+1] === "'")) {{
|
|
511
498
|
var j = i;
|
|
512
499
|
while (j < line.length && /[frbFRB]/.test(line[j])) j++;
|
|
@@ -518,17 +505,14 @@ body {{
|
|
|
518
505
|
if (line[k] === quote) {{ k++; break; }}
|
|
519
506
|
k++;
|
|
520
507
|
}}
|
|
521
|
-
|
|
522
|
-
result += '<span class="jpt-tok-str">' + escapeHtml(s) + '</span>';
|
|
508
|
+
result += '<span class="jpt-tok-str">' + escapeHtml(line.substring(i, k)) + '</span>';
|
|
523
509
|
i = k;
|
|
524
510
|
continue;
|
|
525
511
|
}}
|
|
526
512
|
}}
|
|
527
|
-
// Number
|
|
528
513
|
if (/[0-9]/.test(ch) || (ch === '.' && i+1 < line.length && /[0-9]/.test(line[i+1]))) {{
|
|
529
514
|
var j = i;
|
|
530
|
-
while (j < line.length && /[0-9._xXeE
|
|
531
|
-
// stop on +/- if not part of exponent
|
|
515
|
+
while (j < line.length && /[0-9._xXeE+\\-a-fA-F]/.test(line[j])) {{
|
|
532
516
|
if ((line[j] === '+' || line[j] === '-') && !/[eE]/.test(line[j-1])) break;
|
|
533
517
|
j++;
|
|
534
518
|
}}
|
|
@@ -536,12 +520,10 @@ body {{
|
|
|
536
520
|
i = j;
|
|
537
521
|
continue;
|
|
538
522
|
}}
|
|
539
|
-
// Identifier / keyword / builtin
|
|
540
523
|
if (/[a-zA-Z_]/.test(ch)) {{
|
|
541
524
|
var j = i;
|
|
542
525
|
while (j < line.length && /[a-zA-Z0-9_]/.test(line[j])) j++;
|
|
543
526
|
var word = line.substring(i, j);
|
|
544
|
-
// Check if followed by ( → function call
|
|
545
527
|
var afterWord = line.substring(j).trim();
|
|
546
528
|
if (kwSet[word]) {{
|
|
547
529
|
result += '<span class="jpt-tok-kw">' + escapeHtml(word) + '</span>';
|
|
@@ -555,7 +537,6 @@ body {{
|
|
|
555
537
|
i = j;
|
|
556
538
|
continue;
|
|
557
539
|
}}
|
|
558
|
-
// Default: escape and output
|
|
559
540
|
result += escapeHtml(ch);
|
|
560
541
|
i++;
|
|
561
542
|
}}
|
|
@@ -672,7 +653,7 @@ body {{
|
|
|
672
653
|
html += '<div class="jpt-heap-object jpt-heap-module" data-heap-id="' + heapId + '">' +
|
|
673
654
|
'<div class="jpt-heap-type-label">module ' + escapeHtml(ref.name) + '</div></div>';
|
|
674
655
|
}} else if (ref.is_function) {{
|
|
675
|
-
var tn = ref
|
|
656
|
+
var tn = funcDisplayName(ref);
|
|
676
657
|
var dn = ref.display_name || ref.name;
|
|
677
658
|
var attrs = details.attrs || [];
|
|
678
659
|
var attrHtml = '';
|
|
@@ -710,14 +691,11 @@ body {{
|
|
|
710
691
|
}}
|
|
711
692
|
|
|
712
693
|
function drawArrows() {{
|
|
713
|
-
// Clear existing arrows (keep defs)
|
|
714
694
|
while (svgOverlay.lastChild && svgOverlay.lastChild.tagName !== 'defs') {{
|
|
715
695
|
svgOverlay.removeChild(svgOverlay.lastChild);
|
|
716
696
|
}}
|
|
717
|
-
// SVG overlay lives in .jpt-right-panel, spanning both frames and heap
|
|
718
697
|
var rightPanel = document.getElementById('rightPanel');
|
|
719
698
|
var panelRect = rightPanel.getBoundingClientRect();
|
|
720
|
-
// Find all pointer boxes in frames and matching heap objects
|
|
721
699
|
var pointers = framesSection.querySelectorAll('.jpt-var-pointer[data-heap-id]');
|
|
722
700
|
for (var i = 0; i < pointers.length; i++) {{
|
|
723
701
|
var ptr = pointers[i];
|
|
@@ -727,12 +705,10 @@ body {{
|
|
|
727
705
|
if (!heapObj) continue;
|
|
728
706
|
var ptrRect = ptr.getBoundingClientRect();
|
|
729
707
|
var objRect = heapObj.getBoundingClientRect();
|
|
730
|
-
// Convert to SVG coordinates (relative to rightPanel)
|
|
731
708
|
var x1 = ptrRect.right - panelRect.left;
|
|
732
709
|
var y1 = ptrRect.top + ptrRect.height / 2 - panelRect.top;
|
|
733
710
|
var x2 = objRect.left - panelRect.left;
|
|
734
711
|
var y2 = objRect.top + objRect.height / 2 - panelRect.top;
|
|
735
|
-
// Draw a curved path from pointer to heap object
|
|
736
712
|
var midX = x1 + (x2 - x1) * 0.5;
|
|
737
713
|
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
738
714
|
path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' C ' + midX + ' ' + y1 + ', ' + midX + ' ' + y2 + ', ' + x2 + ' ' + y2);
|
|
@@ -741,45 +717,21 @@ body {{
|
|
|
741
717
|
}}
|
|
742
718
|
}}
|
|
743
719
|
|
|
744
|
-
function autoResize() {{
|
|
745
|
-
// Auto-resize widget to fit content — the widget's height follows
|
|
746
|
-
// the tallest of code/frames/heap so there is no internal scroll
|
|
747
|
-
// and arrows never detach from their targets.
|
|
748
|
-
// The parent page polls the widget height and resizes the iframe.
|
|
749
|
-
var codeH = codeDisplay.scrollHeight;
|
|
750
|
-
var framesH = framesSection.scrollHeight;
|
|
751
|
-
var heapH = heapContent.scrollHeight;
|
|
752
|
-
var contentH = Math.max(codeH, framesH, heapH, {height});
|
|
753
|
-
// Add navbar + output section heights
|
|
754
|
-
var navbarH = document.querySelector('.jpt-navbar').offsetHeight;
|
|
755
|
-
var outputH = document.querySelector('.jpt-output-section').offsetHeight;
|
|
756
|
-
var totalH = contentH + navbarH + outputH + 12; // +12 for borders/padding
|
|
757
|
-
// Set widget height so parent script can read it
|
|
758
|
-
document.getElementById('jptWidget').style.height = totalH + 'px';
|
|
759
|
-
}}
|
|
760
|
-
|
|
761
720
|
// ─── Input handling ──────────────────────────────────────────────
|
|
762
|
-
|
|
763
|
-
// Once collected, navigation back/forth never re-prompts.
|
|
764
|
-
var inputsCollected = {{}}; // input_index → value string
|
|
721
|
+
var inputsCollected = {{}};
|
|
765
722
|
var inputOverlay = document.getElementById('inputOverlay');
|
|
766
723
|
var inputDialogLabel = document.getElementById('inputDialogLabel');
|
|
767
724
|
var inputDialogInput = document.getElementById('inputDialogInput');
|
|
768
725
|
var inputDialogBtn = document.getElementById('inputDialogBtn');
|
|
769
726
|
|
|
770
|
-
// Pre-populate from step data (inputs already provided during tracing)
|
|
771
727
|
for (var si = 0; si < nSteps; si++) {{
|
|
772
728
|
var ss = steps[si];
|
|
773
729
|
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
730
|
if (!(ss.input_index in inputsCollected)) {{
|
|
777
731
|
var prompt = ss.input_prompt || '';
|
|
778
732
|
var out = ss.stdout || '';
|
|
779
|
-
// stdout contains prompt + value + newline, find the value
|
|
780
733
|
if (out.includes(prompt)) {{
|
|
781
734
|
var afterPrompt = out.substring(out.indexOf(prompt) + prompt.length);
|
|
782
|
-
// The value is everything up to the first newline
|
|
783
735
|
var nl = afterPrompt.indexOf(String.fromCharCode(10));
|
|
784
736
|
if (nl >= 0) {{
|
|
785
737
|
inputsCollected[ss.input_index] = afterPrompt.substring(0, nl);
|
|
@@ -791,7 +743,6 @@ body {{
|
|
|
791
743
|
}}
|
|
792
744
|
}}
|
|
793
745
|
|
|
794
|
-
// Show input badge in output section if this step has input
|
|
795
746
|
function updateInputBadge(step) {{
|
|
796
747
|
var badge = document.getElementById('inputBadge');
|
|
797
748
|
if (!badge) return;
|
|
@@ -814,8 +765,8 @@ body {{
|
|
|
814
765
|
renderFrames(step);
|
|
815
766
|
renderHeap(step);
|
|
816
767
|
stdout.textContent = step.stdout || '';
|
|
817
|
-
if (!step.stdout) stdout.style.color = '#86efac'
|
|
818
|
-
else stdout.style.color = '#166534'
|
|
768
|
+
if (!step.stdout) {{ stdout.style.color = '#86efac'; stdout.style.fontStyle = 'italic'; stdout.textContent = '(no output yet)'; }}
|
|
769
|
+
else {{ stdout.style.color = '#166534'; stdout.style.fontStyle = 'normal'; }}
|
|
819
770
|
updateInputBadge(step);
|
|
820
771
|
stepCounter.textContent = 'Step ' + (idx + 1) + ' of ' + nSteps;
|
|
821
772
|
slider.value = idx;
|
|
@@ -823,18 +774,12 @@ body {{
|
|
|
823
774
|
btnPrev.disabled = (idx === 0);
|
|
824
775
|
btnNext.disabled = (idx === nSteps - 1);
|
|
825
776
|
btnLast.disabled = (idx === nSteps - 1);
|
|
826
|
-
// Draw arrows
|
|
777
|
+
// Draw arrows after DOM is updated (height is fixed, no resize needed)
|
|
827
778
|
requestAnimationFrame(function() {{
|
|
828
779
|
requestAnimationFrame(function() {{
|
|
829
|
-
autoResize();
|
|
830
780
|
drawArrows();
|
|
831
781
|
}});
|
|
832
782
|
}});
|
|
833
|
-
// Safety: also redraw after a longer delay in case rAF fires too early
|
|
834
|
-
setTimeout(function() {{
|
|
835
|
-
autoResize();
|
|
836
|
-
drawArrows();
|
|
837
|
-
}}, 100);
|
|
838
783
|
}}
|
|
839
784
|
|
|
840
785
|
// Event handlers
|
|
@@ -850,14 +795,12 @@ body {{
|
|
|
850
795
|
else if (e.key === 'ArrowRight') {{ renderStep(curStep + 1); e.preventDefault(); }}
|
|
851
796
|
}});
|
|
852
797
|
|
|
853
|
-
// Redraw arrows
|
|
854
|
-
window.addEventListener('resize',
|
|
798
|
+
// Redraw arrows on resize/scroll (height is fixed, just reposition arrows)
|
|
799
|
+
window.addEventListener('resize', drawArrows);
|
|
855
800
|
framesSection.addEventListener('scroll', drawArrows);
|
|
856
801
|
heapContent.addEventListener('scroll', drawArrows);
|
|
857
802
|
|
|
858
803
|
// ─── Draggable dividers ──────────────────────────────────────────
|
|
859
|
-
// Divider 1: between code section and right panel
|
|
860
|
-
// Divider 2: between frames section and heap section
|
|
861
804
|
var divider1 = document.getElementById('divider1');
|
|
862
805
|
var divider2 = document.getElementById('divider2');
|
|
863
806
|
var codeSection = document.getElementById('codeSection');
|
|
@@ -865,13 +808,11 @@ body {{
|
|
|
865
808
|
var mainEl = document.querySelector('.jpt-main');
|
|
866
809
|
var rightPanelEl = document.getElementById('rightPanel');
|
|
867
810
|
|
|
868
|
-
// Remember pre-collapse flex values so we can restore
|
|
869
811
|
var savedCodeFlex = '0 0 40%';
|
|
870
812
|
var savedFramesFlex = '0 0 35%';
|
|
871
813
|
|
|
872
|
-
function startDrag(divider
|
|
814
|
+
function startDrag(divider) {{
|
|
873
815
|
return function(e) {{
|
|
874
|
-
// Don't start drag if it was a double-click (collapse toggle)
|
|
875
816
|
if (divider._justCollapsed) {{
|
|
876
817
|
divider._justCollapsed = false;
|
|
877
818
|
return;
|
|
@@ -881,43 +822,35 @@ body {{
|
|
|
881
822
|
var startX = e.clientX;
|
|
882
823
|
var startCodeW = codeSection.offsetWidth;
|
|
883
824
|
var startFramesW = framesSection.offsetWidth;
|
|
884
|
-
var startHeapW = heapSection.offsetWidth;
|
|
885
825
|
var mainW = mainEl.offsetWidth;
|
|
886
826
|
var panelW = rightPanelEl.offsetWidth;
|
|
887
827
|
|
|
888
828
|
function onMove(ev) {{
|
|
889
829
|
var dx = ev.clientX - startX;
|
|
890
830
|
if (divider === divider1) {{
|
|
891
|
-
// Resize code vs (divider + right panel)
|
|
892
831
|
var newCodeW = Math.max(30, Math.min(mainW - 60, startCodeW + dx));
|
|
893
832
|
var pct = (newCodeW / mainW) * 100;
|
|
894
833
|
codeSection.style.flex = '0 0 ' + pct + '%';
|
|
895
|
-
// Ensure right panel takes remaining space
|
|
896
834
|
rightPanelEl.style.flex = '1';
|
|
897
835
|
}} else {{
|
|
898
|
-
// Resize frames vs heap within right panel
|
|
899
836
|
var newFramesW = Math.max(30, Math.min(panelW - 36, startFramesW + dx));
|
|
900
837
|
var fPct = (newFramesW / panelW) * 100;
|
|
901
838
|
framesSection.style.flex = '0 0 ' + fPct + '%';
|
|
902
839
|
heapSection.style.flex = '1';
|
|
903
840
|
}}
|
|
904
|
-
// Live redraw: arrows must follow the new column positions
|
|
905
841
|
drawArrows();
|
|
906
|
-
autoResize();
|
|
907
842
|
}}
|
|
908
843
|
|
|
909
|
-
function onUp(
|
|
844
|
+
function onUp() {{
|
|
910
845
|
divider.classList.remove('jpt-dragging');
|
|
911
846
|
document.removeEventListener('mousemove', onMove);
|
|
912
847
|
document.removeEventListener('mouseup', onUp);
|
|
913
|
-
// Save flex values for collapse restore
|
|
914
848
|
if (divider === divider1) {{
|
|
915
849
|
savedCodeFlex = codeSection.style.flex || '0 0 40%';
|
|
916
850
|
}} else {{
|
|
917
851
|
savedFramesFlex = framesSection.style.flex || '0 0 35%';
|
|
918
852
|
}}
|
|
919
853
|
drawArrows();
|
|
920
|
-
autoResize();
|
|
921
854
|
}}
|
|
922
855
|
|
|
923
856
|
document.addEventListener('mousemove', onMove);
|
|
@@ -925,14 +858,12 @@ body {{
|
|
|
925
858
|
}};
|
|
926
859
|
}}
|
|
927
860
|
|
|
928
|
-
|
|
929
|
-
function toggleCollapse(divider, sectionToHide, savedFlexVar, flexVarName) {{
|
|
861
|
+
function toggleCollapse(divider, sectionToHide) {{
|
|
930
862
|
return function(e) {{
|
|
931
863
|
e.preventDefault();
|
|
932
864
|
e.stopPropagation();
|
|
933
865
|
var isCollapsed = divider.classList.contains('jpt-collapsed');
|
|
934
866
|
if (isCollapsed) {{
|
|
935
|
-
// Expand: restore saved flex
|
|
936
867
|
divider.classList.remove('jpt-collapsed');
|
|
937
868
|
sectionToHide.style.display = '';
|
|
938
869
|
if (divider === divider1) {{
|
|
@@ -943,7 +874,6 @@ body {{
|
|
|
943
874
|
heapSection.style.flex = '1';
|
|
944
875
|
}}
|
|
945
876
|
}} else {{
|
|
946
|
-
// Collapse: save current flex then hide
|
|
947
877
|
if (divider === divider1) {{
|
|
948
878
|
savedCodeFlex = codeSection.style.flex || '0 0 40%';
|
|
949
879
|
}} else {{
|
|
@@ -951,7 +881,6 @@ body {{
|
|
|
951
881
|
}}
|
|
952
882
|
divider.classList.add('jpt-collapsed');
|
|
953
883
|
sectionToHide.style.display = 'none';
|
|
954
|
-
// Let the other side take full space
|
|
955
884
|
if (divider === divider1) {{
|
|
956
885
|
codeSection.style.flex = '0 0 0';
|
|
957
886
|
rightPanelEl.style.flex = '1';
|
|
@@ -962,14 +891,13 @@ body {{
|
|
|
962
891
|
}}
|
|
963
892
|
divider._justCollapsed = true;
|
|
964
893
|
drawArrows();
|
|
965
|
-
autoResize();
|
|
966
894
|
}};
|
|
967
895
|
}}
|
|
968
896
|
|
|
969
|
-
divider1.addEventListener('mousedown', startDrag(divider1
|
|
970
|
-
divider2.addEventListener('mousedown', startDrag(divider2
|
|
971
|
-
divider1.addEventListener('dblclick', toggleCollapse(divider1, codeSection
|
|
972
|
-
divider2.addEventListener('dblclick', toggleCollapse(divider2, framesSection
|
|
897
|
+
divider1.addEventListener('mousedown', startDrag(divider1));
|
|
898
|
+
divider2.addEventListener('mousedown', startDrag(divider2));
|
|
899
|
+
divider1.addEventListener('dblclick', toggleCollapse(divider1, codeSection));
|
|
900
|
+
divider2.addEventListener('dblclick', toggleCollapse(divider2, framesSection));
|
|
973
901
|
|
|
974
902
|
// Initial render
|
|
975
903
|
renderStep(0);
|
|
@@ -983,7 +911,6 @@ def render_trace(
|
|
|
983
911
|
steps: list[dict],
|
|
984
912
|
source_code: str | None = None,
|
|
985
913
|
height: int = 500,
|
|
986
|
-
widget_id: str | None = None,
|
|
987
914
|
) -> str:
|
|
988
915
|
"""Render trace steps as interactive HTML inside an iframe.
|
|
989
916
|
|
|
@@ -991,10 +918,11 @@ def render_trace(
|
|
|
991
918
|
``<iframe srcdoc="...">`` which works in trusted JupyterLab notebooks.
|
|
992
919
|
|
|
993
920
|
Features:
|
|
921
|
+
- Fixed widget height with internal scrolling — no "dancing" cell
|
|
994
922
|
- Slider navigation (``<input type="range">``) — drag to scrub through steps
|
|
995
923
|
- SVG arrows from frame variables to heap objects (like OPT_Mentor)
|
|
996
924
|
- Single-step view — only current step shown, JS swaps content
|
|
997
|
-
-
|
|
925
|
+
- Draggable dividers between columns (double-click to collapse)
|
|
998
926
|
- Functions show TRUE name (co_name), not __name__ (functools.wraps-safe)
|
|
999
927
|
- Heap only shows objects reachable from the current call stack
|
|
1000
928
|
- Keyboard navigation (← → arrows)
|
|
@@ -1002,31 +930,33 @@ def render_trace(
|
|
|
1002
930
|
inner = _generate_inner_html(steps, source_code or "", height)
|
|
1003
931
|
# Escape for srcdoc attribute
|
|
1004
932
|
escaped = html.escape(inner, quote=True)
|
|
1005
|
-
#
|
|
1006
|
-
#
|
|
1007
|
-
#
|
|
933
|
+
# Match cpp-tutor's approach exactly to prevent "dancing":
|
|
934
|
+
# 1. border:none so offsetHeight === style.height (no mismatch loop)
|
|
935
|
+
# 2. display:block + margin:0 to avoid inline baseline gap
|
|
936
|
+
# 3. Script reads body.scrollHeight + 20 (idempotent — always writes
|
|
937
|
+
# the same value, so no reflow)
|
|
938
|
+
# 4. onload for initial sizing (stripped by DOMPurify when untrusted,
|
|
939
|
+
# but setInterval catches up within 200ms)
|
|
1008
940
|
return (
|
|
1009
941
|
f'<iframe srcdoc="{escaped}" class="jpt-iframe" '
|
|
1010
|
-
f'style="width:100%;border:
|
|
1011
|
-
f'
|
|
1012
|
-
f'
|
|
1013
|
-
f'
|
|
942
|
+
f'style="width:100%;border:none;overflow:hidden;'
|
|
943
|
+
f'display:block;margin:0;" '
|
|
944
|
+
f'onload="this.style.height=this.contentDocument.body.scrollHeight+20+\'px\'">'
|
|
945
|
+
f'</iframe>'
|
|
946
|
+
f'<script>'
|
|
947
|
+
f'(function(){{'
|
|
948
|
+
f'function autoResize(){{'
|
|
949
|
+
f'document.querySelectorAll(\'iframe.jpt-iframe\').forEach(function(frame){{'
|
|
1014
950
|
f'try{{'
|
|
1015
|
-
f'var
|
|
1016
|
-
f'if(
|
|
1017
|
-
f'var w=d.getElementById("jptWidget");'
|
|
1018
|
-
f'if(!w)return;'
|
|
1019
|
-
f'var h=w.offsetHeight+20;'
|
|
1020
|
-
f'if(h>0&&frame.offsetHeight!==h){{frame.style.height=h+"px";}}'
|
|
951
|
+
f'var h=frame.contentDocument.body.scrollHeight;'
|
|
952
|
+
f'if(h>0)frame.style.height=(h+20)+\'px\';'
|
|
1021
953
|
f'}}catch(e){{}}'
|
|
954
|
+
f'}});'
|
|
955
|
+
f'}}'
|
|
956
|
+
f'if(!window._jptPollStarted){{'
|
|
957
|
+
f'window._jptPollStarted=true;'
|
|
958
|
+
f'setInterval(autoResize,200);'
|
|
1022
959
|
f'}}'
|
|
1023
|
-
f'
|
|
1024
|
-
f'setInterval(resize,200);'
|
|
1025
|
-
f'}}).call(document.currentScript);'
|
|
960
|
+
f'}})();'
|
|
1026
961
|
f'</script>'
|
|
1027
962
|
)
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
def render_trace_widget(steps: list[dict], source_code: str | None = None) -> str:
|
|
1031
|
-
"""Alias for render_trace (returns HTML string)."""
|
|
1032
|
-
return render_trace(steps, source_code=source_code)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jupyter-python-tutor"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|