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.
Files changed (17) hide show
  1. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/PKG-INFO +1 -1
  2. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/__init__.py +1 -1
  3. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/renderer.py +73 -143
  4. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/PKG-INFO +1 -1
  5. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/pyproject.toml +1 -1
  6. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/LICENSE +0 -0
  7. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/README.md +0 -0
  8. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/magics.py +0 -0
  9. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor/tracer.py +0 -0
  10. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/SOURCES.txt +0 -0
  11. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/dependency_links.txt +0 -0
  12. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/requires.txt +0 -0
  13. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/jupyter_python_tutor.egg-info/top_level.txt +0 -0
  14. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/setup.cfg +0 -0
  15. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/tests/test_magics.py +0 -0
  16. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/tests/test_renderer.py +0 -0
  17. {jupyter_python_tutor-0.3.0 → jupyter_python_tutor-0.3.1}/tests/test_tracer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jupyter-python-tutor
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -11,6 +11,6 @@ from .tracer import trace_code
11
11
  from .renderer import render_trace
12
12
  from .magics import load_ipython_extension
13
13
 
14
- __version__ = "0.2.0"
14
+ __version__ = "0.3.1"
15
15
 
16
16
  __all__ = ["trace_code", "render_trace", "load_ipython_extension"]
@@ -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
- - Slider navigation (``<input type="range">``) — not number buttons
31
- - SVG arrows from frame variables to heap objects (like OPT_Mentor/jsPlumb)
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
- - Resizable code panel (draggable divider)
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", "render_trace_widget"]
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 f"""<!DOCTYPE html>
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
- align-items: stretch;
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
- position: relative;
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 vertical dividers between columns */
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
- .jpt-divider.jpt-collapsed:hover {{ background: #3b82f6; }}
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; }} /* 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... */
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
- if (ref.true_name) return ref.true_name;
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
- var s = line.substring(i, end+3);
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
- var s = line.substring(i, j);
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
- var s = line.substring(i, k);
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+\-a-fA-F]/.test(line[j])) {{
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.true_name || ref.name;
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
- // inputsCollected: cached values for each input_index
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', stdout.style.fontStyle = 'italic', stdout.textContent = '(no output yet)';
818
- else stdout.style.color = '#166534', stdout.style.fontStyle = 'normal';
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 + auto-resize after DOM is updated
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 + auto-resize on window resize and scroll
854
- window.addEventListener('resize', function() {{ autoResize(); drawArrows(); }});
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, opts) {{
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(ev) {{
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
- // Double-click to collapse/expand
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, savedCodeFlex, 'savedCodeFlex'));
972
- divider2.addEventListener('dblclick', toggleCollapse(divider2, framesSection, savedFramesFlex, 'savedFramesFlex'));
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
- - Resizable code panel (CSS ``resize: horizontal``)
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
- # The iframe auto-resizes via a polling script in the parent that checks
1006
- # each iframe's content height and adjusts. This works in trusted notebooks
1007
- # where <script> survives sanitization.
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:1px solid #d4d4d4;border-radius:6px;"></iframe>'
1011
- f'<script>(function(){{'
1012
- f'var frame=this.previousElementSibling;'
1013
- f'function resize(){{'
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 d=frame.contentDocument;'
1016
- f'if(!d)return;'
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'resize();'
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jupyter-python-tutor
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jupyter-python-tutor"
7
- version = "0.3.0"
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"