recursionlens 0.1.2__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ziyu Jiang and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
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
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ZiyuJiang07/recursionlens
8
+ 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: 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
+ Project-URL: Issues, https://github.com/ZiyuJiang07/recursionlens/issues
13
+ Keywords: recursion,jupyterlab,visualization,memoization,education
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Framework :: Jupyter
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == "dev"
24
+ Requires-Dist: coverage>=7; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # RecursionLens
28
+
29
+ **See not just what recursion returns, but where the repeated work happens.**
30
+
31
+ 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
+
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).
34
+
35
+ ## Project team
36
+
37
+ CS1302A Group 50:
38
+
39
+ - Ziyu Jiang
40
+ - Xinyue CAI
41
+ - Kehan CAO
42
+ - Ruoxi LYU
43
+ - Haobo MA
44
+
45
+ ## Why RecursionLens?
46
+
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:
48
+
49
+ 1. **Call tree:** what actually happened, invocation by invocation?
50
+ 2. **Subproblem DAG:** how many distinct problems and dependencies exist after identical arguments are merged?
51
+ 3. **Execution comparison:** what changes when memoization is enabled?
52
+
53
+ For `fib(5)`, that story is **15 observed calls → 6 unique subproblems → 6 executed bodies with memoization**.
54
+
55
+ ## Innovation beyond a call graph
56
+
57
+ The existing `rcviz` package documents decorator-based recursion trees with arguments, return values, and Jupyter rendering. RecursionLens deliberately focuses on a different workflow:
58
+
59
+ - transform one trace between an invocation tree and a compressed subproblem DAG;
60
+ - show the complete call tree at once in an SVG view or keep a collapsible tree for larger runs;
61
+ - quantify repeated calls, executed bodies, cache hits, and maximum depth;
62
+ - compare naive and memoized runs in one report with side-by-side DAGs;
63
+ - render self-contained HTML/SVG in JupyterLab without Graphviz, native libraries, external JavaScript, or a CDN;
64
+ - export versioned JSON for reproducible analysis and testing.
65
+
66
+ ## Installation
67
+
68
+ RecursionLens requires Python 3.9 or later.
69
+
70
+ Install from PyPI:
71
+
72
+ ```bash
73
+ python -m pip install recursionlens
74
+ ```
75
+
76
+ For local development:
77
+
78
+ ```bash
79
+ git clone https://github.com/ZiyuJiang07/recursionlens.git
80
+ cd recursionlens
81
+ python -m pip install -e ".[dev]"
82
+ ```
83
+
84
+ The package has no runtime dependencies.
85
+
86
+ ## Quickstart in JupyterLab
87
+
88
+ ```python
89
+ from recursionlens import trace
90
+
91
+ @trace
92
+ def fib(n):
93
+ if n < 2:
94
+ return n
95
+ return fib(n - 1) + fib(n - 2)
96
+
97
+ naive = fib.run(5)
98
+ naive # expandable call tree in the notebook output area
99
+ ```
100
+
101
+ Switch the same trace to the compressed subproblem DAG:
102
+
103
+ ```python
104
+ naive.show(mode="dag")
105
+ ```
106
+
107
+ Or show every call at once as one compact SVG tree, with no nodes collapsed:
108
+
109
+ ```python
110
+ naive.show(mode="expanded")
111
+ ```
112
+
113
+ Run the function again with memoization and compare the evidence:
114
+
115
+ ```python
116
+ memoized = fib.with_options(memoize=True).run(5)
117
+ comparison = naive.compare(memoized)
118
+ comparison
119
+ ```
120
+
121
+ The comparison confirms the returned values match and reports the measured changes, including **15 → 6 executed function bodies**.
122
+
123
+ Normal function behavior is preserved:
124
+
125
+ ```python
126
+ assert fib(5) == 5
127
+ assert fib.last_report.total_calls == 15
128
+ ```
129
+
130
+ ## Export a report
131
+
132
+ ```python
133
+ naive.export_html("fib-tree.html")
134
+ naive.export_html("fib-dag.html", mode="dag")
135
+ naive.export_json("fib-trace.json")
136
+ comparison.export_html("fib-comparison.html")
137
+ ```
138
+
139
+ HTML reports are standalone files that can be opened without a notebook server. JSON contains the tree, DAG nodes and edges, metrics, warnings, and schema version.
140
+
141
+ ## Supported scope
142
+
143
+ Version 0.1 targets synchronous, deterministic teaching examples such as factorial, Fibonacci, and pure mathematical recursion.
144
+
145
+ - Memoization requires hashable bound arguments; unhashable arguments produce a warning instead of a crash.
146
+ - Identical arguments do not necessarily imply redundant work for functions with side effects or external state.
147
+ - `max_calls` bounds the stored tree without changing the function's result. A truncated report explicitly marks partial tree/DAG metrics.
148
+ - RecursionLens executes your function; it is **not a sandbox for untrusted code**.
149
+ - Async functions, generators, automatic Big-O inference, and automatic code rewriting are non-goals for 0.1.
150
+
151
+ ## Documentation and demo
152
+
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
155
+ - [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
+
160
+ ## Development and testing
161
+
162
+ ```bash
163
+ python -m pytest tests -v
164
+ python -m build
165
+ python -m twine check dist/*
166
+ ```
167
+
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.
169
+
170
+ ## License
171
+
172
+ MIT © 2026 Ziyu Jiang, Xinyue Cai, Kehan Cao, Ruoxi Lyu, Haobo Ma.
@@ -0,0 +1,146 @@
1
+ # RecursionLens
2
+
3
+ **See not just what recursion returns, but where the repeated work happens.**
4
+
5
+ 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
+
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).
8
+
9
+ ## Project team
10
+
11
+ CS1302A Group 50:
12
+
13
+ - Ziyu Jiang
14
+ - Xinyue CAI
15
+ - Kehan CAO
16
+ - Ruoxi LYU
17
+ - Haobo MA
18
+
19
+ ## Why RecursionLens?
20
+
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:
22
+
23
+ 1. **Call tree:** what actually happened, invocation by invocation?
24
+ 2. **Subproblem DAG:** how many distinct problems and dependencies exist after identical arguments are merged?
25
+ 3. **Execution comparison:** what changes when memoization is enabled?
26
+
27
+ For `fib(5)`, that story is **15 observed calls → 6 unique subproblems → 6 executed bodies with memoization**.
28
+
29
+ ## Innovation beyond a call graph
30
+
31
+ The existing `rcviz` package documents decorator-based recursion trees with arguments, return values, and Jupyter rendering. RecursionLens deliberately focuses on a different workflow:
32
+
33
+ - transform one trace between an invocation tree and a compressed subproblem DAG;
34
+ - show the complete call tree at once in an SVG view or keep a collapsible tree for larger runs;
35
+ - quantify repeated calls, executed bodies, cache hits, and maximum depth;
36
+ - compare naive and memoized runs in one report with side-by-side DAGs;
37
+ - render self-contained HTML/SVG in JupyterLab without Graphviz, native libraries, external JavaScript, or a CDN;
38
+ - export versioned JSON for reproducible analysis and testing.
39
+
40
+ ## Installation
41
+
42
+ RecursionLens requires Python 3.9 or later.
43
+
44
+ Install from PyPI:
45
+
46
+ ```bash
47
+ python -m pip install recursionlens
48
+ ```
49
+
50
+ For local development:
51
+
52
+ ```bash
53
+ git clone https://github.com/ZiyuJiang07/recursionlens.git
54
+ cd recursionlens
55
+ python -m pip install -e ".[dev]"
56
+ ```
57
+
58
+ The package has no runtime dependencies.
59
+
60
+ ## Quickstart in JupyterLab
61
+
62
+ ```python
63
+ from recursionlens import trace
64
+
65
+ @trace
66
+ def fib(n):
67
+ if n < 2:
68
+ return n
69
+ return fib(n - 1) + fib(n - 2)
70
+
71
+ naive = fib.run(5)
72
+ naive # expandable call tree in the notebook output area
73
+ ```
74
+
75
+ Switch the same trace to the compressed subproblem DAG:
76
+
77
+ ```python
78
+ naive.show(mode="dag")
79
+ ```
80
+
81
+ Or show every call at once as one compact SVG tree, with no nodes collapsed:
82
+
83
+ ```python
84
+ naive.show(mode="expanded")
85
+ ```
86
+
87
+ Run the function again with memoization and compare the evidence:
88
+
89
+ ```python
90
+ memoized = fib.with_options(memoize=True).run(5)
91
+ comparison = naive.compare(memoized)
92
+ comparison
93
+ ```
94
+
95
+ The comparison confirms the returned values match and reports the measured changes, including **15 → 6 executed function bodies**.
96
+
97
+ Normal function behavior is preserved:
98
+
99
+ ```python
100
+ assert fib(5) == 5
101
+ assert fib.last_report.total_calls == 15
102
+ ```
103
+
104
+ ## Export a report
105
+
106
+ ```python
107
+ naive.export_html("fib-tree.html")
108
+ naive.export_html("fib-dag.html", mode="dag")
109
+ naive.export_json("fib-trace.json")
110
+ comparison.export_html("fib-comparison.html")
111
+ ```
112
+
113
+ HTML reports are standalone files that can be opened without a notebook server. JSON contains the tree, DAG nodes and edges, metrics, warnings, and schema version.
114
+
115
+ ## Supported scope
116
+
117
+ Version 0.1 targets synchronous, deterministic teaching examples such as factorial, Fibonacci, and pure mathematical recursion.
118
+
119
+ - Memoization requires hashable bound arguments; unhashable arguments produce a warning instead of a crash.
120
+ - Identical arguments do not necessarily imply redundant work for functions with side effects or external state.
121
+ - `max_calls` bounds the stored tree without changing the function's result. A truncated report explicitly marks partial tree/DAG metrics.
122
+ - RecursionLens executes your function; it is **not a sandbox for untrusted code**.
123
+ - Async functions, generators, automatic Big-O inference, and automatic code rewriting are non-goals for 0.1.
124
+
125
+ ## Documentation and demo
126
+
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
129
+ - [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
+
134
+ ## Development and testing
135
+
136
+ ```bash
137
+ python -m pytest tests -v
138
+ python -m build
139
+ python -m twine check dist/*
140
+ ```
141
+
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.
143
+
144
+ ## License
145
+
146
+ MIT © 2026 Ziyu Jiang, Xinyue Cai, Kehan Cao, Ruoxi Lyu, Haobo Ma.
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "recursionlens"
7
+ version = "0.1.2"
8
+ description = "Interactive recursion tracing, subproblem DAGs, and memoization comparisons for JupyterLab."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Ziyu Jiang" },
15
+ { name = "Xinyue Cai" },
16
+ { name = "Kehan Cao" },
17
+ { name = "Ruoxi Lyu" },
18
+ { name = "Haobo Ma" },
19
+ ]
20
+ keywords = ["recursion", "jupyterlab", "visualization", "memoization", "education"]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Intended Audience :: Education",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Framework :: Jupyter",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/ZiyuJiang07/recursionlens"
31
+ Repository = "https://github.com/ZiyuJiang07/recursionlens"
32
+ Documentation = "https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/USER.md"
33
+ Tutorial = "https://github.com/ZiyuJiang07/recursionlens/blob/main/docs/RecursionLens_Tutorial.ipynb"
34
+ Changelog = "https://github.com/ZiyuJiang07/recursionlens/blob/main/CHANGELOG.md"
35
+ Issues = "https://github.com/ZiyuJiang07/recursionlens/issues"
36
+
37
+ [project.optional-dependencies]
38
+ dev = ["pytest>=8", "coverage>=7"]
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ """RecursionLens: recursive execution analysis for JupyterLab."""
2
+
3
+ from ._model import CallNode, DAGNode
4
+ from ._report import ComparisonReport, TraceReport
5
+ from ._trace import trace
6
+
7
+ __version__ = "0.1.2"
8
+
9
+ __all__ = [
10
+ "CallNode",
11
+ "ComparisonReport",
12
+ "DAGNode",
13
+ "TraceReport",
14
+ "trace",
15
+ "__version__",
16
+ ]
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
5
+
6
+
7
+ @dataclass
8
+ class CallNode:
9
+ """One observed function invocation, including cache-only invocations."""
10
+
11
+ call_id: int
12
+ identity: str
13
+ signature: str
14
+ depth: int
15
+ status: str = "running"
16
+ result: Optional[str] = None
17
+ error: Optional[str] = None
18
+ repeated: bool = False
19
+ children: List["CallNode"] = field(default_factory=list)
20
+
21
+ @property
22
+ def cached(self) -> bool:
23
+ return self.status == "cached"
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ return {
27
+ "call_id": self.call_id,
28
+ "identity": self.identity,
29
+ "signature": self.signature,
30
+ "depth": self.depth,
31
+ "status": self.status,
32
+ "result": self.result,
33
+ "error": self.error,
34
+ "repeated": self.repeated,
35
+ "children": [child.to_dict() for child in self.children],
36
+ }
37
+
38
+
39
+ @dataclass
40
+ class DAGNode:
41
+ """All invocations with one internal argument identity, merged together."""
42
+
43
+ identity: str
44
+ signature: str
45
+ calls: int = 0
46
+ executed: int = 0
47
+ cached: int = 0
48
+ result: Optional[str] = None
49
+ children: List[str] = field(default_factory=list)
50
+
51
+ def to_dict(self) -> Dict[str, Any]:
52
+ return {
53
+ "identity": self.identity,
54
+ "signature": self.signature,
55
+ "calls": self.calls,
56
+ "executed": self.executed,
57
+ "cached": self.cached,
58
+ "result": self.result,
59
+ "children": list(self.children),
60
+ }
61
+
62
+
63
+ def build_dag(nodes: Iterable[CallNode]) -> Tuple[List[DAGNode], List[Dict[str, str]]]:
64
+ """Compress a call forest into a deterministic subproblem DAG."""
65
+
66
+ merged: Dict[str, DAGNode] = {}
67
+ edges = set()
68
+
69
+ def visit(node: CallNode) -> None:
70
+ dag_node = merged.get(node.identity)
71
+ if dag_node is None:
72
+ dag_node = DAGNode(identity=node.identity, signature=node.signature)
73
+ merged[node.identity] = dag_node
74
+
75
+ dag_node.calls += 1
76
+ if node.cached:
77
+ dag_node.cached += 1
78
+ else:
79
+ dag_node.executed += 1
80
+ if dag_node.result is None and node.result is not None:
81
+ dag_node.result = node.result
82
+
83
+ for child in node.children:
84
+ if child.identity not in dag_node.children:
85
+ dag_node.children.append(child.identity)
86
+ edges.add((node.identity, child.identity))
87
+ visit(child)
88
+
89
+ for root in nodes:
90
+ visit(root)
91
+
92
+ edge_dicts = [{"source": source, "target": target} for source, target in sorted(edges)]
93
+ return list(merged.values()), edge_dicts