recursionlens 0.1.2__tar.gz → 0.2.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.
- {recursionlens-0.1.2 → recursionlens-0.2.0}/LICENSE +1 -1
- {recursionlens-0.1.2/src/recursionlens.egg-info → recursionlens-0.2.0}/PKG-INFO +57 -17
- {recursionlens-0.1.2 → recursionlens-0.2.0}/README.md +48 -11
- {recursionlens-0.1.2 → recursionlens-0.2.0}/pyproject.toml +12 -11
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/__init__.py +6 -1
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_render.py +84 -32
- recursionlens-0.2.0/src/recursionlens/_tutor.py +629 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0/src/recursionlens.egg-info}/PKG-INFO +57 -17
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens.egg-info/SOURCES.txt +3 -1
- recursionlens-0.2.0/src/recursionlens.egg-info/requires.txt +8 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/tests/test_render.py +1 -1
- {recursionlens-0.1.2 → recursionlens-0.2.0}/tests/test_smoke.py +1 -1
- recursionlens-0.2.0/tests/test_tutor.py +174 -0
- recursionlens-0.1.2/src/recursionlens.egg-info/requires.txt +0 -4
- {recursionlens-0.1.2 → recursionlens-0.2.0}/setup.cfg +0 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_model.py +0 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_report.py +0 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_trace.py +0 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens.egg-info/dependency_links.txt +0 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens.egg-info/top_level.txt +0 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/tests/test_regressions.py +0 -0
- {recursionlens-0.1.2 → recursionlens-0.2.0}/tests/test_trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Ziyu
|
|
3
|
+
Copyright (c) 2026 Ziyu JIANG and contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: recursionlens
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Interactive recursion tracing, subproblem DAGs,
|
|
5
|
-
Author: Ziyu
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Interactive recursion tracing, subproblem DAGs, memoization comparisons, and a Python Tutor-style step-through tutor for JupyterLab.
|
|
5
|
+
Author: Ziyu JIANG, Xinyue CAI, Kehan CAO, Ruoxi LYU, Haobo MA
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/ZiyuJiang07/recursionlens
|
|
8
|
+
Project-URL: Website, https://ziyujiang07.github.io/recursionlens/
|
|
8
9
|
Project-URL: Repository, https://github.com/ZiyuJiang07/recursionlens
|
|
9
|
-
Project-URL: Documentation, https://github.com/ZiyuJiang07/recursionlens
|
|
10
|
+
Project-URL: Documentation, https://github.com/ZiyuJiang07/recursionlens#readme
|
|
10
11
|
Project-URL: Tutorial, https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb
|
|
11
|
-
Project-URL: Changelog, https://github.com/ZiyuJiang07/recursionlens/blob/main/CHANGELOG.md
|
|
12
12
|
Project-URL: Issues, https://github.com/ZiyuJiang07/recursionlens/issues
|
|
13
|
-
Keywords: recursion,jupyterlab,visualization,memoization,education
|
|
13
|
+
Keywords: recursion,jupyterlab,visualization,memoization,education,debugging,python-tutor
|
|
14
14
|
Classifier: Development Status :: 3 - Alpha
|
|
15
15
|
Classifier: Intended Audience :: Education
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -22,21 +22,28 @@ License-File: LICENSE
|
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
24
|
Requires-Dist: coverage>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: ipywidgets>=7.6; extra == "dev"
|
|
26
|
+
Provides-Extra: tutor
|
|
27
|
+
Requires-Dist: ipywidgets>=7.6; extra == "tutor"
|
|
25
28
|
Dynamic: license-file
|
|
26
29
|
|
|
27
30
|
# RecursionLens
|
|
28
31
|
|
|
32
|
+
**English** | [简体中文](https://github.com/ZiyuJiang07/recursionlens/blob/main/README.zh-CN.md)
|
|
33
|
+
|
|
29
34
|
**See not just what recursion returns, but where the repeated work happens.**
|
|
30
35
|
|
|
36
|
+
**Try it in the browser:** https://ziyujiang07.github.io/recursionlens/ — every view and the line-by-line player run client-side with Pyodide, no installation. The examples menu collects every recursive function found in the CS1302A lectures.
|
|
37
|
+
|
|
31
38
|
RecursionLens is a zero-runtime-dependency Python package for exploring recursive execution in JupyterLab. It turns a traced run into an expandable call tree, a compressed subproblem DAG, and a quantitative naive-versus-memoized comparison.
|
|
32
39
|
|
|
33
|
-
> **JupyterLab tutorial, already executed:** [Open RecursionLens_Tutorial.ipynb](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb).
|
|
40
|
+
> **JupyterLab tutorial, already executed:** [Open RecursionLens_Tutorial.ipynb](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb) ([简体中文版](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.zh-CN.ipynb)). Sixteen code cells have saved output; the final cell opens the interactive player when you run it in a live kernel. If Jupyter hides HTML/SVG from a downloaded notebook, use its **Trust** action or click **Run All Cells** once. For a zero-setup preview, download and open the [static HTML companion](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.html) or its [Chinese version](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.zh-CN.html).
|
|
34
41
|
|
|
35
42
|
## Project team
|
|
36
43
|
|
|
37
44
|
CS1302A Group 50:
|
|
38
45
|
|
|
39
|
-
- Ziyu
|
|
46
|
+
- Ziyu JIANG
|
|
40
47
|
- Xinyue CAI
|
|
41
48
|
- Kehan CAO
|
|
42
49
|
- Ruoxi LYU
|
|
@@ -44,7 +51,9 @@ CS1302A Group 50:
|
|
|
44
51
|
|
|
45
52
|
## Why RecursionLens?
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
RecursionLens grew out of a beginner's own difficulty with recursion. Loops can be read line by line, but a non-tail-recursive call such as `fib` or a tree traversal expands into many interleaved calls: the same subproblem is entered again and again, and a value only comes back after its whole subtree has finished. The step-by-step mentor provided by our course (OPT) is excellent for ordinary programs, since it shows one execution step at a time, yet a recursive run becomes a long stream of push and pop steps with no view of the overall shape of the computation, and no indication of which subproblems repeat. The usual fallback is to draw the call tree by hand on paper. RecursionLens automates that drawing and adds the numbers a hand-drawn tree cannot show.
|
|
55
|
+
|
|
56
|
+
It answers three different questions:
|
|
48
57
|
|
|
49
58
|
1. **Call tree:** what actually happened, invocation by invocation?
|
|
50
59
|
2. **Subproblem DAG:** how many distinct problems and dependencies exist after identical arguments are merged?
|
|
@@ -81,7 +90,7 @@ cd recursionlens
|
|
|
81
90
|
python -m pip install -e ".[dev]"
|
|
82
91
|
```
|
|
83
92
|
|
|
84
|
-
The package has no runtime dependencies.
|
|
93
|
+
The package has no runtime dependencies. The interactive tutor is the only optional extra, and it needs `ipywidgets`.
|
|
85
94
|
|
|
86
95
|
## Quickstart in JupyterLab
|
|
87
96
|
|
|
@@ -127,6 +136,36 @@ assert fib(5) == 5
|
|
|
127
136
|
assert fib.last_report.total_calls == 15
|
|
128
137
|
```
|
|
129
138
|
|
|
139
|
+
## Step through a function line by line
|
|
140
|
+
|
|
141
|
+
Version 0.2 adds a Python Tutor-style player that runs inside the notebook. Paste a function, choose the call, and step through it:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from recursionlens import tutor
|
|
145
|
+
|
|
146
|
+
tutor() # editable code panel, an input box for the call, and step controls
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The player highlights the current line and grows the call tree as the run proceeds: waiting calls are blue, the call being executed is coral, and returned calls stay on screen in green. The current frame's local variables sit under the tree, and changing the call (for example `fib(5)` → `fib(3)`) records a fresh run and redraws from a single node. It reports each call, line, return, and exception, which makes the order of a recursion easy to follow.
|
|
150
|
+
|
|
151
|
+
The tutor runs the code you paste with normal Python semantics; it is not a sandbox.
|
|
152
|
+
|
|
153
|
+
`tutor()` needs `ipywidgets`:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
python -m pip install "recursionlens[tutor]"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The recording is plain Python, so it also works without the widget:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from recursionlens import record_tutor
|
|
163
|
+
|
|
164
|
+
trace = record_tutor("def fib(n): ...", "fib(5)")
|
|
165
|
+
print(trace.describe())
|
|
166
|
+
trace.steps[0].frames # stack snapshot for one step
|
|
167
|
+
```
|
|
168
|
+
|
|
130
169
|
## Export a report
|
|
131
170
|
|
|
132
171
|
```python
|
|
@@ -150,12 +189,11 @@ Version 0.1 targets synchronous, deterministic teaching examples such as factori
|
|
|
150
189
|
|
|
151
190
|
## Documentation and demo
|
|
152
191
|
|
|
153
|
-
- [
|
|
154
|
-
- [
|
|
192
|
+
- [Command guide (English)](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb) - one example per public command: decorator, report object, three views, metrics, memoization, comparison, export, and error handling
|
|
193
|
+
- [命令手册(简体中文)](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.zh-CN.ipynb) - the same guide in Simplified Chinese
|
|
194
|
+
- [Static HTML preview](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.html) - English, no Jupyter required
|
|
195
|
+
- [静态 HTML 预览](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.zh-CN.html) - 中文,不需要 Jupyter
|
|
155
196
|
- [Runnable example notebook](https://github.com/ZiyuJiang07/recursionlens/blob/main/examples/demo.ipynb)
|
|
156
|
-
- [User guide](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/USER.md)
|
|
157
|
-
- [Developer guide](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/DEVELOPER.md)
|
|
158
|
-
- [Changelog](https://github.com/ZiyuJiang07/recursionlens/blob/main/CHANGELOG.md)
|
|
159
197
|
|
|
160
198
|
## Development and testing
|
|
161
199
|
|
|
@@ -165,8 +203,10 @@ python -m build
|
|
|
165
203
|
python -m twine check dist/*
|
|
166
204
|
```
|
|
167
205
|
|
|
168
|
-
The test suite covers trace invariants, cache behavior, signatures, exceptions, truncation, JSON, HTML escaping, DAG rendering, and comparison reports. Contributions should keep the public API, tests, and
|
|
206
|
+
The test suite covers trace invariants, cache behavior, signatures, exceptions, truncation, JSON, HTML escaping, DAG rendering, and comparison reports. Contributions should keep the public API, tests, and README synchronized.
|
|
207
|
+
|
|
208
|
+
Documentation is bilingual: `README.md` / `README.zh-CN.md` and `docs/RecursionLens_Tutorial.ipynb` / `docs/RecursionLens_Tutorial.zh-CN.ipynb` must be updated together, including their generated HTML files.
|
|
169
209
|
|
|
170
210
|
## License
|
|
171
211
|
|
|
172
|
-
MIT © 2026 Ziyu
|
|
212
|
+
MIT © 2026 Ziyu JIANG, Xinyue CAI, Kehan CAO, Ruoxi LYU, Haobo MA.
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# RecursionLens
|
|
2
2
|
|
|
3
|
+
**English** | [简体中文](https://github.com/ZiyuJiang07/recursionlens/blob/main/README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|
**See not just what recursion returns, but where the repeated work happens.**
|
|
4
6
|
|
|
7
|
+
**Try it in the browser:** https://ziyujiang07.github.io/recursionlens/ — every view and the line-by-line player run client-side with Pyodide, no installation. The examples menu collects every recursive function found in the CS1302A lectures.
|
|
8
|
+
|
|
5
9
|
RecursionLens is a zero-runtime-dependency Python package for exploring recursive execution in JupyterLab. It turns a traced run into an expandable call tree, a compressed subproblem DAG, and a quantitative naive-versus-memoized comparison.
|
|
6
10
|
|
|
7
|
-
> **JupyterLab tutorial, already executed:** [Open RecursionLens_Tutorial.ipynb](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb).
|
|
11
|
+
> **JupyterLab tutorial, already executed:** [Open RecursionLens_Tutorial.ipynb](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb) ([简体中文版](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.zh-CN.ipynb)). Sixteen code cells have saved output; the final cell opens the interactive player when you run it in a live kernel. If Jupyter hides HTML/SVG from a downloaded notebook, use its **Trust** action or click **Run All Cells** once. For a zero-setup preview, download and open the [static HTML companion](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.html) or its [Chinese version](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.zh-CN.html).
|
|
8
12
|
|
|
9
13
|
## Project team
|
|
10
14
|
|
|
11
15
|
CS1302A Group 50:
|
|
12
16
|
|
|
13
|
-
- Ziyu
|
|
17
|
+
- Ziyu JIANG
|
|
14
18
|
- Xinyue CAI
|
|
15
19
|
- Kehan CAO
|
|
16
20
|
- Ruoxi LYU
|
|
@@ -18,7 +22,9 @@ CS1302A Group 50:
|
|
|
18
22
|
|
|
19
23
|
## Why RecursionLens?
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
RecursionLens grew out of a beginner's own difficulty with recursion. Loops can be read line by line, but a non-tail-recursive call such as `fib` or a tree traversal expands into many interleaved calls: the same subproblem is entered again and again, and a value only comes back after its whole subtree has finished. The step-by-step mentor provided by our course (OPT) is excellent for ordinary programs, since it shows one execution step at a time, yet a recursive run becomes a long stream of push and pop steps with no view of the overall shape of the computation, and no indication of which subproblems repeat. The usual fallback is to draw the call tree by hand on paper. RecursionLens automates that drawing and adds the numbers a hand-drawn tree cannot show.
|
|
26
|
+
|
|
27
|
+
It answers three different questions:
|
|
22
28
|
|
|
23
29
|
1. **Call tree:** what actually happened, invocation by invocation?
|
|
24
30
|
2. **Subproblem DAG:** how many distinct problems and dependencies exist after identical arguments are merged?
|
|
@@ -55,7 +61,7 @@ cd recursionlens
|
|
|
55
61
|
python -m pip install -e ".[dev]"
|
|
56
62
|
```
|
|
57
63
|
|
|
58
|
-
The package has no runtime dependencies.
|
|
64
|
+
The package has no runtime dependencies. The interactive tutor is the only optional extra, and it needs `ipywidgets`.
|
|
59
65
|
|
|
60
66
|
## Quickstart in JupyterLab
|
|
61
67
|
|
|
@@ -101,6 +107,36 @@ assert fib(5) == 5
|
|
|
101
107
|
assert fib.last_report.total_calls == 15
|
|
102
108
|
```
|
|
103
109
|
|
|
110
|
+
## Step through a function line by line
|
|
111
|
+
|
|
112
|
+
Version 0.2 adds a Python Tutor-style player that runs inside the notebook. Paste a function, choose the call, and step through it:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from recursionlens import tutor
|
|
116
|
+
|
|
117
|
+
tutor() # editable code panel, an input box for the call, and step controls
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The player highlights the current line and grows the call tree as the run proceeds: waiting calls are blue, the call being executed is coral, and returned calls stay on screen in green. The current frame's local variables sit under the tree, and changing the call (for example `fib(5)` → `fib(3)`) records a fresh run and redraws from a single node. It reports each call, line, return, and exception, which makes the order of a recursion easy to follow.
|
|
121
|
+
|
|
122
|
+
The tutor runs the code you paste with normal Python semantics; it is not a sandbox.
|
|
123
|
+
|
|
124
|
+
`tutor()` needs `ipywidgets`:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
python -m pip install "recursionlens[tutor]"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The recording is plain Python, so it also works without the widget:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from recursionlens import record_tutor
|
|
134
|
+
|
|
135
|
+
trace = record_tutor("def fib(n): ...", "fib(5)")
|
|
136
|
+
print(trace.describe())
|
|
137
|
+
trace.steps[0].frames # stack snapshot for one step
|
|
138
|
+
```
|
|
139
|
+
|
|
104
140
|
## Export a report
|
|
105
141
|
|
|
106
142
|
```python
|
|
@@ -124,12 +160,11 @@ Version 0.1 targets synchronous, deterministic teaching examples such as factori
|
|
|
124
160
|
|
|
125
161
|
## Documentation and demo
|
|
126
162
|
|
|
127
|
-
- [
|
|
128
|
-
- [
|
|
163
|
+
- [Command guide (English)](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb) - one example per public command: decorator, report object, three views, metrics, memoization, comparison, export, and error handling
|
|
164
|
+
- [命令手册(简体中文)](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.zh-CN.ipynb) - the same guide in Simplified Chinese
|
|
165
|
+
- [Static HTML preview](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.html) - English, no Jupyter required
|
|
166
|
+
- [静态 HTML 预览](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.zh-CN.html) - 中文,不需要 Jupyter
|
|
129
167
|
- [Runnable example notebook](https://github.com/ZiyuJiang07/recursionlens/blob/main/examples/demo.ipynb)
|
|
130
|
-
- [User guide](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/USER.md)
|
|
131
|
-
- [Developer guide](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/DEVELOPER.md)
|
|
132
|
-
- [Changelog](https://github.com/ZiyuJiang07/recursionlens/blob/main/CHANGELOG.md)
|
|
133
168
|
|
|
134
169
|
## Development and testing
|
|
135
170
|
|
|
@@ -139,8 +174,10 @@ python -m build
|
|
|
139
174
|
python -m twine check dist/*
|
|
140
175
|
```
|
|
141
176
|
|
|
142
|
-
The test suite covers trace invariants, cache behavior, signatures, exceptions, truncation, JSON, HTML escaping, DAG rendering, and comparison reports. Contributions should keep the public API, tests, and
|
|
177
|
+
The test suite covers trace invariants, cache behavior, signatures, exceptions, truncation, JSON, HTML escaping, DAG rendering, and comparison reports. Contributions should keep the public API, tests, and README synchronized.
|
|
178
|
+
|
|
179
|
+
Documentation is bilingual: `README.md` / `README.zh-CN.md` and `docs/RecursionLens_Tutorial.ipynb` / `docs/RecursionLens_Tutorial.zh-CN.ipynb` must be updated together, including their generated HTML files.
|
|
143
180
|
|
|
144
181
|
## License
|
|
145
182
|
|
|
146
|
-
MIT © 2026 Ziyu
|
|
183
|
+
MIT © 2026 Ziyu JIANG, Xinyue CAI, Kehan CAO, Ruoxi LYU, Haobo MA.
|
|
@@ -4,20 +4,20 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "recursionlens"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Interactive recursion tracing, subproblem DAGs,
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Interactive recursion tracing, subproblem DAGs, memoization comparisons, and a Python Tutor-style step-through tutor for JupyterLab."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [
|
|
14
|
-
{ name = "Ziyu
|
|
15
|
-
{ name = "Xinyue
|
|
16
|
-
{ name = "Kehan
|
|
17
|
-
{ name = "Ruoxi
|
|
18
|
-
{ name = "Haobo
|
|
14
|
+
{ name = "Ziyu JIANG" },
|
|
15
|
+
{ name = "Xinyue CAI" },
|
|
16
|
+
{ name = "Kehan CAO" },
|
|
17
|
+
{ name = "Ruoxi LYU" },
|
|
18
|
+
{ name = "Haobo MA" },
|
|
19
19
|
]
|
|
20
|
-
keywords = ["recursion", "jupyterlab", "visualization", "memoization", "education"]
|
|
20
|
+
keywords = ["recursion", "jupyterlab", "visualization", "memoization", "education", "debugging", "python-tutor"]
|
|
21
21
|
classifiers = [
|
|
22
22
|
"Development Status :: 3 - Alpha",
|
|
23
23
|
"Intended Audience :: Education",
|
|
@@ -28,14 +28,15 @@ classifiers = [
|
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
30
30
|
Homepage = "https://github.com/ZiyuJiang07/recursionlens"
|
|
31
|
+
Website = "https://ziyujiang07.github.io/recursionlens/"
|
|
31
32
|
Repository = "https://github.com/ZiyuJiang07/recursionlens"
|
|
32
|
-
Documentation = "https://github.com/ZiyuJiang07/recursionlens
|
|
33
|
+
Documentation = "https://github.com/ZiyuJiang07/recursionlens#readme"
|
|
33
34
|
Tutorial = "https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb"
|
|
34
|
-
Changelog = "https://github.com/ZiyuJiang07/recursionlens/blob/main/CHANGELOG.md"
|
|
35
35
|
Issues = "https://github.com/ZiyuJiang07/recursionlens/issues"
|
|
36
36
|
|
|
37
37
|
[project.optional-dependencies]
|
|
38
|
-
dev = ["pytest>=8", "coverage>=7"]
|
|
38
|
+
dev = ["pytest>=8", "coverage>=7", "ipywidgets>=7.6"]
|
|
39
|
+
tutor = ["ipywidgets>=7.6"]
|
|
39
40
|
|
|
40
41
|
[tool.setuptools.packages.find]
|
|
41
42
|
where = ["src"]
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
from ._model import CallNode, DAGNode
|
|
4
4
|
from ._report import ComparisonReport, TraceReport
|
|
5
5
|
from ._trace import trace
|
|
6
|
+
from ._tutor import TutorStep, TutorTrace, record_tutor, tutor
|
|
6
7
|
|
|
7
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.2.0"
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"CallNode",
|
|
11
12
|
"ComparisonReport",
|
|
12
13
|
"DAGNode",
|
|
13
14
|
"TraceReport",
|
|
15
|
+
"TutorStep",
|
|
16
|
+
"TutorTrace",
|
|
17
|
+
"record_tutor",
|
|
14
18
|
"trace",
|
|
19
|
+
"tutor",
|
|
15
20
|
"__version__",
|
|
16
21
|
]
|
|
@@ -9,7 +9,7 @@ from ._model import DAGNode
|
|
|
9
9
|
|
|
10
10
|
_CSS = """
|
|
11
11
|
.rl-wrap{--rl-blue:#185fa5;--rl-blue-bg:#e6f1fb;--rl-coral:#d85a30;--rl-coral-bg:#faece7;--rl-green:#0f6e56;--rl-green-bg:#e1f5ee;--rl-text:#242424;--rl-muted:#66645f;--rl-border:#dedcd5;color:var(--rl-text);background:#fff;border:1px solid var(--rl-border);border-radius:12px;padding:18px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.5;box-sizing:border-box;margin:8px 0;max-width:100%;overflow:auto}
|
|
12
|
-
.rl-wrap *{box-sizing:border-box}.rl-title{font-size:18px;font-weight:600;margin:0 0 4px}.rl-subtitle{color:var(--rl-muted);font-size:13px;margin:0 0 16px}.rl-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(105px,1fr));gap:8px;margin:12px 0 18px}.rl-metric{background:#f6f5f1;border-radius:8px;padding:10px}.rl-metric-label{font-size:11px;color:var(--rl-muted)}.rl-metric-value{font-size:21px;font-weight:600;margin-top:2px}.rl-section{font-size:14px;font-weight:600;margin:18px 0 8px}.rl-tree,.rl-tree ul{list-style:none;padding-left:
|
|
12
|
+
.rl-wrap *{box-sizing:border-box}.rl-title{font-size:18px;font-weight:600;margin:0 0 4px}.rl-subtitle{color:var(--rl-muted);font-size:13px;margin:0 0 16px}.rl-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(105px,1fr));gap:8px;margin:12px 0 18px}.rl-metric{background:#f6f5f1;border-radius:8px;padding:10px}.rl-metric-label{font-size:11px;color:var(--rl-muted)}.rl-metric-value{font-size:21px;font-weight:600;margin-top:2px}.rl-section{font-size:14px;font-weight:600;margin:18px 0 8px}.rl-tree,.rl-tree ul{list-style:none;padding-left:18px}.rl-tree{padding-left:0}.rl-tree ul{border-left:1px solid #eae7de;margin-left:8px}.rl-tree details{margin:2px 0}.rl-tree summary{cursor:pointer;padding:6px 9px;border-radius:7px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;color:#2b2a26}.rl-tree summary:hover{background:#f4f2ec}.rl-result{color:var(--rl-muted);font-size:11px;margin-left:6px}.rl-badge{display:inline-block;border-radius:4px;padding:1px 5px;margin-left:5px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:10px;font-weight:500}.rl-repeat{color:#993c1d;background:var(--rl-coral-bg)}.rl-cached{color:var(--rl-green);background:var(--rl-green-bg)}.rl-error{color:#a32d2d;background:#fcebeb}.rl-warning{color:#854f0b;background:#faeeda;border-radius:6px;padding:8px;margin:8px 0;font-size:12px}.rl-dag{max-width:100%;overflow:auto;background:#fafaf8;border-radius:8px;padding:8px}.rl-legend{font-size:11px;color:var(--rl-muted);margin:6px 0}.rl-table{border-collapse:collapse;width:100%;font-size:12px;margin:8px 0 16px}.rl-table th,.rl-table td{text-align:right;border-bottom:1px solid var(--rl-border);padding:8px}.rl-table th:first-child,.rl-table td:first-child{text-align:left}.rl-table th{color:var(--rl-muted);font-weight:500}.rl-delta-up{color:#a32d2d}.rl-delta-down{color:var(--rl-green)}.rl-columns{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px}.rl-note{font-size:12px;color:var(--rl-muted)}.rl-expanded{max-width:100%;overflow:auto;background:#fbfaf6;border:1px solid #efede5;border-radius:10px;padding:12px}.rl-expanded svg{display:block;margin:auto}
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
|
|
@@ -105,6 +105,24 @@ def render_tree(report) -> str:
|
|
|
105
105
|
)
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def _mono_width(text: str, size: float = 12.0) -> float:
|
|
109
|
+
"""Approximate the advance width of monospace text at a given size."""
|
|
110
|
+
|
|
111
|
+
return len(text) * size * 0.6
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _node_style(node) -> Tuple[str, str, str, str, str]:
|
|
115
|
+
"""Return fill, stroke, dot colour, label colour, and dash pattern."""
|
|
116
|
+
|
|
117
|
+
if node.cached:
|
|
118
|
+
return "#eaf6f0", "#9ecbb9", "#0f6e56", "#0b5c48", ""
|
|
119
|
+
if node.status == "error":
|
|
120
|
+
return "#fdecec", "#e3a6a6", "#a32d2d", "#a32d2d", ""
|
|
121
|
+
if node.repeated:
|
|
122
|
+
return "#fdf1ec", "#e4b3a2", "#d85a30", "#993c1d", ' stroke-dasharray="4 3"'
|
|
123
|
+
return "#ffffff", "#d8d5cc", "#185fa5", "#242424", ""
|
|
124
|
+
|
|
125
|
+
|
|
108
126
|
def render_expanded_tree(report, limit: int = 120) -> str:
|
|
109
127
|
"""Render every selected invocation as a compact, fully visible SVG tree."""
|
|
110
128
|
|
|
@@ -126,8 +144,17 @@ def render_expanded_tree(report, limit: int = 120) -> str:
|
|
|
126
144
|
for root in report.roots:
|
|
127
145
|
collect(root)
|
|
128
146
|
|
|
147
|
+
labels = {id(node): compact_call(node.signature) for node in selected}
|
|
148
|
+
widths = {
|
|
149
|
+
id(node): max(62.0, min(210.0, _mono_width(labels[id(node)]) + 30))
|
|
150
|
+
for node in selected
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
node_height = 38.0
|
|
154
|
+
level_gap = 92.0
|
|
155
|
+
sibling_gap = 26.0
|
|
129
156
|
positions: Dict[int, Tuple[float, float]] = {}
|
|
130
|
-
|
|
157
|
+
cursor = [0.0]
|
|
131
158
|
maximum_depth = 0
|
|
132
159
|
|
|
133
160
|
def place(node, depth: int) -> None:
|
|
@@ -135,32 +162,31 @@ def render_expanded_tree(report, limit: int = 120) -> str:
|
|
|
135
162
|
maximum_depth = max(maximum_depth, depth)
|
|
136
163
|
children = child_lists[id(node)]
|
|
137
164
|
if not children:
|
|
138
|
-
x =
|
|
139
|
-
|
|
165
|
+
x = cursor[0] + widths[id(node)] / 2
|
|
166
|
+
cursor[0] = x + widths[id(node)] / 2 + sibling_gap
|
|
140
167
|
else:
|
|
141
168
|
for child in children:
|
|
142
169
|
place(child, depth + 1)
|
|
143
170
|
first_x = positions[id(children[0])][0]
|
|
144
171
|
last_x = positions[id(children[-1])][0]
|
|
145
172
|
x = (first_x + last_x) / 2
|
|
146
|
-
positions[id(node)] = (x,
|
|
173
|
+
positions[id(node)] = (x, 30 + depth * level_gap)
|
|
147
174
|
|
|
148
175
|
for root in report.roots:
|
|
149
176
|
if id(root) in child_lists:
|
|
150
177
|
place(root, 0)
|
|
151
178
|
|
|
152
|
-
|
|
153
|
-
width = max(
|
|
154
|
-
|
|
155
|
-
if
|
|
179
|
+
content_width = max(0.0, cursor[0] - sibling_gap)
|
|
180
|
+
width = max(640.0, content_width + 64)
|
|
181
|
+
shift = (width - content_width) / 2
|
|
182
|
+
if shift:
|
|
156
183
|
positions = {
|
|
157
|
-
node_id: (x +
|
|
158
|
-
for node_id, (x, y) in positions.items()
|
|
184
|
+
node_id: (x + shift, y) for node_id, (x, y) in positions.items()
|
|
159
185
|
}
|
|
160
|
-
height = max(
|
|
186
|
+
height = max(140.0, 30 + maximum_depth * level_gap + node_height / 2 + 28)
|
|
161
187
|
parts = [
|
|
162
188
|
f'<svg role="img" aria-label="Fully expanded recursion tree for {esc(report.function_name)}" '
|
|
163
|
-
f'viewBox="0 0 {width} {height}" width="{width}" height="{height}" '
|
|
189
|
+
f'viewBox="0 0 {width:.0f} {height:.0f}" width="{width:.0f}" height="{height:.0f}" '
|
|
164
190
|
'xmlns="http://www.w3.org/2000/svg">',
|
|
165
191
|
f"<title>{esc(report.function_name)} expanded call tree</title>",
|
|
166
192
|
]
|
|
@@ -169,39 +195,65 @@ def render_expanded_tree(report, limit: int = 120) -> str:
|
|
|
169
195
|
parent_x, parent_y = positions[id(parent)]
|
|
170
196
|
for child in child_lists[id(parent)]:
|
|
171
197
|
child_x, child_y = positions[id(child)]
|
|
198
|
+
start_y = parent_y + node_height / 2
|
|
199
|
+
end_y = child_y - node_height / 2
|
|
200
|
+
middle = (start_y + end_y) / 2
|
|
172
201
|
parts.append(
|
|
173
|
-
f'<path class="rl-expanded-edge" d="M{parent_x},{
|
|
174
|
-
f'
|
|
202
|
+
f'<path class="rl-expanded-edge" d="M{parent_x:.1f},{start_y:.1f} '
|
|
203
|
+
f'C{parent_x:.1f},{middle:.1f} {child_x:.1f},{middle:.1f} '
|
|
204
|
+
f'{child_x:.1f},{end_y:.1f}" fill="none" stroke="#c6c3ba" '
|
|
205
|
+
'stroke-width="1.2"/>'
|
|
175
206
|
)
|
|
176
207
|
|
|
177
208
|
for node in selected:
|
|
178
209
|
x, y = positions[id(node)]
|
|
210
|
+
fill, stroke, dot, ink, dash = _node_style(node)
|
|
179
211
|
note = [node.signature]
|
|
180
212
|
if node.cached:
|
|
181
213
|
note.append("cache hit")
|
|
182
|
-
color = "#0f6e56"
|
|
183
|
-
decoration = ""
|
|
184
214
|
elif node.status == "error":
|
|
185
215
|
note.append(node.error or "error")
|
|
186
|
-
color = "#a32d2d"
|
|
187
|
-
decoration = ""
|
|
188
216
|
elif node.repeated:
|
|
189
217
|
note.append("repeated")
|
|
190
|
-
color = "#993c1d"
|
|
191
|
-
decoration = ' text-decoration="underline"'
|
|
192
|
-
else:
|
|
193
|
-
color = "#242424"
|
|
194
|
-
decoration = ""
|
|
195
218
|
if node.result is not None:
|
|
196
219
|
note.append(f"returned {node.result}")
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
220
|
+
|
|
221
|
+
if node.status == "error":
|
|
222
|
+
value = "error"
|
|
223
|
+
elif node.result is not None:
|
|
224
|
+
value = f"→ {node.result}"
|
|
225
|
+
else:
|
|
226
|
+
value = ""
|
|
227
|
+
|
|
228
|
+
left = x - widths[id(node)] / 2
|
|
229
|
+
if value:
|
|
230
|
+
label_y = y - 2
|
|
231
|
+
value_y = y + 12
|
|
232
|
+
else:
|
|
233
|
+
label_y = y + 4.3
|
|
234
|
+
value_y = None
|
|
235
|
+
|
|
236
|
+
parts.append(f'<g class="rl-expanded-node"><title>{esc(" · ".join(note))}</title>')
|
|
237
|
+
parts.append(
|
|
238
|
+
f'<rect x="{left:.1f}" y="{y - node_height / 2:.1f}" '
|
|
239
|
+
f'width="{widths[id(node)]:.1f}" height="{node_height:.1f}" rx="9" '
|
|
240
|
+
f'fill="{fill}" stroke="{stroke}" stroke-width="1"{dash}/>'
|
|
241
|
+
)
|
|
242
|
+
parts.append(
|
|
243
|
+
f'<circle cx="{left + 11:.1f}" cy="{y:.1f}" r="3" fill="{dot}"/>'
|
|
204
244
|
)
|
|
245
|
+
parts.append(
|
|
246
|
+
f'<text x="{left + 20:.1f}" y="{label_y:.1f}" '
|
|
247
|
+
f'font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="12" '
|
|
248
|
+
f'fill="{ink}">{esc(labels[id(node)])}</text>'
|
|
249
|
+
)
|
|
250
|
+
if value_y is not None:
|
|
251
|
+
parts.append(
|
|
252
|
+
f'<text x="{left + 20:.1f}" y="{value_y:.1f}" '
|
|
253
|
+
'font-family="-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" '
|
|
254
|
+
f'font-size="10" fill="#7a776e">{esc(short(value, 22))}</text>'
|
|
255
|
+
)
|
|
256
|
+
parts.append("</g>")
|
|
205
257
|
parts.append("</svg>")
|
|
206
258
|
|
|
207
259
|
omitted = report.recorded_calls - len(selected)
|
|
@@ -213,7 +265,7 @@ def render_expanded_tree(report, limit: int = 120) -> str:
|
|
|
213
265
|
return (
|
|
214
266
|
'<h3 class="rl-section">Expanded call tree</h3>'
|
|
215
267
|
f'<div class="rl-legend">All {len(selected)} calls are visible. '
|
|
216
|
-
"
|
|
268
|
+
"White = executed · dashed coral = repeated · green = cache hit · red = error.</div>"
|
|
217
269
|
f'<div class="rl-expanded">{"".join(parts)}</div>{notice}'
|
|
218
270
|
)
|
|
219
271
|
|