jupyter-python-tutor 0.3.3__tar.gz → 0.3.4__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.3 → jupyter_python_tutor-0.3.4}/PKG-INFO +1 -1
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor/__init__.py +1 -1
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor/magics.py +83 -5
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor/renderer.py +76 -4
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor.egg-info/PKG-INFO +1 -1
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/pyproject.toml +1 -1
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/tests/test_magics.py +52 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/tests/test_renderer.py +54 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/LICENSE +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/README.md +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor/tracer.py +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor.egg-info/SOURCES.txt +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor.egg-info/dependency_links.txt +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor.egg-info/requires.txt +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor.egg-info/top_level.txt +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/setup.cfg +0 -0
- {jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/tests/test_tracer.py +0 -0
|
@@ -12,20 +12,31 @@ Options:
|
|
|
12
12
|
%%pytutor --raw — output the trace as JSON instead of HTML
|
|
13
13
|
%%pytutor --height 400 — set max-height of the variables panel (px)
|
|
14
14
|
%%pytutor --input 2026 — pre-collected input for input() calls
|
|
15
|
+
%%pytutor --no-scroll — window fits all content (no inner scrollbars)
|
|
16
|
+
|
|
17
|
+
Configure scrolling for the entire notebook:
|
|
18
|
+
|
|
19
|
+
%pytutor_config --no-scroll # default to fit-all (no inner scroll)
|
|
20
|
+
%pytutor_config --scroll # default to fixed window with inner scroll
|
|
21
|
+
%pytutor_config --reset # back to defaults
|
|
22
|
+
%pytutor_config # show current settings
|
|
15
23
|
"""
|
|
16
24
|
from __future__ import annotations
|
|
17
25
|
|
|
18
26
|
import argparse
|
|
19
27
|
import typing
|
|
20
28
|
|
|
21
|
-
from IPython.core.magic import Magics, magics_class, cell_magic
|
|
22
|
-
from IPython.display import HTML, JSON
|
|
29
|
+
from IPython.core.magic import Magics, magics_class, cell_magic, line_magic
|
|
30
|
+
from IPython.display import HTML, JSON, display
|
|
23
31
|
|
|
24
32
|
from .tracer import trace_code
|
|
25
33
|
from .renderer import render_trace
|
|
26
34
|
|
|
27
35
|
__all__ = ["TutorMagics", "load_ipython_extension", "unload_ipython_extension"]
|
|
28
36
|
|
|
37
|
+
# Defaults
|
|
38
|
+
_DEFAULT_SCROLL = True
|
|
39
|
+
|
|
29
40
|
|
|
30
41
|
def _make_parser() -> argparse.ArgumentParser:
|
|
31
42
|
parser = argparse.ArgumentParser(
|
|
@@ -50,19 +61,78 @@ def _make_parser() -> argparse.ArgumentParser:
|
|
|
50
61
|
"Repeat --input for multiple values: --input 2026 --input John. "
|
|
51
62
|
"If omitted and code contains input(), a dialog prompts for values.",
|
|
52
63
|
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--no-scroll", dest="no_scroll", action="store_true",
|
|
66
|
+
help="Fit-all for this cell: window grows to show all content, no inner scroll",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--scroll", dest="use_scroll", action="store_true",
|
|
70
|
+
help="Fixed window with inner scroll for this cell (overrides --no-scroll config)",
|
|
71
|
+
)
|
|
53
72
|
return parser
|
|
54
73
|
|
|
55
74
|
|
|
56
75
|
@magics_class
|
|
57
76
|
class TutorMagics(Magics):
|
|
58
|
-
"""Register the %%pytutor cell magic."""
|
|
77
|
+
"""Register the %%pytutor cell magic and %pytutor_config line magic."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, shell):
|
|
80
|
+
super().__init__(shell)
|
|
81
|
+
# Per-notebook config stored on the magics instance.
|
|
82
|
+
self._scroll = _DEFAULT_SCROLL
|
|
83
|
+
|
|
84
|
+
# ── %pytutor_config ──────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
@line_magic("pytutor_config")
|
|
87
|
+
def pytutor_config(self, line: str):
|
|
88
|
+
"""Configure how %%pytutor visualizations behave for this notebook.
|
|
89
|
+
|
|
90
|
+
Examples::
|
|
91
|
+
|
|
92
|
+
%pytutor_config --no-scroll # default to fit-all (no inner scroll)
|
|
93
|
+
%pytutor_config --scroll # default to fixed window with inner scroll
|
|
94
|
+
%pytutor_config --reset # back to defaults
|
|
95
|
+
%pytutor_config # show current settings
|
|
96
|
+
"""
|
|
97
|
+
parser = argparse.ArgumentParser(prog="%pytutor_config", add_help=False)
|
|
98
|
+
parser.add_argument("--no-scroll", dest="no_scroll", action="store_true",
|
|
99
|
+
help="Default to fit-all: window grows to show all content, no inner scroll")
|
|
100
|
+
parser.add_argument("--scroll", dest="use_scroll", action="store_true",
|
|
101
|
+
help="Default to fixed window with inner scroll (the default)")
|
|
102
|
+
parser.add_argument("--reset", action="store_true",
|
|
103
|
+
help="Reset all settings to defaults")
|
|
104
|
+
try:
|
|
105
|
+
args = parser.parse_known_args(line.split() if line else [])[0]
|
|
106
|
+
except SystemExit:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if args.reset:
|
|
110
|
+
self._scroll = _DEFAULT_SCROLL
|
|
111
|
+
display(HTML("<p>pytutor config reset to defaults.</p>"))
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# --no-scroll / --scroll are both store_true; --no-scroll wins if both given.
|
|
115
|
+
if args.no_scroll:
|
|
116
|
+
self._scroll = False
|
|
117
|
+
elif args.use_scroll:
|
|
118
|
+
self._scroll = True
|
|
119
|
+
|
|
120
|
+
# Show current settings
|
|
121
|
+
display(HTML(
|
|
122
|
+
"<table style='font-family:monospace;font-size:13px;'>"
|
|
123
|
+
f"<tr><td><b>Scrolling</b></td>"
|
|
124
|
+
f"<td>{'fixed window (inner scroll)' if self._scroll else 'fit-all (no inner scroll)'}</td></tr>"
|
|
125
|
+
"</table>"
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
# ── %%pytutor ────────────────────────────────────────────────────
|
|
59
129
|
|
|
60
130
|
@cell_magic("pytutor")
|
|
61
131
|
def pytutor(self, line: str, cell: str):
|
|
62
132
|
"""Trace the cell code and display an interactive visualization."""
|
|
63
133
|
parser = _make_parser()
|
|
64
134
|
try:
|
|
65
|
-
args = parser.
|
|
135
|
+
args, _unknown = parser.parse_known_args(line.split() if line else [])
|
|
66
136
|
except SystemExit:
|
|
67
137
|
return
|
|
68
138
|
|
|
@@ -87,6 +157,14 @@ class TutorMagics(Magics):
|
|
|
87
157
|
inputs=inputs,
|
|
88
158
|
)
|
|
89
159
|
|
|
160
|
+
# Scrolling: a cell flag wins; otherwise fall back to the notebook default.
|
|
161
|
+
if args.no_scroll:
|
|
162
|
+
scroll = False
|
|
163
|
+
elif args.use_scroll:
|
|
164
|
+
scroll = True
|
|
165
|
+
else:
|
|
166
|
+
scroll = self._scroll
|
|
167
|
+
|
|
90
168
|
if args.raw:
|
|
91
169
|
return JSON(steps)
|
|
92
170
|
else:
|
|
@@ -96,7 +174,7 @@ class TutorMagics(Magics):
|
|
|
96
174
|
# strips <style>, <script>, <iframe>, <input>, etc. from untrusted
|
|
97
175
|
# outputs. When the user runs the cell live, the output is trusted
|
|
98
176
|
# and the iframe renders with full interactivity (slider, SVG arrows).
|
|
99
|
-
result = render_trace(steps, source_code=cell, height=args.height)
|
|
177
|
+
result = render_trace(steps, source_code=cell, height=args.height, scroll=scroll)
|
|
100
178
|
import warnings
|
|
101
179
|
with warnings.catch_warnings():
|
|
102
180
|
# Suppress IPython's "Consider using IFrame instead" warning —
|
|
@@ -42,10 +42,22 @@ import json
|
|
|
42
42
|
__all__ = ["render_trace"]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def _generate_inner_html(
|
|
46
|
-
|
|
45
|
+
def _generate_inner_html(
|
|
46
|
+
steps: list[dict], source_code: str, height: int = 500, scroll: bool = True
|
|
47
|
+
) -> str:
|
|
48
|
+
"""Generate the full HTML page that goes inside the iframe srcdoc.
|
|
49
|
+
|
|
50
|
+
``scroll`` controls the initial scrolling mode:
|
|
51
|
+
- True (default): fixed-height window, the columns scroll internally.
|
|
52
|
+
- False: the window grows to fit ALL content (no inner scrollbars); the
|
|
53
|
+
parent page's auto-resize watcher stretches the iframe to match.
|
|
54
|
+
The mode can also be toggled at runtime via the "⤢ Fit all / ⇅ Scroll"
|
|
55
|
+
button in the navbar.
|
|
56
|
+
"""
|
|
47
57
|
if not steps:
|
|
48
58
|
return '<!DOCTYPE html><html><body><p style="color:red;">No trace steps to display.</p></body></html>'
|
|
59
|
+
initial_scroll_js = "true" if scroll else "false"
|
|
60
|
+
widget_cls = "jpt-widget" if scroll else "jpt-widget no-scroll"
|
|
49
61
|
|
|
50
62
|
# Determine source lines
|
|
51
63
|
if source_code:
|
|
@@ -144,6 +156,13 @@ body {{
|
|
|
144
156
|
}}
|
|
145
157
|
.jpt-navbar button:hover {{ background: #f0f0f0; }}
|
|
146
158
|
.jpt-navbar button:disabled {{ opacity: 0.4; cursor: default; }}
|
|
159
|
+
.jpt-mode-btn {{
|
|
160
|
+
margin-left: auto;
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
background: #fff;
|
|
163
|
+
}}
|
|
164
|
+
.jpt-mode-btn.active {{ background: #3b82f6; color: #fff; border-color: #2563eb; }}
|
|
165
|
+
.jpt-mode-btn.active:hover {{ background: #2563eb; }}
|
|
147
166
|
.jpt-slider {{
|
|
148
167
|
flex: 1;
|
|
149
168
|
min-width: 100px;
|
|
@@ -190,6 +209,24 @@ body {{
|
|
|
190
209
|
overflow: hidden;
|
|
191
210
|
min-height: 0;
|
|
192
211
|
}}
|
|
212
|
+
/* No-scroll ("fit all") mode: the widget grows to fit its full content and the
|
|
213
|
+
columns stop scrolling internally. The parent auto-resize watcher then
|
|
214
|
+
stretches the iframe to the new height. Placed after the base rules so it
|
|
215
|
+
wins the cascade. */
|
|
216
|
+
.jpt-widget.no-scroll {{
|
|
217
|
+
height: auto !important;
|
|
218
|
+
min-height: 120px;
|
|
219
|
+
}}
|
|
220
|
+
.jpt-widget.no-scroll .jpt-main,
|
|
221
|
+
.jpt-widget.no-scroll .jpt-right-panel {{
|
|
222
|
+
overflow: visible;
|
|
223
|
+
min-height: 0;
|
|
224
|
+
}}
|
|
225
|
+
.jpt-widget.no-scroll .jpt-code-section,
|
|
226
|
+
.jpt-widget.no-scroll .jpt-frames-section,
|
|
227
|
+
.jpt-widget.no-scroll .jpt-heap-section {{
|
|
228
|
+
overflow: visible;
|
|
229
|
+
}}
|
|
193
230
|
.jpt-code-section {{
|
|
194
231
|
flex: 0 0 40%;
|
|
195
232
|
min-width: 30px;
|
|
@@ -411,7 +448,7 @@ body {{
|
|
|
411
448
|
</style>
|
|
412
449
|
</head>
|
|
413
450
|
<body>
|
|
414
|
-
<div class="
|
|
451
|
+
<div class="{widget_cls}" id="jptWidget" style="height:{height}px;position:relative;">
|
|
415
452
|
<!-- Input dialog overlay (hidden by default) -->
|
|
416
453
|
<div class="jpt-input-overlay" id="inputOverlay" style="display:none;">
|
|
417
454
|
<div class="jpt-input-dialog">
|
|
@@ -427,6 +464,7 @@ body {{
|
|
|
427
464
|
<button id="btnNext" title="Next">▶</button>
|
|
428
465
|
<button id="btnLast" title="Last">⏭</button>
|
|
429
466
|
<span class="jpt-step-counter" id="stepCounter">Step 1 of {n_steps}</span>
|
|
467
|
+
<button id="btn-scroll-mode" class="jpt-mode-btn" title="Toggle scrolling: off = window fits all content (no inner scroll); on = fixed window with inner scroll">⇅ Scroll</button>
|
|
430
468
|
</div>
|
|
431
469
|
<div class="jpt-output-section">
|
|
432
470
|
<div class="jpt-output-label">Program Output</div>
|
|
@@ -940,6 +978,33 @@ body {{
|
|
|
940
978
|
divider1.addEventListener('dblclick', toggleCollapse(divider1, codeSection));
|
|
941
979
|
divider2.addEventListener('dblclick', toggleCollapse(divider2, framesSection));
|
|
942
980
|
|
|
981
|
+
// ── Scroll mode (fixed window + inner scroll ⇄ fit-all, no inner scroll) ──
|
|
982
|
+
// scrollMode true = fixed-height window, columns scroll internally.
|
|
983
|
+
// scrollMode false = window grows to fit ALL content (no inner scrollbars);
|
|
984
|
+
// the parent page's auto-resize watcher stretches the iframe to match.
|
|
985
|
+
var scrollMode = {initial_scroll_js};
|
|
986
|
+
var scrollBtn = document.getElementById('btn-scroll-mode');
|
|
987
|
+
var jptWidgetEl = document.querySelector('.jpt-widget');
|
|
988
|
+
function applyScrollMode() {{
|
|
989
|
+
jptWidgetEl.classList.toggle('no-scroll', !scrollMode);
|
|
990
|
+
if (scrollMode) {{
|
|
991
|
+
scrollBtn.textContent = '⇅ Scroll';
|
|
992
|
+
scrollBtn.classList.remove('active');
|
|
993
|
+
scrollBtn.title = 'Scrolling ON: fixed window, columns scroll internally. Click to fit all content (no inner scroll).';
|
|
994
|
+
}} else {{
|
|
995
|
+
scrollBtn.textContent = '⤢ Fit all';
|
|
996
|
+
scrollBtn.classList.add('active');
|
|
997
|
+
scrollBtn.title = 'Scrolling OFF: window fits all content. Click to return to fixed window with inner scroll.';
|
|
998
|
+
}}
|
|
999
|
+
// Re-lay arrows once the box has settled at its new size.
|
|
1000
|
+
requestAnimationFrame(function() {{ requestAnimationFrame(drawArrows); }});
|
|
1001
|
+
}}
|
|
1002
|
+
scrollBtn.onclick = function() {{
|
|
1003
|
+
scrollMode = !scrollMode;
|
|
1004
|
+
applyScrollMode();
|
|
1005
|
+
}};
|
|
1006
|
+
if (!scrollMode) {{ applyScrollMode(); }}
|
|
1007
|
+
|
|
943
1008
|
// Initial render — if the program raises an error, start on the
|
|
944
1009
|
// first exception step (like Python Tutor, which steps to the error),
|
|
945
1010
|
// so the red line + error message are visible immediately.
|
|
@@ -961,12 +1026,19 @@ def render_trace(
|
|
|
961
1026
|
steps: list[dict],
|
|
962
1027
|
source_code: str | None = None,
|
|
963
1028
|
height: int = 500,
|
|
1029
|
+
scroll: bool = True,
|
|
964
1030
|
) -> str:
|
|
965
1031
|
"""Render trace steps as interactive HTML inside an iframe.
|
|
966
1032
|
|
|
967
1033
|
The entire visualization (HTML + CSS + JS) is wrapped in
|
|
968
1034
|
``<iframe srcdoc="...">`` which works in trusted JupyterLab notebooks.
|
|
969
1035
|
|
|
1036
|
+
``height`` sets the fixed window height in *scrolling* mode. ``scroll``
|
|
1037
|
+
sets the initial scrolling mode: ``True`` (default) = fixed window with
|
|
1038
|
+
inner column scrollbars; ``False`` = the window grows to fit all content
|
|
1039
|
+
(no inner scrollbars). Students can flip it at any time with the
|
|
1040
|
+
"⤢ Fit all / ⇅ Scroll" button in the navbar.
|
|
1041
|
+
|
|
970
1042
|
Features:
|
|
971
1043
|
- Fixed widget height with internal scrolling — no "dancing" cell
|
|
972
1044
|
- Slider navigation (``<input type="range">``) — drag to scrub through steps
|
|
@@ -977,7 +1049,7 @@ def render_trace(
|
|
|
977
1049
|
- Heap only shows objects reachable from the current call stack
|
|
978
1050
|
- Keyboard navigation (← → arrows)
|
|
979
1051
|
"""
|
|
980
|
-
inner = _generate_inner_html(steps, source_code or "", height)
|
|
1052
|
+
inner = _generate_inner_html(steps, source_code or "", height, scroll=scroll)
|
|
981
1053
|
# Escape for srcdoc attribute
|
|
982
1054
|
escaped = html.escape(inner, quote=True)
|
|
983
1055
|
# Match cpp-tutor's approach exactly to prevent "dancing":
|
|
@@ -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.4"
|
|
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"
|
|
@@ -77,3 +77,55 @@ c = Circle('red', 5)
|
|
|
77
77
|
def test_height_option(self, ipython):
|
|
78
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)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestScrollModeMagic:
|
|
83
|
+
"""Test the --no-scroll/--scroll cell flag and the %pytutor_config line magic."""
|
|
84
|
+
|
|
85
|
+
def _run_cell_capturing_scroll(self, ipython, line=""):
|
|
86
|
+
"""Run %%pytutor with render_trace mocked; return the scroll kwarg passed."""
|
|
87
|
+
from unittest import mock
|
|
88
|
+
with mock.patch(
|
|
89
|
+
"jupyter_python_tutor.magics.render_trace",
|
|
90
|
+
return_value="<html></html>",
|
|
91
|
+
) as mr:
|
|
92
|
+
ipython.run_cell_magic("pytutor", line, "x = 3")
|
|
93
|
+
assert mr.called, "render_trace was not called"
|
|
94
|
+
return mr.call_args.kwargs.get("scroll")
|
|
95
|
+
|
|
96
|
+
def test_config_magic_registered(self, ipython):
|
|
97
|
+
assert "pytutor_config" in ipython.magics_manager.magics["line"]
|
|
98
|
+
|
|
99
|
+
def test_default_scroll_true(self, ipython):
|
|
100
|
+
ipython.run_line_magic("pytutor_config", "--reset")
|
|
101
|
+
assert self._run_cell_capturing_scroll(ipython) is True
|
|
102
|
+
|
|
103
|
+
def test_cell_no_scroll_flag(self, ipython):
|
|
104
|
+
ipython.run_line_magic("pytutor_config", "--reset")
|
|
105
|
+
assert self._run_cell_capturing_scroll(ipython, line="--no-scroll") is False
|
|
106
|
+
|
|
107
|
+
def test_cell_scroll_overrides_config(self, ipython):
|
|
108
|
+
ipython.run_line_magic("pytutor_config", "--no-scroll")
|
|
109
|
+
# a cell --scroll wins over the notebook --no-scroll default
|
|
110
|
+
assert self._run_cell_capturing_scroll(ipython, line="--scroll") is True
|
|
111
|
+
|
|
112
|
+
def test_config_no_scroll(self, ipython):
|
|
113
|
+
ipython.run_line_magic("pytutor_config", "--no-scroll")
|
|
114
|
+
assert self._run_cell_capturing_scroll(ipython) is False
|
|
115
|
+
|
|
116
|
+
def test_config_scroll_restores(self, ipython):
|
|
117
|
+
ipython.run_line_magic("pytutor_config", "--no-scroll")
|
|
118
|
+
ipython.run_line_magic("pytutor_config", "--scroll")
|
|
119
|
+
assert self._run_cell_capturing_scroll(ipython) is True
|
|
120
|
+
|
|
121
|
+
def test_config_reset(self, ipython):
|
|
122
|
+
ipython.run_line_magic("pytutor_config", "--no-scroll")
|
|
123
|
+
ipython.run_line_magic("pytutor_config", "--reset")
|
|
124
|
+
assert self._run_cell_capturing_scroll(ipython) is True
|
|
125
|
+
|
|
126
|
+
def test_config_no_args_shows_setting(self, ipython):
|
|
127
|
+
ipython.run_line_magic("pytutor_config", "--no-scroll")
|
|
128
|
+
# running with no args displays the current setting (no exception)
|
|
129
|
+
ipython.run_line_magic("pytutor_config", "")
|
|
130
|
+
# and it is still no-scroll
|
|
131
|
+
assert self._run_cell_capturing_scroll(ipython) is False
|
|
@@ -416,3 +416,57 @@ class TestErrorDisplay:
|
|
|
416
416
|
import json
|
|
417
417
|
data = json.loads(jsline.strip()[len("var steps = "):-1])
|
|
418
418
|
assert not any(s.get("error") for s in data)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class TestScrollMode:
|
|
422
|
+
"""Test the optional scrolling (fixed window ⇄ fit-all) feature."""
|
|
423
|
+
|
|
424
|
+
def _inner(self, scroll: bool) -> str:
|
|
425
|
+
steps = trace_code("x = 1\ny = 2")
|
|
426
|
+
return _get_srcdoc_content(
|
|
427
|
+
render_trace(steps, source_code="x = 1\ny = 2", scroll=scroll)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def test_default_is_scroll_mode(self):
|
|
431
|
+
inner = _get_srcdoc_content(render_trace(trace_code("x = 1"), source_code="x = 1"))
|
|
432
|
+
# Default scroll=True: no 'no-scroll' class on the widget
|
|
433
|
+
assert 'class="jpt-widget no-scroll"' not in inner
|
|
434
|
+
assert "var scrollMode = true;" in inner
|
|
435
|
+
|
|
436
|
+
def test_no_scroll_mode_class(self):
|
|
437
|
+
inner = self._inner(False)
|
|
438
|
+
assert 'class="jpt-widget no-scroll"' in inner
|
|
439
|
+
assert "var scrollMode = false;" in inner
|
|
440
|
+
|
|
441
|
+
def test_no_scroll_css_present(self):
|
|
442
|
+
inner = self._inner(False)
|
|
443
|
+
assert ".jpt-widget.no-scroll" in inner
|
|
444
|
+
# columns stop scrolling in fit-all
|
|
445
|
+
assert ".jpt-widget.no-scroll .jpt-code-section" in inner
|
|
446
|
+
|
|
447
|
+
def test_mode_button_present(self):
|
|
448
|
+
inner = self._inner(False)
|
|
449
|
+
assert 'id="btn-scroll-mode"' in inner
|
|
450
|
+
assert "jpt-mode-btn" in inner
|
|
451
|
+
|
|
452
|
+
def test_mode_button_active_state_in_js(self):
|
|
453
|
+
inner = self._inner(False)
|
|
454
|
+
# applyScrollMode swaps label + active class
|
|
455
|
+
assert "applyScrollMode" in inner
|
|
456
|
+
assert "Fit all" in inner
|
|
457
|
+
assert "⇅ Scroll" in inner
|
|
458
|
+
|
|
459
|
+
def test_toggle_redraws_arrows(self):
|
|
460
|
+
inner = self._inner(False)
|
|
461
|
+
# fitting all changes layout, so arrows must be re-laid out
|
|
462
|
+
assert "requestAnimationFrame(drawArrows)" in inner
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class TestScrollParamSignature:
|
|
466
|
+
"""render_trace / _generate_inner_html accept a scroll parameter."""
|
|
467
|
+
|
|
468
|
+
def test_render_trace_has_scroll_param(self):
|
|
469
|
+
import inspect
|
|
470
|
+
sig = inspect.signature(render_trace)
|
|
471
|
+
assert "scroll" in sig.parameters
|
|
472
|
+
assert sig.parameters["scroll"].default is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{jupyter_python_tutor-0.3.3 → jupyter_python_tutor-0.3.4}/jupyter_python_tutor.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|