codelens-widget 0.1.28__py3-none-any.whl
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.
- codelens_widget/NOTICE +16 -0
- codelens_widget/__init__.py +315 -0
- codelens_widget/pg_encoder.py +545 -0
- codelens_widget/pg_logger.py +1706 -0
- codelens_widget/vendor/d3.v2.min.js +4 -0
- codelens_widget/vendor/jquery-ui.min.css +7 -0
- codelens_widget/vendor/jquery-ui.min.js +13 -0
- codelens_widget/vendor/jquery.ba-bbq.min.js +18 -0
- codelens_widget/vendor/jquery.min.js +2 -0
- codelens_widget/vendor/jquery.qtip.css +573 -0
- codelens_widget/vendor/jquery.qtip.min.js +2 -0
- codelens_widget/vendor/jsplumb.min.js +1 -0
- codelens_widget/vendor/pytutor.css +920 -0
- codelens_widget/vendor/pytutor.js +4859 -0
- codelens_widget-0.1.28.dist-info/METADATA +76 -0
- codelens_widget-0.1.28.dist-info/RECORD +33 -0
- codelens_widget-0.1.28.dist-info/WHEEL +5 -0
- codelens_widget-0.1.28.dist-info/licenses/LICENSE +674 -0
- codelens_widget-0.1.28.dist-info/top_level.txt +2 -0
- pytutor_widget/NOTICE +16 -0
- pytutor_widget/__init__.py +315 -0
- pytutor_widget/pg_encoder.py +545 -0
- pytutor_widget/pg_logger.py +1706 -0
- pytutor_widget/vendor/d3.v2.min.js +4 -0
- pytutor_widget/vendor/jquery-ui.min.css +7 -0
- pytutor_widget/vendor/jquery-ui.min.js +13 -0
- pytutor_widget/vendor/jquery.ba-bbq.min.js +18 -0
- pytutor_widget/vendor/jquery.min.js +2 -0
- pytutor_widget/vendor/jquery.qtip.css +573 -0
- pytutor_widget/vendor/jquery.qtip.min.js +2 -0
- pytutor_widget/vendor/jsplumb.min.js +1 -0
- pytutor_widget/vendor/pytutor.css +920 -0
- pytutor_widget/vendor/pytutor.js +4859 -0
codelens_widget/NOTICE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
This package vendors Online Python Tutor by Philip J. Guo (philip@pgbovine.net),
|
|
2
|
+
which is distributed under the MIT License.
|
|
3
|
+
|
|
4
|
+
Online Python Tutor
|
|
5
|
+
https://github.com/pgbovine/OnlinePythonTutor/
|
|
6
|
+
Copyright (C) Philip J. Guo
|
|
7
|
+
|
|
8
|
+
Vendored files retain their original MIT license headers:
|
|
9
|
+
- pg_encoder.py, pg_logger.py (Python backend / trace generator)
|
|
10
|
+
- vendor/codelens.js, vendor/codelens.css (frontend visualizer)
|
|
11
|
+
- vendor/jsplumb.min.js (jsPlumb 1.3.10), vendor/jquery*, vendor/d3.v2.min.js,
|
|
12
|
+
vendor/jquery-ui.* , vendor/jquery.qtip.* (third-party libraries OPT depends on,
|
|
13
|
+
under their respective MIT/permissive licenses)
|
|
14
|
+
|
|
15
|
+
The codelens_widget wrapper code (__init__.py and this packaging) only adapts and
|
|
16
|
+
embeds the above; it makes no claim over the vendored works.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codelens_widget
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
A CodeLens-style execution visualizer for Jupyter that uses **Philip Guo's real
|
|
6
|
+
Online Python Tutor frontend** (`codelens.js`). The trace is produced in the live
|
|
7
|
+
kernel by Guo's own ``pg_logger`` (vendored, MIT-licensed), so the data is in his
|
|
8
|
+
exact schema; it is then handed to ``codelens.js`` rendered inside a self-contained
|
|
9
|
+
``<iframe>`` so the legacy jQuery/jsPlumb visualizer runs in the clean document it
|
|
10
|
+
expects. Everything (codelens.js, jsPlumb 1.3.10, jQuery, jQuery UI, D3 v2) is
|
|
11
|
+
vendored under ``vendor/``, so it works fully offline and identically across
|
|
12
|
+
VS Code, JupyterLab, Notebook 7, and Colab.
|
|
13
|
+
|
|
14
|
+
Usage
|
|
15
|
+
-----
|
|
16
|
+
from codelens_widget import CodeLens
|
|
17
|
+
|
|
18
|
+
CodeLens('''
|
|
19
|
+
def insertion_sort(a):
|
|
20
|
+
for i in range(1, len(a)):
|
|
21
|
+
key = a[i]
|
|
22
|
+
j = i - 1
|
|
23
|
+
while j >= 0 and a[j] > key:
|
|
24
|
+
a[j + 1] = a[j]
|
|
25
|
+
j -= 1
|
|
26
|
+
a[j + 1] = key
|
|
27
|
+
return a
|
|
28
|
+
|
|
29
|
+
data = [5, 2, 9, 1]
|
|
30
|
+
insertion_sort(data)
|
|
31
|
+
''')
|
|
32
|
+
|
|
33
|
+
Or, after import, the cell magic visualizes a whole cell::
|
|
34
|
+
|
|
35
|
+
%%codelens
|
|
36
|
+
x = [1, 2, 3]
|
|
37
|
+
y = x
|
|
38
|
+
y.append(4)
|
|
39
|
+
|
|
40
|
+
Attribution
|
|
41
|
+
-----------
|
|
42
|
+
Online Python Tutor (``pg_logger.py``, ``pg_encoder.py``, ``codelens.js`` and the
|
|
43
|
+
bundled libraries) is Copyright (C) Philip J. Guo, released under the MIT license.
|
|
44
|
+
See the headers of the vendored files. This package only wraps and embeds it.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import json
|
|
50
|
+
import os
|
|
51
|
+
import re
|
|
52
|
+
|
|
53
|
+
import anywidget
|
|
54
|
+
import traitlets
|
|
55
|
+
|
|
56
|
+
from . import pg_logger
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
from IPython import get_ipython
|
|
60
|
+
from IPython.display import display as _ipy_display
|
|
61
|
+
except Exception: # pragma: no cover
|
|
62
|
+
def get_ipython():
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def _ipy_display(*a, **k):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["CodeLens", "trace_code", "register_codelens_magic"]
|
|
70
|
+
|
|
71
|
+
_VENDOR = os.path.join(os.path.dirname(__file__), "vendor")
|
|
72
|
+
|
|
73
|
+
# Load order matters: jQuery -> jQuery UI -> D3 -> jsPlumb -> ba-bbq -> qtip -> codelens.
|
|
74
|
+
_JS_FILES = [
|
|
75
|
+
"jquery.min.js",
|
|
76
|
+
"jquery-ui.min.js",
|
|
77
|
+
"d3.v2.min.js",
|
|
78
|
+
"jsplumb.min.js",
|
|
79
|
+
"jquery.ba-bbq.min.js",
|
|
80
|
+
"jquery.qtip.min.js",
|
|
81
|
+
"codelens.js",
|
|
82
|
+
]
|
|
83
|
+
_CSS_FILES = ["jquery-ui.min.css", "jquery.qtip.css", "codelens.css"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _read_vendor(name):
|
|
87
|
+
with open(os.path.join(_VENDOR, name), "r", encoding="utf-8") as fh:
|
|
88
|
+
return fh.read()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _html_inline_safe(text):
|
|
92
|
+
r"""Defuse sequences that would prematurely terminate an inlined ``<script>``.
|
|
93
|
+
|
|
94
|
+
The vendor bundle is inlined verbatim into an HTML ``<script>`` element, but
|
|
95
|
+
``codelens.js`` contains literal ``</script>`` and ``<!--`` -- in its header
|
|
96
|
+
comment (which shows example ``<script src=...>`` includes) and in one string
|
|
97
|
+
that builds a ``<script>`` block. The browser's HTML tokenizer ends the
|
|
98
|
+
element at the first ``</script`` and is knocked into its "escaped" state by
|
|
99
|
+
``<!--``, so without this everything after codelens.js's header (including the
|
|
100
|
+
global ``ExecutionVisualizer`` definition) is parsed as stray HTML and never
|
|
101
|
+
runs. Backslash-escaping the ``<`` lead-in hides the tag from the tokenizer
|
|
102
|
+
while staying inert in JS: inside a string ``"<\/script>"`` == ``"</script>"``
|
|
103
|
+
and ``"<\!--"`` == ``"<!--"``, and inside a comment it is just text. (We do
|
|
104
|
+
*not* touch the ``<script`` opening tag -- it is harmless in script-data state
|
|
105
|
+
and also occurs inside minified jQuery/qTip code.) This generalises the guard
|
|
106
|
+
that ``_build_html`` already applies to the embedded trace JSON.
|
|
107
|
+
"""
|
|
108
|
+
text = re.sub(r"</(script|style)", lambda m: "<\\/" + m.group(1), text, flags=re.IGNORECASE)
|
|
109
|
+
return text.replace("<!--", "<\\!--")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Concatenate the vendor bundle once at import (module-level cache).
|
|
113
|
+
def _load_bundle():
|
|
114
|
+
js = "\n;\n".join(_read_vendor(n) for n in _JS_FILES)
|
|
115
|
+
css = "\n".join(_read_vendor(n) for n in _CSS_FILES)
|
|
116
|
+
return _html_inline_safe(js), _html_inline_safe(css)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_BUNDLE_JS, _BUNDLE_CSS = _load_bundle()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------------- #
|
|
123
|
+
# Trace generation -- Guo's exact schema, via the vendored logger. #
|
|
124
|
+
# --------------------------------------------------------------------------- #
|
|
125
|
+
|
|
126
|
+
def trace_code(code, cumulative_mode=False, heap_primitives=False,
|
|
127
|
+
allow_all_modules=True):
|
|
128
|
+
"""Return ``{"code": <source>, "trace": [...]}`` in Online Python Tutor's
|
|
129
|
+
exact format, produced by Guo's own ``pg_logger``."""
|
|
130
|
+
|
|
131
|
+
def finalizer(input_code, output_trace):
|
|
132
|
+
return {"code": input_code, "trace": output_trace}
|
|
133
|
+
|
|
134
|
+
return pg_logger.exec_script_str_local(
|
|
135
|
+
code, None, cumulative_mode, heap_primitives, finalizer,
|
|
136
|
+
allow_all_modules=allow_all_modules,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --------------------------------------------------------------------------- #
|
|
141
|
+
# Self-contained HTML document for the iframe. #
|
|
142
|
+
# --------------------------------------------------------------------------- #
|
|
143
|
+
|
|
144
|
+
_DEFAULT_OPTIONS = {
|
|
145
|
+
"embeddedMode": True,
|
|
146
|
+
"lang": "py3",
|
|
147
|
+
"disableHeapNesting": False,
|
|
148
|
+
"drawParentPointers": False,
|
|
149
|
+
"textualMemoryLabels": False,
|
|
150
|
+
"showOnlyOutputs": False,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _build_html(trace_data, options):
|
|
155
|
+
trace_json = json.dumps(trace_data)
|
|
156
|
+
# neutralize any "</script>" that might appear inside string data
|
|
157
|
+
trace_json = trace_json.replace("</", "<\\/")
|
|
158
|
+
opts_json = json.dumps(options)
|
|
159
|
+
|
|
160
|
+
return """<!DOCTYPE html>
|
|
161
|
+
<html><head><meta charset="utf-8">
|
|
162
|
+
<style>
|
|
163
|
+
""" + _BUNDLE_CSS + """
|
|
164
|
+
html, body { margin: 0; padding: 6px; background: #fff;
|
|
165
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
166
|
+
#viz { overflow: visible; }
|
|
167
|
+
.cl-fail { color: #b00; font: 13px ui-monospace, Menlo, monospace; padding: 8px; }
|
|
168
|
+
</style>
|
|
169
|
+
</head><body>
|
|
170
|
+
<div id="viz"></div>
|
|
171
|
+
<script>
|
|
172
|
+
""" + _BUNDLE_JS + """
|
|
173
|
+
</script>
|
|
174
|
+
<script>
|
|
175
|
+
(function () {
|
|
176
|
+
var FRAME_ID = "__OPT_FRAME_ID__";
|
|
177
|
+
var traceData = """ + trace_json + """;
|
|
178
|
+
var options = """ + opts_json + """;
|
|
179
|
+
function reportHeight() {
|
|
180
|
+
try {
|
|
181
|
+
var h = document.body.scrollHeight;
|
|
182
|
+
parent.postMessage({ __opt_height: h, __opt_id: FRAME_ID }, "*");
|
|
183
|
+
} catch (e) {}
|
|
184
|
+
}
|
|
185
|
+
function boot() {
|
|
186
|
+
try {
|
|
187
|
+
window.__optViz = new ExecutionVisualizer("viz", traceData, options);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
document.getElementById("viz").innerHTML =
|
|
190
|
+
'<div class="cl-fail">codelens failed to render: ' + (e && e.message) + '</div>';
|
|
191
|
+
}
|
|
192
|
+
[50, 300, 800].forEach(function (t) { setTimeout(reportHeight, t); });
|
|
193
|
+
window.addEventListener("resize", reportHeight);
|
|
194
|
+
}
|
|
195
|
+
if (window.jQuery) { jQuery(boot); } else { window.addEventListener("load", boot); }
|
|
196
|
+
})();
|
|
197
|
+
</script>
|
|
198
|
+
</body></html>"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --------------------------------------------------------------------------- #
|
|
202
|
+
# Widget #
|
|
203
|
+
# --------------------------------------------------------------------------- #
|
|
204
|
+
|
|
205
|
+
_ESM = r"""
|
|
206
|
+
function render({ model, el }){
|
|
207
|
+
el.innerHTML = "";
|
|
208
|
+
const frameId = "opt_" + Math.random().toString(36).slice(2);
|
|
209
|
+
const iframe = document.createElement("iframe");
|
|
210
|
+
iframe.style.width = "100%";
|
|
211
|
+
iframe.style.boxSizing = "border-box"; // count the 1px border inside 100% so the right edge isn't clipped
|
|
212
|
+
iframe.style.display = "block"; // avoid the inline-element descender gap below the frame
|
|
213
|
+
iframe.style.height = (model.get("height") || 520) + "px";
|
|
214
|
+
iframe.style.border = "1px solid #d0d0d8";
|
|
215
|
+
iframe.style.borderRadius = "8px";
|
|
216
|
+
iframe.style.background = "#fff";
|
|
217
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
218
|
+
iframe.setAttribute("scrolling", "auto");
|
|
219
|
+
|
|
220
|
+
const html = (model.get("srcdoc") || "").replace("__OPT_FRAME_ID__", frameId);
|
|
221
|
+
iframe.srcdoc = html;
|
|
222
|
+
el.appendChild(iframe);
|
|
223
|
+
|
|
224
|
+
function onMessage(ev){
|
|
225
|
+
const d = ev.data;
|
|
226
|
+
if (d && d.__opt_id === frameId && typeof d.__opt_height === "number"){
|
|
227
|
+
iframe.style.height = Math.max(200, d.__opt_height + 10) + "px";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
window.addEventListener("message", onMessage);
|
|
231
|
+
|
|
232
|
+
const onHeight = () => { iframe.style.height = (model.get("height") || 520) + "px"; };
|
|
233
|
+
model.on("change:height", onHeight);
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
window.removeEventListener("message", onMessage);
|
|
237
|
+
model.off("change:height", onHeight);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
export default { render };
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class CodeLens(anywidget.AnyWidget):
|
|
245
|
+
_esm = _ESM
|
|
246
|
+
|
|
247
|
+
srcdoc = traitlets.Unicode("").tag(sync=True)
|
|
248
|
+
height = traitlets.Int(520).tag(sync=True)
|
|
249
|
+
|
|
250
|
+
def __init__(self, code, height=520, lang="py3", options=None,
|
|
251
|
+
cumulative_mode=False, heap_primitives=False):
|
|
252
|
+
super().__init__()
|
|
253
|
+
if not isinstance(code, str):
|
|
254
|
+
raise TypeError("CodeLens(code) expects a source string")
|
|
255
|
+
self.height = height
|
|
256
|
+
opts = dict(_DEFAULT_OPTIONS)
|
|
257
|
+
opts["lang"] = lang
|
|
258
|
+
opts["heapPrimitives"] = heap_primitives
|
|
259
|
+
if options:
|
|
260
|
+
opts.update(options)
|
|
261
|
+
data = trace_code(code, cumulative_mode=cumulative_mode,
|
|
262
|
+
heap_primitives=heap_primitives)
|
|
263
|
+
self.srcdoc = _build_html(data, opts)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def register_codelens_magic(ipython=None):
|
|
267
|
+
r"""Register the ``%%codelens`` cell magic.
|
|
268
|
+
|
|
269
|
+
In IPython/Jupyter, prefixing a cell with ``%%codelens`` visualizes the cell
|
|
270
|
+
body with :class:`CodeLens` (the cell is traced and rendered, not run the
|
|
271
|
+
usual way). Optional flags on the magic line mirror the constructor::
|
|
272
|
+
|
|
273
|
+
%%codelens --height 600 --lang py3
|
|
274
|
+
x = [1, 2, 3]
|
|
275
|
+
y = x
|
|
276
|
+
y.append(4)
|
|
277
|
+
|
|
278
|
+
Called automatically on import; returns ``True`` when a live shell is found,
|
|
279
|
+
``False`` otherwise (e.g. plain Python).
|
|
280
|
+
"""
|
|
281
|
+
ip = ipython or get_ipython()
|
|
282
|
+
if ip is None:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
from IPython.core.magic_arguments import (argument, magic_arguments,
|
|
286
|
+
parse_argstring)
|
|
287
|
+
|
|
288
|
+
@magic_arguments()
|
|
289
|
+
@argument("--height", type=int, default=520,
|
|
290
|
+
help="iframe height in pixels (default: 520)")
|
|
291
|
+
@argument("--lang", default="py3", choices=("py2", "py3"),
|
|
292
|
+
help="tracer language mode (default: py3)")
|
|
293
|
+
@argument("--cumulative", action="store_true",
|
|
294
|
+
help="accumulate every executed line in the trace")
|
|
295
|
+
@argument("--heap-primitives", action="store_true",
|
|
296
|
+
help="render primitive values as separate heap objects")
|
|
297
|
+
def codelens(line, cell):
|
|
298
|
+
args = parse_argstring(codelens, line)
|
|
299
|
+
if not (cell and cell.strip()):
|
|
300
|
+
print("%%codelens: cell is empty -- put Python code below the magic line.")
|
|
301
|
+
return
|
|
302
|
+
_ipy_display(CodeLens(
|
|
303
|
+
cell, height=args.height, lang=args.lang,
|
|
304
|
+
cumulative_mode=args.cumulative,
|
|
305
|
+
heap_primitives=args.heap_primitives,
|
|
306
|
+
))
|
|
307
|
+
|
|
308
|
+
ip.register_magic_function(codelens, magic_kind="cell", magic_name="codelens")
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
register_codelens_magic()
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|