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 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