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.
Files changed (22) hide show
  1. {recursionlens-0.1.2 → recursionlens-0.2.0}/LICENSE +1 -1
  2. {recursionlens-0.1.2/src/recursionlens.egg-info → recursionlens-0.2.0}/PKG-INFO +57 -17
  3. {recursionlens-0.1.2 → recursionlens-0.2.0}/README.md +48 -11
  4. {recursionlens-0.1.2 → recursionlens-0.2.0}/pyproject.toml +12 -11
  5. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/__init__.py +6 -1
  6. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_render.py +84 -32
  7. recursionlens-0.2.0/src/recursionlens/_tutor.py +629 -0
  8. {recursionlens-0.1.2 → recursionlens-0.2.0/src/recursionlens.egg-info}/PKG-INFO +57 -17
  9. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens.egg-info/SOURCES.txt +3 -1
  10. recursionlens-0.2.0/src/recursionlens.egg-info/requires.txt +8 -0
  11. {recursionlens-0.1.2 → recursionlens-0.2.0}/tests/test_render.py +1 -1
  12. {recursionlens-0.1.2 → recursionlens-0.2.0}/tests/test_smoke.py +1 -1
  13. recursionlens-0.2.0/tests/test_tutor.py +174 -0
  14. recursionlens-0.1.2/src/recursionlens.egg-info/requires.txt +0 -4
  15. {recursionlens-0.1.2 → recursionlens-0.2.0}/setup.cfg +0 -0
  16. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_model.py +0 -0
  17. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_report.py +0 -0
  18. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens/_trace.py +0 -0
  19. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens.egg-info/dependency_links.txt +0 -0
  20. {recursionlens-0.1.2 → recursionlens-0.2.0}/src/recursionlens.egg-info/top_level.txt +0 -0
  21. {recursionlens-0.1.2 → recursionlens-0.2.0}/tests/test_regressions.py +0 -0
  22. {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 Jiang and contributors
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.1.2
4
- Summary: Interactive recursion tracing, subproblem DAGs, and memoization comparisons for JupyterLab.
5
- Author: Ziyu Jiang, Xinyue Cai, Kehan Cao, Ruoxi Lyu, Haobo Ma
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/blob/main/docs/USER.md
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). All eight code cells have saved output. 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).
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 Jiang
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
- A recursive function normally exposes only its final return value or traceback. Students still have to reconstruct the execution in their heads. RecursionLens answers three different questions:
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
- - [Executed JupyterLab tutorial](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb) - setup, three views, comparison, export, troubleshooting, and a one-minute demo recipe
154
- - [Static HTML tutorial preview](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.html) - the same tutorial with all visual reports rendered, no Jupyter required
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 user documentation synchronized.
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 Jiang, Xinyue Cai, Kehan Cao, Ruoxi Lyu, Haobo Ma.
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). All eight code cells have saved output. 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).
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 Jiang
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
- A recursive function normally exposes only its final return value or traceback. Students still have to reconstruct the execution in their heads. RecursionLens answers three different questions:
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
- - [Executed JupyterLab tutorial](https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb) - setup, three views, comparison, export, troubleshooting, and a one-minute demo recipe
128
- - [Static HTML tutorial preview](https://github.com/ZiyuJiang07/recursionlens/raw/main/docs/RecursionLens_Tutorial.html) - the same tutorial with all visual reports rendered, no Jupyter required
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 user documentation synchronized.
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 Jiang, Xinyue Cai, Kehan Cao, Ruoxi Lyu, Haobo Ma.
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.1.2"
8
- description = "Interactive recursion tracing, subproblem DAGs, and memoization comparisons for JupyterLab."
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 Jiang" },
15
- { name = "Xinyue Cai" },
16
- { name = "Kehan Cao" },
17
- { name = "Ruoxi Lyu" },
18
- { name = "Haobo Ma" },
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/blob/main/docs/USER.md"
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.1.2"
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:20px}.rl-tree{padding-left:0}.rl-tree details{margin:4px 0}.rl-tree summary{cursor:pointer;padding:5px 8px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.rl-tree summary:hover{background:var(--rl-blue-bg)}.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:#fafaf8;border-radius:8px;padding:8px}.rl-expanded svg{display:block;margin: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: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
- leaf_cursor = [0]
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 = 80 + leaf_cursor[0] * 135
139
- leaf_cursor[0] += 1
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, 24 + depth * 72)
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
- tree_width = 160 + max(0, leaf_cursor[0] - 1) * 135
153
- width = max(600, tree_width)
154
- horizontal_shift = (width - tree_width) / 2
155
- if horizontal_shift:
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 + horizontal_shift, y)
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(110, 24 + maximum_depth * 72 + 56)
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},{parent_y + 28} '
174
- f'L{child_x},{child_y + 6}" fill="none" stroke="#a9a7a0" stroke-width="1"/>'
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
- parts.extend(
198
- [
199
- f'<g class="rl-expanded-node"><title>{esc(" · ".join(note))}</title>',
200
- f'<text x="{x}" y="{y + 17}" text-anchor="middle" '
201
- f'font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="12" '
202
- f'fill="{color}"{decoration}>{esc(compact_call(node.signature))}</text></g>',
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
- "Underlined coral = repeated; green = cache hit; red = error.</div>"
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