jupyter-python-tutor 0.3.1__tar.gz → 0.3.3__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.1 → jupyter_python_tutor-0.3.3}/PKG-INFO +1 -1
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor/__init__.py +1 -1
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor/renderer.py +56 -6
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor/tracer.py +62 -12
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor.egg-info/PKG-INFO +1 -1
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/pyproject.toml +1 -1
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/tests/test_renderer.py +45 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/tests/test_tracer.py +89 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/LICENSE +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/README.md +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor/magics.py +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor.egg-info/SOURCES.txt +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor.egg-info/dependency_links.txt +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor.egg-info/requires.txt +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor.egg-info/top_level.txt +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/setup.cfg +0 -0
- {jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/tests/test_magics.py +0 -0
|
@@ -68,6 +68,8 @@ def _generate_inner_html(steps: list[dict], source_code: str, height: int = 500)
|
|
|
68
68
|
"stdout": step.get("stdout", ""),
|
|
69
69
|
"call_stack": [],
|
|
70
70
|
"return_value": step.get("return_value"),
|
|
71
|
+
"error": step.get("error"),
|
|
72
|
+
"func": step.get("func"),
|
|
71
73
|
"event": step.get("event", ""),
|
|
72
74
|
"input_prompt": step.get("input_prompt", ""),
|
|
73
75
|
"input_index": step.get("input_index", -1),
|
|
@@ -250,6 +252,31 @@ body {{
|
|
|
250
252
|
background: #fce7f3;
|
|
251
253
|
border-left: 3px solid #e10c65;
|
|
252
254
|
}}
|
|
255
|
+
.jpt-code-line.jpt-error {{
|
|
256
|
+
background: #fee2e2;
|
|
257
|
+
border-left: 3px solid #dc2626;
|
|
258
|
+
}}
|
|
259
|
+
.jpt-error .jpt-arrow {{ color: #dc2626; }}
|
|
260
|
+
.jpt-error .jpt-code-text {{ color: #b91c1c; font-weight: 600; }}
|
|
261
|
+
/* Error banner shown in the Program Output panel on exception steps */
|
|
262
|
+
.jpt-error-banner {{
|
|
263
|
+
margin-top: 4px;
|
|
264
|
+
background: #fee2e2;
|
|
265
|
+
border: 1px solid #fca5a5;
|
|
266
|
+
border-left: 4px solid #dc2626;
|
|
267
|
+
border-radius: 4px;
|
|
268
|
+
padding: 6px 10px;
|
|
269
|
+
color: #991b1b;
|
|
270
|
+
font-family: 'DejaVu Sans Mono', monospace;
|
|
271
|
+
font-size: 12px;
|
|
272
|
+
white-space: pre-wrap;
|
|
273
|
+
word-break: break-word;
|
|
274
|
+
}}
|
|
275
|
+
.jpt-error-banner .jpt-error-banner-title {{
|
|
276
|
+
display: block;
|
|
277
|
+
font-weight: 700;
|
|
278
|
+
margin-bottom: 2px;
|
|
279
|
+
}}
|
|
253
280
|
.jpt-arrow {{
|
|
254
281
|
width: 16px; flex: 0 0 16px; text-align: center;
|
|
255
282
|
color: transparent; font-weight: bold; user-select: none;
|
|
@@ -278,6 +305,10 @@ body {{
|
|
|
278
305
|
display: flex; justify-content: space-between;
|
|
279
306
|
}}
|
|
280
307
|
.jpt-frame-line {{ font-size: 10px; font-weight: normal; color: #64748b; }}
|
|
308
|
+
.jpt-return-badge {{
|
|
309
|
+
font-size: 10px; font-weight: 600; color: #065f46; background: #d1fae5;
|
|
310
|
+
border-radius: 3px; padding: 0 5px; font-family: 'DejaVu Sans Mono', monospace;
|
|
311
|
+
}}
|
|
281
312
|
.jpt-frame-body {{ padding: 3px 8px; }}
|
|
282
313
|
.jpt-var-row {{
|
|
283
314
|
display: flex; align-items: center; padding: 1px 0; font-size: 12px; gap: 2px;
|
|
@@ -400,6 +431,7 @@ body {{
|
|
|
400
431
|
<div class="jpt-output-section">
|
|
401
432
|
<div class="jpt-output-label">Program Output</div>
|
|
402
433
|
<div class="jpt-stdout" id="stdout"></div>
|
|
434
|
+
<div class="jpt-error-banner" id="errorBanner" style="display:none;"></div>
|
|
403
435
|
<div class="jpt-input-badge" id="inputBadge" style="display:none;"></div>
|
|
404
436
|
</div>
|
|
405
437
|
<div class="jpt-main">
|
|
@@ -435,6 +467,7 @@ body {{
|
|
|
435
467
|
var slider = document.getElementById('slider');
|
|
436
468
|
var stepCounter = document.getElementById('stepCounter');
|
|
437
469
|
var stdout = document.getElementById('stdout');
|
|
470
|
+
var errorBanner = document.getElementById('errorBanner');
|
|
438
471
|
var codeDisplay = document.getElementById('codeDisplay');
|
|
439
472
|
var framesSection = document.getElementById('framesSection');
|
|
440
473
|
var heapContent = document.getElementById('heapContent');
|
|
@@ -543,7 +576,7 @@ body {{
|
|
|
543
576
|
return result;
|
|
544
577
|
}}
|
|
545
578
|
|
|
546
|
-
function renderCode(lineNo, prevLineNo) {{
|
|
579
|
+
function renderCode(lineNo, prevLineNo, errorLineNo) {{
|
|
547
580
|
if (!lineNo || lineNo < 1) lineNo = 1;
|
|
548
581
|
if (!prevLineNo) prevLineNo = 0;
|
|
549
582
|
var html = '';
|
|
@@ -552,10 +585,11 @@ body {{
|
|
|
552
585
|
var line = sourceLines[i] || '';
|
|
553
586
|
var highlighted = line.trim() ? highlightPython(line) : ' ';
|
|
554
587
|
var cls = '';
|
|
555
|
-
if (ln ===
|
|
588
|
+
if (errorLineNo && ln === errorLineNo) cls = ' jpt-error';
|
|
589
|
+
else if (ln === lineNo) cls = ' jpt-current';
|
|
556
590
|
else if (ln === prevLineNo) cls = ' jpt-executed';
|
|
557
591
|
html += '<div class="jpt-code-line' + cls + '">' +
|
|
558
|
-
'<span class="jpt-arrow"
|
|
592
|
+
'<span class="jpt-arrow">' + (errorLineNo && ln === errorLineNo ? '✗' : '▶') + '</span>' +
|
|
559
593
|
'<span class="jpt-lineno">' + ln + '</span>' +
|
|
560
594
|
'<span class="jpt-code-text">' + highlighted + '</span>' +
|
|
561
595
|
'</div>';
|
|
@@ -761,12 +795,19 @@ body {{
|
|
|
761
795
|
curStep = idx;
|
|
762
796
|
var step = steps[idx];
|
|
763
797
|
var prevLine = (idx > 0) ? steps[idx - 1].line : 0;
|
|
764
|
-
|
|
798
|
+
var isError = (step.event === 'exception' && step.error);
|
|
799
|
+
renderCode(step.line || 0, prevLine, isError ? step.line : 0);
|
|
765
800
|
renderFrames(step);
|
|
766
801
|
renderHeap(step);
|
|
767
802
|
stdout.textContent = step.stdout || '';
|
|
768
803
|
if (!step.stdout) {{ stdout.style.color = '#86efac'; stdout.style.fontStyle = 'italic'; stdout.textContent = '(no output yet)'; }}
|
|
769
804
|
else {{ stdout.style.color = '#166534'; stdout.style.fontStyle = 'normal'; }}
|
|
805
|
+
if (isError) {{
|
|
806
|
+
errorBanner.innerHTML = '<span class="jpt-error-banner-title">Uncaught exception</span>' + escapeHtml(step.error);
|
|
807
|
+
errorBanner.style.display = 'block';
|
|
808
|
+
}} else {{
|
|
809
|
+
errorBanner.style.display = 'none';
|
|
810
|
+
}}
|
|
770
811
|
updateInputBadge(step);
|
|
771
812
|
stepCounter.textContent = 'Step ' + (idx + 1) + ' of ' + nSteps;
|
|
772
813
|
slider.value = idx;
|
|
@@ -899,8 +940,17 @@ body {{
|
|
|
899
940
|
divider1.addEventListener('dblclick', toggleCollapse(divider1, codeSection));
|
|
900
941
|
divider2.addEventListener('dblclick', toggleCollapse(divider2, framesSection));
|
|
901
942
|
|
|
902
|
-
// Initial render
|
|
903
|
-
|
|
943
|
+
// Initial render — if the program raises an error, start on the
|
|
944
|
+
// first exception step (like Python Tutor, which steps to the error),
|
|
945
|
+
// so the red line + error message are visible immediately.
|
|
946
|
+
var initialStep = 0;
|
|
947
|
+
for (var si0 = 0; si0 < nSteps; si0++) {{
|
|
948
|
+
if (steps[si0].event === 'exception' && steps[si0].error) {{
|
|
949
|
+
initialStep = si0;
|
|
950
|
+
break;
|
|
951
|
+
}}
|
|
952
|
+
}}
|
|
953
|
+
renderStep(initialStep);
|
|
904
954
|
}})();
|
|
905
955
|
</script>
|
|
906
956
|
</body>
|
|
@@ -514,14 +514,35 @@ def _trace_code_internal(
|
|
|
514
514
|
# Same object always gets the same label across all steps.
|
|
515
515
|
id_map: dict[int, str] = {}
|
|
516
516
|
|
|
517
|
+
# Frames that have seen an 'exception' event but not yet resumed
|
|
518
|
+
# (no intervening 'line'/'call' in the same frame). A 'return' event
|
|
519
|
+
# from such a frame is a spurious interpreter artifact of exception
|
|
520
|
+
# unwinding (Python fires 'return' with arg=None as each frame is
|
|
521
|
+
# popped), NOT a real return — we suppress it so the student isn't
|
|
522
|
+
# shown a misleading "Return: None" step after an error.
|
|
523
|
+
pending_exc_frames: set[int] = set()
|
|
524
|
+
|
|
517
525
|
def _trace(frame, event, arg):
|
|
518
526
|
nonlocal step_count, _current_input_prompt, _current_input_index
|
|
519
527
|
if frame.f_code.co_filename != filename:
|
|
520
528
|
return _trace # skip frames from other files
|
|
521
529
|
if step_count >= max_steps:
|
|
522
530
|
raise _StepLimitExceeded() # stop execution, not just tracing
|
|
531
|
+
|
|
532
|
+
# Suppress the synthetic module-frame 'call' event. CPython fires it
|
|
533
|
+
# with f_lineno == 0 before the first line runs, which rendered a
|
|
534
|
+
# phantom "line 0" step. The first real 'line' event is step 1, so
|
|
535
|
+
# the visualization now starts at line 1 (matching Python Tutor).
|
|
536
|
+
if frame.f_code.co_name == "<module>" and event == "call":
|
|
537
|
+
return _trace
|
|
538
|
+
|
|
523
539
|
step_count += 1
|
|
524
540
|
|
|
541
|
+
if event in ("line", "call"):
|
|
542
|
+
# Execution resumed in this frame → any earlier exception was
|
|
543
|
+
# caught here, so clear its unwinding marker.
|
|
544
|
+
pending_exc_frames.discard(id(frame))
|
|
545
|
+
|
|
525
546
|
# Capture input info from the PREVIOUS step's execution.
|
|
526
547
|
# input() runs during line execution (between trace callbacks),
|
|
527
548
|
# so _current_input_prompt was set by the line that just ran.
|
|
@@ -561,7 +582,22 @@ def _trace_code_internal(
|
|
|
561
582
|
"input_index": step_input_index,
|
|
562
583
|
})
|
|
563
584
|
elif event == "return":
|
|
564
|
-
|
|
585
|
+
# Suppress the spurious 'return' (arg=None) that CPython emits
|
|
586
|
+
# when an exception unwinds a frame that did NOT catch it.
|
|
587
|
+
# (During unwinding even the module frame reports line 0, so
|
|
588
|
+
# this also keeps the phantom "line 0" out of error traces.)
|
|
589
|
+
if id(frame) in pending_exc_frames:
|
|
590
|
+
pending_exc_frames.discard(id(frame))
|
|
591
|
+
return _trace
|
|
592
|
+
pending_exc_frames.discard(id(frame))
|
|
593
|
+
if frame.f_code.co_name == "<module>":
|
|
594
|
+
# Scripts don't return values. Keep this step — it is the
|
|
595
|
+
# one that carries the FINAL state (all variables bound,
|
|
596
|
+
# complete stdout, since 'line' events fire before each
|
|
597
|
+
# statement) — but present no return value.
|
|
598
|
+
ret_repr = None
|
|
599
|
+
else:
|
|
600
|
+
ret_repr = _safe_repr(arg) if arg is not None else "None"
|
|
565
601
|
steps.append({
|
|
566
602
|
"line": lineno,
|
|
567
603
|
"source": source,
|
|
@@ -578,6 +614,15 @@ def _trace_code_internal(
|
|
|
578
614
|
# Don't record our internal exception
|
|
579
615
|
if exc_type is _StepLimitExceeded:
|
|
580
616
|
return None
|
|
617
|
+
if lineno == 0:
|
|
618
|
+
# The interpreter re-fires the 'exception' event as a frame
|
|
619
|
+
# finishes unwinding; that second event reports line 0
|
|
620
|
+
# (no source line). Only the first event — with the real
|
|
621
|
+
# line number — is worth showing.
|
|
622
|
+
return _trace
|
|
623
|
+
# Mark this frame as mid-unwind; a 'return' or re-fired
|
|
624
|
+
# 'exception' from it before resuming is spurious.
|
|
625
|
+
pending_exc_frames.add(id(frame))
|
|
581
626
|
steps.append({
|
|
582
627
|
"line": lineno,
|
|
583
628
|
"source": source,
|
|
@@ -603,17 +648,22 @@ def _trace_code_internal(
|
|
|
603
648
|
except _StepLimitExceeded:
|
|
604
649
|
pass # execution stopped due to step limit
|
|
605
650
|
except Exception as e:
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
651
|
+
# Safety net for exceptions the trace callback can't see (e.g.
|
|
652
|
+
# SyntaxError, which is raised before execution starts). If the
|
|
653
|
+
# trace already recorded the exception with its real line number,
|
|
654
|
+
# don't append a duplicate "line 0" step.
|
|
655
|
+
if not any(s.get("event") == "exception" for s in steps):
|
|
656
|
+
steps.append({
|
|
657
|
+
"line": 0,
|
|
658
|
+
"source": f"# {type(e).__name__}: {e}",
|
|
659
|
+
"call_stack": [],
|
|
660
|
+
"event": "exception",
|
|
661
|
+
"func": "<module>",
|
|
662
|
+
"error": f"{type(e).__name__}: {e}",
|
|
663
|
+
"stdout": captured.getvalue(),
|
|
664
|
+
"input_prompt": "",
|
|
665
|
+
"input_index": -1,
|
|
666
|
+
})
|
|
617
667
|
finally:
|
|
618
668
|
sys.settrace(old_trace)
|
|
619
669
|
sys.stdout = old_stdout
|
|
@@ -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.3"
|
|
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"
|
|
@@ -371,3 +371,48 @@ outer(5)
|
|
|
371
371
|
result = render_trace(steps, source_code=code)
|
|
372
372
|
assert "<iframe" in result
|
|
373
373
|
assert "outer" in result or "inner" in result
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class TestErrorDisplay:
|
|
377
|
+
"""Error steps must be delivered to and displayed by the frontend."""
|
|
378
|
+
|
|
379
|
+
def test_error_field_delivered_to_frontend(self):
|
|
380
|
+
code = 'x = 1+"1"\nprint(x)'
|
|
381
|
+
steps = trace_code(code)
|
|
382
|
+
result = render_trace(steps, source_code=code)
|
|
383
|
+
# The error message must survive into the embedded step JSON
|
|
384
|
+
# (it was previously dropped by the clean_step projection).
|
|
385
|
+
# (quotes are HTML-escaped inside the srcdoc, so match a stable
|
|
386
|
+
# unquoted substring)
|
|
387
|
+
assert "unsupported operand type(s) for +" in result
|
|
388
|
+
assert "int" in result and "str" in result
|
|
389
|
+
|
|
390
|
+
def test_frontend_has_error_rendering(self):
|
|
391
|
+
code = 'x = 1+"1"'
|
|
392
|
+
steps = trace_code(code)
|
|
393
|
+
inner = _get_srcdoc_content(render_trace(steps, source_code=code))
|
|
394
|
+
assert 'id="errorBanner"' in inner # banner element
|
|
395
|
+
assert ".jpt-code-line.jpt-error" in inner # red line CSS
|
|
396
|
+
assert "Uncaught exception" in inner # banner text
|
|
397
|
+
assert "jpt-error" in inner # line gets class
|
|
398
|
+
|
|
399
|
+
def test_frontend_starts_at_error_step(self):
|
|
400
|
+
code = 'x = 1+"1"\nprint(x)'
|
|
401
|
+
steps = trace_code(code)
|
|
402
|
+
inner = _get_srcdoc_content(render_trace(steps, source_code=code))
|
|
403
|
+
# Auto-jump: initial step is the first exception step
|
|
404
|
+
assert "initialStep" in inner
|
|
405
|
+
assert "event === 'exception'" in inner
|
|
406
|
+
|
|
407
|
+
def test_no_error_banner_for_successful_code(self):
|
|
408
|
+
steps = trace_code("x = 1\nprint(x)")
|
|
409
|
+
result = render_trace(steps, source_code="x = 1\nprint(x)")
|
|
410
|
+
# No exception steps → no error data in the embedded JSON
|
|
411
|
+
assert "Uncaught exception" in _get_srcdoc_content(result) # CSS/JS strings
|
|
412
|
+
# but no step carries an error payload
|
|
413
|
+
import html as H
|
|
414
|
+
src = _get_srcdoc_content(result)
|
|
415
|
+
jsline = [l for l in src.splitlines() if l.strip().startswith("var steps = ")][0]
|
|
416
|
+
import json
|
|
417
|
+
data = json.loads(jsline.strip()[len("var steps = "):-1])
|
|
418
|
+
assert not any(s.get("error") for s in data)
|
|
@@ -13,6 +13,10 @@ class TestBasicTracing:
|
|
|
13
13
|
def test_simple_assignment(self):
|
|
14
14
|
steps = get_trace("x = 3")
|
|
15
15
|
assert len(steps) >= 1
|
|
16
|
+
# No synthetic "call <module>" (line 0) step — the first step is
|
|
17
|
+
# the first real line.
|
|
18
|
+
assert steps[0]["event"] == "line"
|
|
19
|
+
assert steps[0]["line"] == 1
|
|
16
20
|
assert steps[-1]["event"] in ("line", "return")
|
|
17
21
|
|
|
18
22
|
def test_step_has_line_number(self):
|
|
@@ -282,6 +286,91 @@ class TestErrorHandling:
|
|
|
282
286
|
steps = get_trace("1/0")
|
|
283
287
|
assert any(s.get("event") == "exception" for s in steps)
|
|
284
288
|
|
|
289
|
+
def test_uncaught_error_has_correct_line_and_message(self):
|
|
290
|
+
steps = get_trace('x = 1+"1"\nprint(x)')
|
|
291
|
+
excs = [s for s in steps if s.get("event") == "exception"]
|
|
292
|
+
assert len(excs) >= 1
|
|
293
|
+
# The failing line is highlighted
|
|
294
|
+
assert any(s["line"] == 1 for s in excs)
|
|
295
|
+
# The error message matches Python's
|
|
296
|
+
first = excs[0]
|
|
297
|
+
assert "TypeError" in first["error"]
|
|
298
|
+
assert "unsupported operand type(s) for +: 'int' and 'str'" in first["error"]
|
|
299
|
+
|
|
300
|
+
def test_no_phantom_return_after_uncaught_error(self):
|
|
301
|
+
"""CPython fires a spurious 'return' (arg=None) when an uncaught
|
|
302
|
+
exception unwinds a frame. The tracer must not surface it as a
|
|
303
|
+
'Return: None' step."""
|
|
304
|
+
steps = get_trace('x = 1+"1"\nprint(x)')
|
|
305
|
+
for s in steps:
|
|
306
|
+
if s["event"] == "return":
|
|
307
|
+
assert s.get("return_value") != "None", \
|
|
308
|
+
f"phantom None-return step leaked: {s}"
|
|
309
|
+
# Exact step sequence: line 1 -> exception(s); no call/return steps
|
|
310
|
+
events = [s["event"] for s in steps]
|
|
311
|
+
assert events[0] == "line"
|
|
312
|
+
assert steps[0]["line"] == 1 # no phantom "call <module> line 0"
|
|
313
|
+
assert all(e == "exception" for e in events[1:])
|
|
314
|
+
|
|
315
|
+
def test_no_phantom_return_uncaught_in_nested_function(self):
|
|
316
|
+
steps = get_trace('def f():\n return 1/0\n\ny = f()\nprint(y)')
|
|
317
|
+
for s in steps:
|
|
318
|
+
if s["event"] == "return":
|
|
319
|
+
assert s.get("return_value") != "None"
|
|
320
|
+
assert any(s["event"] == "exception" and "division by zero" in s["error"]
|
|
321
|
+
for s in steps)
|
|
322
|
+
|
|
323
|
+
def test_real_return_kept_when_error_caught_in_same_frame(self):
|
|
324
|
+
code = ("def f():\n"
|
|
325
|
+
" try:\n"
|
|
326
|
+
" x = 1/0\n"
|
|
327
|
+
" except ZeroDivisionError:\n"
|
|
328
|
+
" x = 5\n"
|
|
329
|
+
" return x\n"
|
|
330
|
+
"\ny = f()\nprint(y)")
|
|
331
|
+
steps = get_trace(code)
|
|
332
|
+
returns = [s for s in steps if s["event"] == "return"]
|
|
333
|
+
# f() really returns 5 — that step must survive
|
|
334
|
+
assert any(s.get("return_value") == "5" for s in returns)
|
|
335
|
+
assert any(s["event"] == "exception" for s in steps)
|
|
336
|
+
|
|
337
|
+
def test_real_return_kept_when_error_caught_in_caller(self):
|
|
338
|
+
code = ("def f():\n"
|
|
339
|
+
" return 1/0\n"
|
|
340
|
+
"\ntry:\n"
|
|
341
|
+
" f()\n"
|
|
342
|
+
"except ZeroDivisionError:\n"
|
|
343
|
+
" pass\n"
|
|
344
|
+
'print("done")')
|
|
345
|
+
steps = get_trace(code)
|
|
346
|
+
# module finally returns None as a normal (non-unwind) return
|
|
347
|
+
assert any(s["event"] == "return" and s["func"] == "<module>"
|
|
348
|
+
for s in steps)
|
|
349
|
+
assert "done" in steps[-1]["stdout"]
|
|
350
|
+
|
|
351
|
+
def test_no_line0_step_and_final_state_preserved(self):
|
|
352
|
+
"""The synthetic module 'call' (line 0) must not be a step, and the
|
|
353
|
+
final module 'return' step must carry the complete final state
|
|
354
|
+
(all variables + all stdout) with NO return_value on the module."""
|
|
355
|
+
steps = get_trace("x = 3\ny = 4\nprint(x + y)")
|
|
356
|
+
assert steps[0]["line"] == 1
|
|
357
|
+
assert all(s["line"] >= 1 for s in steps)
|
|
358
|
+
last = steps[-1]
|
|
359
|
+
assert last["event"] == "return"
|
|
360
|
+
assert last["func"] == "<module>"
|
|
361
|
+
assert last.get("return_value") is None # scripts don't return values
|
|
362
|
+
gv = last["call_stack"][0]
|
|
363
|
+
assert gv["simple_vars"].get("x") == "3"
|
|
364
|
+
assert gv["simple_vars"].get("y") == "4"
|
|
365
|
+
assert "7" in last["stdout"]
|
|
366
|
+
|
|
367
|
+
def test_module_return_has_no_return_value(self):
|
|
368
|
+
steps = get_trace("x = 1")
|
|
369
|
+
for s in steps:
|
|
370
|
+
if s["event"] == "return" and s["func"] == "<module>":
|
|
371
|
+
assert s.get("return_value") is None, \
|
|
372
|
+
f"module frame must not show a return value: {s.get('return_value')!r}"
|
|
373
|
+
|
|
285
374
|
def test_max_steps_limit(self):
|
|
286
375
|
# Infinite loop should be stopped
|
|
287
376
|
code = "while True:\n pass"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_python_tutor-0.3.1 → jupyter_python_tutor-0.3.3}/jupyter_python_tutor.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|