lectrace 1.0.0__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.
- lectrace-1.0.0/.gitignore +17 -0
- lectrace-1.0.0/PKG-INFO +185 -0
- lectrace-1.0.0/README.md +174 -0
- lectrace-1.0.0/frontend/App.tsx +13 -0
- lectrace-1.0.0/frontend/components/TraceApp.tsx +46 -0
- lectrace-1.0.0/frontend/components/TraceBrowser.tsx +39 -0
- lectrace-1.0.0/frontend/components/TraceViewer/EnvPanel.tsx +64 -0
- lectrace-1.0.0/frontend/components/TraceViewer/LinesPanel.tsx +148 -0
- lectrace-1.0.0/frontend/components/TraceViewer/NavigationBar.tsx +50 -0
- lectrace-1.0.0/frontend/components/TraceViewer/index.tsx +124 -0
- lectrace-1.0.0/frontend/components/renderers/ExternalLinkCard.tsx +48 -0
- lectrace-1.0.0/frontend/components/renderers/MarkdownView.tsx +23 -0
- lectrace-1.0.0/frontend/components/renderers/PlotView.tsx +22 -0
- lectrace-1.0.0/frontend/components/renderers/TensorView.tsx +56 -0
- lectrace-1.0.0/frontend/components/renderers/ValueRenderer.tsx +77 -0
- lectrace-1.0.0/frontend/eslint.config.js +25 -0
- lectrace-1.0.0/frontend/hooks/useDrag.ts +37 -0
- lectrace-1.0.0/frontend/hooks/useKeyboard.ts +27 -0
- lectrace-1.0.0/frontend/hooks/useNavigation.ts +107 -0
- lectrace-1.0.0/frontend/hooks/useTrace.ts +45 -0
- lectrace-1.0.0/frontend/index.css +491 -0
- lectrace-1.0.0/frontend/index.html +17 -0
- lectrace-1.0.0/frontend/main.tsx +9 -0
- lectrace-1.0.0/frontend/package-lock.json +5452 -0
- lectrace-1.0.0/frontend/package.json +39 -0
- lectrace-1.0.0/frontend/tsconfig.json +15 -0
- lectrace-1.0.0/frontend/types/trace.ts +82 -0
- lectrace-1.0.0/frontend/utils/format.ts +29 -0
- lectrace-1.0.0/frontend/utils/navigation.ts +2 -0
- lectrace-1.0.0/frontend/vite.config.ts +11 -0
- lectrace-1.0.0/lectrace/__init__.py +9 -0
- lectrace-1.0.0/lectrace/_init.py +62 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- lectrace-1.0.0/lectrace/_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- lectrace-1.0.0/lectrace/_static/assets/index-BrrzxwDR.css +10 -0
- lectrace-1.0.0/lectrace/_static/assets/index-hXDH48Z0.js +542 -0
- lectrace-1.0.0/lectrace/_static/index.html +18 -0
- lectrace-1.0.0/lectrace/_templates/docs.yml +44 -0
- lectrace-1.0.0/lectrace/_templates/lectrace.yml +45 -0
- lectrace-1.0.0/lectrace/arxiv.py +86 -0
- lectrace-1.0.0/lectrace/builder.py +117 -0
- lectrace-1.0.0/lectrace/cli.py +78 -0
- lectrace-1.0.0/lectrace/directives.py +42 -0
- lectrace-1.0.0/lectrace/files.py +45 -0
- lectrace-1.0.0/lectrace/reference.py +32 -0
- lectrace-1.0.0/lectrace/renderings.py +117 -0
- lectrace-1.0.0/lectrace/serializer.py +86 -0
- lectrace-1.0.0/lectrace/server.py +62 -0
- lectrace-1.0.0/lectrace/tracer.py +203 -0
- lectrace-1.0.0/mkdocs.yml +70 -0
- lectrace-1.0.0/pyproject.toml +48 -0
- lectrace-1.0.0/uv.lock +664 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
env/
|
|
9
|
+
_site/
|
|
10
|
+
.lectrace_cache.json
|
|
11
|
+
frontend/node_modules/
|
|
12
|
+
frontend/dist/
|
|
13
|
+
frontend/**/*.js
|
|
14
|
+
!frontend/eslint.config.js
|
|
15
|
+
frontend/tsconfig.tsbuildinfo
|
|
16
|
+
.DS_Store
|
|
17
|
+
*.swp
|
lectrace-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lectrace
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Write Python lecture code. Get an interactive viewer on GitHub Pages.
|
|
5
|
+
Author-email: "PraiseGod D. Adesanmi" <dayopraisegod@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Provides-Extra: arxiv
|
|
9
|
+
Requires-Dist: beautifulsoup4; extra == 'arxiv'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# lectrace
|
|
13
|
+
|
|
14
|
+
Write Python lecture code. Get an interactive step-through viewer on GitHub Pages — automatically.
|
|
15
|
+
|
|
16
|
+
lectrace traces your Python code line by line using `sys.settrace`, captures variable state and rendered content at each step, and produces a static React app that lets anyone step through the execution with arrow keys. No Node.js, no configuration, no build step for the user — just Python.
|
|
17
|
+
|
|
18
|
+
> Inspired by [edtrace](https://github.com/percyliang/edtrace) by Percy Liang.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
uv add lectrace
|
|
26
|
+
# or
|
|
27
|
+
pip install lectrace
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Python 3.11+. Zero mandatory dependencies — lectrace uses the standard library only. numpy, torch, and sympy are detected and rendered automatically if they are already installed in your environment.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## A lecture file
|
|
35
|
+
|
|
36
|
+
A lecture is any `.py` file that imports from `lectrace` and defines a `main()` function:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# 01_binary_search.py
|
|
40
|
+
from lectrace import text, link
|
|
41
|
+
|
|
42
|
+
def binary_search(arr, target):
|
|
43
|
+
lo, hi = 0, len(arr) - 1
|
|
44
|
+
while lo <= hi: # @inspect lo hi
|
|
45
|
+
mid = (lo + hi) // 2 # @inspect mid
|
|
46
|
+
if arr[mid] == target:
|
|
47
|
+
return mid
|
|
48
|
+
elif arr[mid] < target:
|
|
49
|
+
lo = mid + 1
|
|
50
|
+
else:
|
|
51
|
+
hi = mid - 1
|
|
52
|
+
return -1
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
text("# Binary Search")
|
|
56
|
+
text("Finds a target in a sorted array in $O(\\log n)$ time.")
|
|
57
|
+
|
|
58
|
+
arr = [2, 5, 8, 12, 16, 23, 38, 42] # @inspect arr
|
|
59
|
+
result = binary_search(arr, 23) # @inspect result
|
|
60
|
+
|
|
61
|
+
text(f"Found 23 at index `{result}`")
|
|
62
|
+
link(binary_search) # click to jump to the function definition
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Directives
|
|
68
|
+
|
|
69
|
+
Directives are inline comments that control tracing and display:
|
|
70
|
+
|
|
71
|
+
| Directive | Effect |
|
|
72
|
+
|-----------|--------|
|
|
73
|
+
| `# @inspect x y` | Show `x` and `y` in the variable panel after this line runs |
|
|
74
|
+
| `# @clear x` | Remove `x` from the variable panel |
|
|
75
|
+
| `# @stepover` | Execute this line as a single step — don't trace into any calls it makes |
|
|
76
|
+
| `# @hide` | Run this line silently — never shown in the viewer |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Rendering functions
|
|
81
|
+
|
|
82
|
+
Call these anywhere inside `main()` or any function it calls:
|
|
83
|
+
|
|
84
|
+
| Function | What it renders |
|
|
85
|
+
|----------|----------------|
|
|
86
|
+
| `text("# Heading")` | Markdown with LaTeX math (`$...$` inline, `$$...$$` display) |
|
|
87
|
+
| `text("...", verbatim=True)` | Monospace, whitespace preserved |
|
|
88
|
+
| `image("fig.png", width=400)` | Local file or remote URL (cached) |
|
|
89
|
+
| `video("demo.mp4")` | Embedded video with controls |
|
|
90
|
+
| `link(MyClass)` | Clickable jump to that class or function in the viewer |
|
|
91
|
+
| `link(title="Paper", url="...", authors=["Smith"], date="2024")` | Reference card with hover metadata |
|
|
92
|
+
| `plot({...})` | Interactive Vega-Lite chart |
|
|
93
|
+
| `note("speaker annotation")` | Presenter note, hidden until `N` is pressed |
|
|
94
|
+
| `system_text(["python3", "--version"])` | Shell command output as verbatim text |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Custom type rendering
|
|
99
|
+
|
|
100
|
+
Implement `__lectrace__` on any class to control how it appears in the variable panel:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
class Node:
|
|
104
|
+
def __init__(self, val, left=None, right=None):
|
|
105
|
+
self.val = val
|
|
106
|
+
self.left = left
|
|
107
|
+
self.right = right
|
|
108
|
+
|
|
109
|
+
def __lectrace__(self):
|
|
110
|
+
return {
|
|
111
|
+
"val": self.val,
|
|
112
|
+
"left": self.left.val if self.left else None,
|
|
113
|
+
"right": self.right.val if self.right else None,
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Without `__lectrace__`, nested objects show their full repr. With it, you control exactly what students see.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## File naming
|
|
122
|
+
|
|
123
|
+
| Pattern | Behaviour |
|
|
124
|
+
|---------|-----------|
|
|
125
|
+
| `01_intro.py` | Lecture — appears in sidebar, traced and deployed |
|
|
126
|
+
| `02_sorting.py` | Lecture — sidebar order follows alphabetical sort |
|
|
127
|
+
| `_utils.py` | Helper — imported normally, never traced or shown |
|
|
128
|
+
|
|
129
|
+
Number prefixes control sidebar order. Helper files starting with `_` are ignored by lectrace entirely.
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
my-course/
|
|
133
|
+
_data.py ← shared data, ignored by lectrace
|
|
134
|
+
01_intro.py ← first in sidebar
|
|
135
|
+
02_complexity.py ← second
|
|
136
|
+
03_sorting.py ← third
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## CLI
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
lectrace serve # build + serve all lectures at http://localhost:7000
|
|
145
|
+
lectrace serve 01_intro.py # serve a single file
|
|
146
|
+
lectrace build --output _site # build static site for deployment
|
|
147
|
+
lectrace init # generate GitHub Actions workflow + lectrace.toml
|
|
148
|
+
lectrace run 01_intro.py # execute and print trace stats (no server)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Deploy to GitHub Pages
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
lectrace init # generates .github/workflows/lectrace.yml
|
|
157
|
+
git add .
|
|
158
|
+
git commit -m "add lectures"
|
|
159
|
+
git push
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Enable GitHub Pages in your repo settings (Source: **GitHub Actions**). Every push to `main` rebuilds and redeploys automatically.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## How it works
|
|
167
|
+
|
|
168
|
+
- **Tracer** — `sys.settrace` intercepts every line of execution. At each step it captures the call stack, inspected variable values (serialized to JSON), and any pending renderings.
|
|
169
|
+
- **Serializer** — converts Python values to JSON. Primitives are direct. Collections recurse. numpy/torch/sympy are imported lazily only when encountered.
|
|
170
|
+
- **Builder** — discovers lecture files, runs each through the tracer, writes `traces/*.json` plus a `traces/index.json` manifest. Incremental: files are skipped if their SHA-256 hash hasn't changed.
|
|
171
|
+
- **Viewer** — a pre-built React + TypeScript SPA bundled into `lectrace/_static/` and shipped inside the pip package. Uses HashRouter so it works at any URL depth with zero configuration. Math via KaTeX, charts via Vega-Lite, syntax highlighting via highlight.js.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Documentation
|
|
176
|
+
|
|
177
|
+
Full documentation: **https://praisegee.github.io/lectrace/**
|
|
178
|
+
|
|
179
|
+
- [Getting Started](https://praisegee.github.io/lectrace/getting-started/)
|
|
180
|
+
- [Directives](https://praisegee.github.io/lectrace/directives/)
|
|
181
|
+
- [Rendering API](https://praisegee.github.io/lectrace/rendering-api/)
|
|
182
|
+
- [Custom Types](https://praisegee.github.io/lectrace/custom-types/)
|
|
183
|
+
- [Deploying](https://praisegee.github.io/lectrace/deploying/)
|
|
184
|
+
- [API Reference](https://praisegee.github.io/lectrace/api-reference/)
|
|
185
|
+
|
lectrace-1.0.0/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# lectrace
|
|
2
|
+
|
|
3
|
+
Write Python lecture code. Get an interactive step-through viewer on GitHub Pages — automatically.
|
|
4
|
+
|
|
5
|
+
lectrace traces your Python code line by line using `sys.settrace`, captures variable state and rendered content at each step, and produces a static React app that lets anyone step through the execution with arrow keys. No Node.js, no configuration, no build step for the user — just Python.
|
|
6
|
+
|
|
7
|
+
> Inspired by [edtrace](https://github.com/percyliang/edtrace) by Percy Liang.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
uv add lectrace
|
|
15
|
+
# or
|
|
16
|
+
pip install lectrace
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Python 3.11+. Zero mandatory dependencies — lectrace uses the standard library only. numpy, torch, and sympy are detected and rendered automatically if they are already installed in your environment.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## A lecture file
|
|
24
|
+
|
|
25
|
+
A lecture is any `.py` file that imports from `lectrace` and defines a `main()` function:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# 01_binary_search.py
|
|
29
|
+
from lectrace import text, link
|
|
30
|
+
|
|
31
|
+
def binary_search(arr, target):
|
|
32
|
+
lo, hi = 0, len(arr) - 1
|
|
33
|
+
while lo <= hi: # @inspect lo hi
|
|
34
|
+
mid = (lo + hi) // 2 # @inspect mid
|
|
35
|
+
if arr[mid] == target:
|
|
36
|
+
return mid
|
|
37
|
+
elif arr[mid] < target:
|
|
38
|
+
lo = mid + 1
|
|
39
|
+
else:
|
|
40
|
+
hi = mid - 1
|
|
41
|
+
return -1
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
text("# Binary Search")
|
|
45
|
+
text("Finds a target in a sorted array in $O(\\log n)$ time.")
|
|
46
|
+
|
|
47
|
+
arr = [2, 5, 8, 12, 16, 23, 38, 42] # @inspect arr
|
|
48
|
+
result = binary_search(arr, 23) # @inspect result
|
|
49
|
+
|
|
50
|
+
text(f"Found 23 at index `{result}`")
|
|
51
|
+
link(binary_search) # click to jump to the function definition
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Directives
|
|
57
|
+
|
|
58
|
+
Directives are inline comments that control tracing and display:
|
|
59
|
+
|
|
60
|
+
| Directive | Effect |
|
|
61
|
+
|-----------|--------|
|
|
62
|
+
| `# @inspect x y` | Show `x` and `y` in the variable panel after this line runs |
|
|
63
|
+
| `# @clear x` | Remove `x` from the variable panel |
|
|
64
|
+
| `# @stepover` | Execute this line as a single step — don't trace into any calls it makes |
|
|
65
|
+
| `# @hide` | Run this line silently — never shown in the viewer |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Rendering functions
|
|
70
|
+
|
|
71
|
+
Call these anywhere inside `main()` or any function it calls:
|
|
72
|
+
|
|
73
|
+
| Function | What it renders |
|
|
74
|
+
|----------|----------------|
|
|
75
|
+
| `text("# Heading")` | Markdown with LaTeX math (`$...$` inline, `$$...$$` display) |
|
|
76
|
+
| `text("...", verbatim=True)` | Monospace, whitespace preserved |
|
|
77
|
+
| `image("fig.png", width=400)` | Local file or remote URL (cached) |
|
|
78
|
+
| `video("demo.mp4")` | Embedded video with controls |
|
|
79
|
+
| `link(MyClass)` | Clickable jump to that class or function in the viewer |
|
|
80
|
+
| `link(title="Paper", url="...", authors=["Smith"], date="2024")` | Reference card with hover metadata |
|
|
81
|
+
| `plot({...})` | Interactive Vega-Lite chart |
|
|
82
|
+
| `note("speaker annotation")` | Presenter note, hidden until `N` is pressed |
|
|
83
|
+
| `system_text(["python3", "--version"])` | Shell command output as verbatim text |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Custom type rendering
|
|
88
|
+
|
|
89
|
+
Implement `__lectrace__` on any class to control how it appears in the variable panel:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
class Node:
|
|
93
|
+
def __init__(self, val, left=None, right=None):
|
|
94
|
+
self.val = val
|
|
95
|
+
self.left = left
|
|
96
|
+
self.right = right
|
|
97
|
+
|
|
98
|
+
def __lectrace__(self):
|
|
99
|
+
return {
|
|
100
|
+
"val": self.val,
|
|
101
|
+
"left": self.left.val if self.left else None,
|
|
102
|
+
"right": self.right.val if self.right else None,
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Without `__lectrace__`, nested objects show their full repr. With it, you control exactly what students see.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## File naming
|
|
111
|
+
|
|
112
|
+
| Pattern | Behaviour |
|
|
113
|
+
|---------|-----------|
|
|
114
|
+
| `01_intro.py` | Lecture — appears in sidebar, traced and deployed |
|
|
115
|
+
| `02_sorting.py` | Lecture — sidebar order follows alphabetical sort |
|
|
116
|
+
| `_utils.py` | Helper — imported normally, never traced or shown |
|
|
117
|
+
|
|
118
|
+
Number prefixes control sidebar order. Helper files starting with `_` are ignored by lectrace entirely.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
my-course/
|
|
122
|
+
_data.py ← shared data, ignored by lectrace
|
|
123
|
+
01_intro.py ← first in sidebar
|
|
124
|
+
02_complexity.py ← second
|
|
125
|
+
03_sorting.py ← third
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## CLI
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
lectrace serve # build + serve all lectures at http://localhost:7000
|
|
134
|
+
lectrace serve 01_intro.py # serve a single file
|
|
135
|
+
lectrace build --output _site # build static site for deployment
|
|
136
|
+
lectrace init # generate GitHub Actions workflow + lectrace.toml
|
|
137
|
+
lectrace run 01_intro.py # execute and print trace stats (no server)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Deploy to GitHub Pages
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
lectrace init # generates .github/workflows/lectrace.yml
|
|
146
|
+
git add .
|
|
147
|
+
git commit -m "add lectures"
|
|
148
|
+
git push
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Enable GitHub Pages in your repo settings (Source: **GitHub Actions**). Every push to `main` rebuilds and redeploys automatically.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## How it works
|
|
156
|
+
|
|
157
|
+
- **Tracer** — `sys.settrace` intercepts every line of execution. At each step it captures the call stack, inspected variable values (serialized to JSON), and any pending renderings.
|
|
158
|
+
- **Serializer** — converts Python values to JSON. Primitives are direct. Collections recurse. numpy/torch/sympy are imported lazily only when encountered.
|
|
159
|
+
- **Builder** — discovers lecture files, runs each through the tracer, writes `traces/*.json` plus a `traces/index.json` manifest. Incremental: files are skipped if their SHA-256 hash hasn't changed.
|
|
160
|
+
- **Viewer** — a pre-built React + TypeScript SPA bundled into `lectrace/_static/` and shipped inside the pip package. Uses HashRouter so it works at any URL depth with zero configuration. Math via KaTeX, charts via Vega-Lite, syntax highlighting via highlight.js.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Documentation
|
|
165
|
+
|
|
166
|
+
Full documentation: **https://praisegee.github.io/lectrace/**
|
|
167
|
+
|
|
168
|
+
- [Getting Started](https://praisegee.github.io/lectrace/getting-started/)
|
|
169
|
+
- [Directives](https://praisegee.github.io/lectrace/directives/)
|
|
170
|
+
- [Rendering API](https://praisegee.github.io/lectrace/rendering-api/)
|
|
171
|
+
- [Custom Types](https://praisegee.github.io/lectrace/custom-types/)
|
|
172
|
+
- [Deploying](https://praisegee.github.io/lectrace/deploying/)
|
|
173
|
+
- [API Reference](https://praisegee.github.io/lectrace/api-reference/)
|
|
174
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HashRouter, Routes, Route } from "react-router-dom";
|
|
2
|
+
import { TraceApp } from "./components/TraceApp";
|
|
3
|
+
import "./index.css";
|
|
4
|
+
|
|
5
|
+
export default function App() {
|
|
6
|
+
return (
|
|
7
|
+
<HashRouter>
|
|
8
|
+
<Routes>
|
|
9
|
+
<Route path="/" element={<TraceApp />} />
|
|
10
|
+
</Routes>
|
|
11
|
+
</HashRouter>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useSearchParams } from "react-router-dom";
|
|
2
|
+
import { TraceBrowser } from "./TraceBrowser";
|
|
3
|
+
import { TraceViewer } from "./TraceViewer";
|
|
4
|
+
|
|
5
|
+
export function TraceApp() {
|
|
6
|
+
const [params, setParams] = useSearchParams();
|
|
7
|
+
const tracePath = params.get("trace");
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="app-layout">
|
|
11
|
+
<TraceBrowser />
|
|
12
|
+
<main className="app-main">
|
|
13
|
+
{tracePath ? (
|
|
14
|
+
<TraceViewer />
|
|
15
|
+
) : (
|
|
16
|
+
<TracePrompt onSelect={(id) => setParams({ trace: id })} />
|
|
17
|
+
)}
|
|
18
|
+
</main>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TracePrompt({ onSelect }: { onSelect: (id: string) => void }) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="trace-prompt">
|
|
26
|
+
<h2>Open a Lecture</h2>
|
|
27
|
+
<p>Select a lecture from the sidebar, or type a trace name below.</p>
|
|
28
|
+
<form
|
|
29
|
+
onSubmit={(e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
const val = (e.currentTarget.elements.namedItem("trace") as HTMLInputElement).value.trim();
|
|
32
|
+
if (val) onSelect(val);
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<input
|
|
36
|
+
name="trace"
|
|
37
|
+
type="text"
|
|
38
|
+
placeholder="e.g. lecture_01 or traces/lecture_01.json"
|
|
39
|
+
autoFocus
|
|
40
|
+
className="trace-input"
|
|
41
|
+
/>
|
|
42
|
+
<button type="submit">Open</button>
|
|
43
|
+
</form>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
import type { TraceIndex } from "../types/trace";
|
|
4
|
+
|
|
5
|
+
export function TraceBrowser() {
|
|
6
|
+
const [index, setIndex] = useState<TraceIndex | null>(null);
|
|
7
|
+
const [params, setParams] = useSearchParams();
|
|
8
|
+
const active = params.get("trace");
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
fetch("./traces/index.json")
|
|
12
|
+
.then((r) => r.json())
|
|
13
|
+
.then(setIndex)
|
|
14
|
+
.catch(() => null);
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
if (!index || index.traces.length === 0) return null;
|
|
18
|
+
|
|
19
|
+
const select = (id: string) =>
|
|
20
|
+
setParams((p) => { p.set("trace", id); p.delete("step"); return p; });
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<nav className="trace-browser">
|
|
24
|
+
<div className="trace-browser-title">Lectures</div>
|
|
25
|
+
<ul>
|
|
26
|
+
{index.traces.map((entry) => (
|
|
27
|
+
<li
|
|
28
|
+
key={entry.id}
|
|
29
|
+
className={entry.id === active ? "active" : ""}
|
|
30
|
+
onClick={() => select(entry.id)}
|
|
31
|
+
>
|
|
32
|
+
<span className="entry-title">{entry.title}</span>
|
|
33
|
+
<span className="entry-steps">{entry.step_count} steps</span>
|
|
34
|
+
</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
</nav>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Trace, Value } from "../../types/trace";
|
|
2
|
+
import { ValueRenderer } from "../renderers/ValueRenderer";
|
|
3
|
+
import { useDrag } from "../../hooks/useDrag";
|
|
4
|
+
import { inSameFunction, isAncestorOf } from "../../hooks/useNavigation";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
trace: Trace;
|
|
8
|
+
stepIndex: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EnvPanel({ trace, stepIndex }: Props) {
|
|
12
|
+
const { position, onMouseDown, isDragging } = useDrag({ x: 20, y: 80 });
|
|
13
|
+
const env = buildEnv(trace, stepIndex);
|
|
14
|
+
|
|
15
|
+
if (Object.keys(env).length === 0) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className="env-panel"
|
|
20
|
+
style={{
|
|
21
|
+
left: position.x,
|
|
22
|
+
top: position.y,
|
|
23
|
+
cursor: isDragging.current ? "grabbing" : "grab",
|
|
24
|
+
}}
|
|
25
|
+
onMouseDown={onMouseDown}
|
|
26
|
+
>
|
|
27
|
+
<table className="env-table">
|
|
28
|
+
<tbody>
|
|
29
|
+
{Object.entries(env).map(([name, value]) => (
|
|
30
|
+
<tr key={name}>
|
|
31
|
+
<td className="env-key">{name}</td>
|
|
32
|
+
<td className="env-eq">=</td>
|
|
33
|
+
<td><ValueRenderer value={value} /></td>
|
|
34
|
+
</tr>
|
|
35
|
+
))}
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildEnv(trace: Trace, stepIndex: number): Record<string, Value> {
|
|
43
|
+
const current = trace.steps[stepIndex];
|
|
44
|
+
const envs: Record<string, Value | null>[] = [];
|
|
45
|
+
|
|
46
|
+
for (let i = stepIndex; i >= 0; i--) {
|
|
47
|
+
const step = trace.steps[i];
|
|
48
|
+
if (inSameFunction(step.stack, current.stack)) {
|
|
49
|
+
if (Object.keys(step.env).length > 0) envs.push(step.env as Record<string, Value | null>);
|
|
50
|
+
} else if (isAncestorOf(step.stack, current.stack)) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
envs.reverse();
|
|
56
|
+
const merged: Record<string, Value | null> = {};
|
|
57
|
+
for (const e of envs) Object.assign(merged, e);
|
|
58
|
+
|
|
59
|
+
for (const key of Object.keys(merged)) {
|
|
60
|
+
if (merged[key] === null) delete merged[key];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return merged as Record<string, Value>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useMemo, useCallback } from "react";
|
|
2
|
+
import hljs from "highlight.js/lib/core";
|
|
3
|
+
import python from "highlight.js/lib/languages/python";
|
|
4
|
+
import "highlight.js/styles/github.css";
|
|
5
|
+
import type { Trace, Rendering } from "../../types/trace";
|
|
6
|
+
import { computeLinesToShow } from "../../hooks/useNavigation";
|
|
7
|
+
import { MarkdownView } from "../renderers/MarkdownView";
|
|
8
|
+
import { ExternalLinkCard } from "../renderers/ExternalLinkCard";
|
|
9
|
+
import { PlotView } from "../renderers/PlotView";
|
|
10
|
+
|
|
11
|
+
hljs.registerLanguage("python", python);
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
trace: Trace;
|
|
15
|
+
path: string;
|
|
16
|
+
lineNumber: number;
|
|
17
|
+
stepIndex: number;
|
|
18
|
+
rawMode: boolean;
|
|
19
|
+
animateMode: boolean;
|
|
20
|
+
showNotes: boolean;
|
|
21
|
+
onGotoLine: (ln: number) => void;
|
|
22
|
+
onGotoLocation: (path: string, ln: number) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function LinesPanel({
|
|
26
|
+
trace, path, lineNumber, stepIndex,
|
|
27
|
+
rawMode, animateMode, showNotes,
|
|
28
|
+
onGotoLine, onGotoLocation,
|
|
29
|
+
}: Props) {
|
|
30
|
+
const fileContents = trace.files[path] ?? "";
|
|
31
|
+
|
|
32
|
+
const highlightedLines = useMemo(() => {
|
|
33
|
+
const highlighted = hljs.highlight(fileContents, { language: "python" }).value;
|
|
34
|
+
return highlighted.split("\n");
|
|
35
|
+
}, [fileContents]);
|
|
36
|
+
|
|
37
|
+
const linesToShow = useMemo(
|
|
38
|
+
() => computeLinesToShow(trace, stepIndex),
|
|
39
|
+
[trace, stepIndex],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const lineToRenderings = useMemo(() => {
|
|
43
|
+
const map = new Map<number, Rendering[]>();
|
|
44
|
+
for (const step of trace.steps) {
|
|
45
|
+
const frame = step.stack.at(-1);
|
|
46
|
+
if (frame) map.set(frame.line_number, step.renderings);
|
|
47
|
+
}
|
|
48
|
+
return map;
|
|
49
|
+
}, [trace.steps]);
|
|
50
|
+
|
|
51
|
+
const hidden = new Set(trace.hidden_line_numbers[path] ?? []);
|
|
52
|
+
|
|
53
|
+
const scrollRef = useCallback((el: HTMLElement | null) => {
|
|
54
|
+
if (!el) return;
|
|
55
|
+
const rect = el.getBoundingClientRect();
|
|
56
|
+
const inView = rect.top >= 50 && rect.bottom <= window.innerHeight - 50;
|
|
57
|
+
if (!inView) {
|
|
58
|
+
const dist = Math.min(Math.abs(rect.top), Math.abs(rect.bottom - window.innerHeight));
|
|
59
|
+
el.scrollIntoView({ behavior: dist < 150 ? "smooth" : "instant", block: "center" });
|
|
60
|
+
}
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="lines-panel">
|
|
65
|
+
{highlightedLines.map((htmlLine, idx) => {
|
|
66
|
+
const ln = idx + 1;
|
|
67
|
+
if (hidden.has(ln)) return null;
|
|
68
|
+
|
|
69
|
+
const loc = `${path}:${ln}`;
|
|
70
|
+
const isCurrent = ln === lineNumber;
|
|
71
|
+
const cloaked = animateMode && !linesToShow.has(loc);
|
|
72
|
+
const allRenderings = lineToRenderings.get(ln) ?? [];
|
|
73
|
+
const renderings = allRenderings.filter((r) => r.type !== "note");
|
|
74
|
+
const notes = allRenderings.filter((r): r is Extract<Rendering, { type: "note" }> => r.type === "note");
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
key={ln}
|
|
79
|
+
ref={isCurrent ? scrollRef : null}
|
|
80
|
+
className={["line", isCurrent ? "current-line" : "", cloaked ? "cloaked" : ""].filter(Boolean).join(" ")}
|
|
81
|
+
>
|
|
82
|
+
<span
|
|
83
|
+
className="line-number"
|
|
84
|
+
onClick={() => onGotoLine(ln)}
|
|
85
|
+
title={`Go to line ${ln}`}
|
|
86
|
+
>
|
|
87
|
+
{ln}
|
|
88
|
+
</span>
|
|
89
|
+
{!rawMode && renderings.length > 0 ? (
|
|
90
|
+
<span className="renderings">
|
|
91
|
+
<span className="indent" dangerouslySetInnerHTML={{ __html: htmlLine.match(/^(\s*)/)?.[0] ?? "" }} />
|
|
92
|
+
{renderings.map((r, i) => (
|
|
93
|
+
<span key={i}>{renderRendering(r, onGotoLocation)}</span>
|
|
94
|
+
))}
|
|
95
|
+
</span>
|
|
96
|
+
) : (
|
|
97
|
+
<span
|
|
98
|
+
className="code-line"
|
|
99
|
+
dangerouslySetInnerHTML={{ __html: rawMode ? htmlLine : stripDirectives(htmlLine) }}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
{showNotes && notes.map((r, i) => (
|
|
103
|
+
<span key={i} className="note-text">{r.data}</span>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderRendering(r: Rendering, onGotoLocation: (path: string, ln: number) => void): React.ReactNode {
|
|
113
|
+
switch (r.type) {
|
|
114
|
+
case "markdown":
|
|
115
|
+
return <MarkdownView content={r.data} style={r.style} />;
|
|
116
|
+
case "image":
|
|
117
|
+
return <img src={r.data} style={r.style} alt="" />;
|
|
118
|
+
case "video":
|
|
119
|
+
return <video controls style={r.style}><source src={r.data} /></video>;
|
|
120
|
+
case "link":
|
|
121
|
+
if (r.internal_link) {
|
|
122
|
+
const { path, line_number } = r.internal_link;
|
|
123
|
+
return (
|
|
124
|
+
<a href="#" style={r.style} onClick={(e) => { e.preventDefault(); onGotoLocation(path, line_number); }}>
|
|
125
|
+
{r.data ?? `${path}:${line_number}`}
|
|
126
|
+
</a>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (r.external_link) {
|
|
130
|
+
return <ExternalLinkCard link={r.external_link} anchorText={r.data} style={r.style} />;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
case "plot":
|
|
134
|
+
return <PlotView spec={r.data} style={r.style} />;
|
|
135
|
+
default: {
|
|
136
|
+
const fallback = r as { data?: string; style?: React.CSSProperties };
|
|
137
|
+
return <span style={fallback.style}>{fallback.data}</span>;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function stripDirectives(htmlLine: string): string {
|
|
143
|
+
const i = htmlLine.indexOf("#");
|
|
144
|
+
if (i === -1) return htmlLine;
|
|
145
|
+
const code = htmlLine.slice(0, i);
|
|
146
|
+
const comment = htmlLine.slice(i).replace(/@\S+/g, "").replace(/#\s*$/, "");
|
|
147
|
+
return comment.trim() === "" ? code : code + comment;
|
|
148
|
+
}
|