granim-viz 0.1.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.
- granim_viz-0.1.0/LICENSE +21 -0
- granim_viz-0.1.0/PKG-INFO +120 -0
- granim_viz-0.1.0/README.md +89 -0
- granim_viz-0.1.0/pyproject.toml +48 -0
- granim_viz-0.1.0/setup.cfg +4 -0
- granim_viz-0.1.0/src/granim/__init__.py +107 -0
- granim_viz-0.1.0/src/granim/core/__init__.py +0 -0
- granim_viz-0.1.0/src/granim/core/events.py +44 -0
- granim_viz-0.1.0/src/granim/core/recorder.py +260 -0
- granim_viz-0.1.0/src/granim/core/steps.py +107 -0
- granim_viz-0.1.0/src/granim/core/trace.py +38 -0
- granim_viz-0.1.0/src/granim/layout/__init__.py +78 -0
- granim_viz-0.1.0/src/granim/layout/force.py +69 -0
- granim_viz-0.1.0/src/granim/layout/layered.py +66 -0
- granim_viz-0.1.0/src/granim/layout/linear.py +28 -0
- granim_viz-0.1.0/src/granim/layout/tidy.py +141 -0
- granim_viz-0.1.0/src/granim/render/__init__.py +0 -0
- granim_viz-0.1.0/src/granim/render/compiler.py +260 -0
- granim_viz-0.1.0/src/granim/render/html.py +33 -0
- granim_viz-0.1.0/src/granim/render/player/player.css +151 -0
- granim_viz-0.1.0/src/granim/render/player/player.js +442 -0
- granim_viz-0.1.0/src/granim/render/player/template.html +37 -0
- granim_viz-0.1.0/src/granim/structures/__init__.py +0 -0
- granim_viz-0.1.0/src/granim/structures/array.py +70 -0
- granim_viz-0.1.0/src/granim/structures/base.py +135 -0
- granim_viz-0.1.0/src/granim/structures/custom.py +119 -0
- granim_viz-0.1.0/src/granim/structures/graph.py +85 -0
- granim_viz-0.1.0/src/granim/structures/linked_list.py +115 -0
- granim_viz-0.1.0/src/granim/structures/matrix.py +99 -0
- granim_viz-0.1.0/src/granim/structures/tracked.py +68 -0
- granim_viz-0.1.0/src/granim/structures/tree.py +114 -0
- granim_viz-0.1.0/src/granim/themes.py +64 -0
- granim_viz-0.1.0/src/granim_viz.egg-info/PKG-INFO +120 -0
- granim_viz-0.1.0/src/granim_viz.egg-info/SOURCES.txt +36 -0
- granim_viz-0.1.0/src/granim_viz.egg-info/dependency_links.txt +1 -0
- granim_viz-0.1.0/src/granim_viz.egg-info/requires.txt +4 -0
- granim_viz-0.1.0/src/granim_viz.egg-info/top_level.txt +1 -0
- granim_viz-0.1.0/tests/test_granim.py +523 -0
granim_viz-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Musaib Bashir
|
|
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,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: granim-viz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Write the algorithm, get the animation - instrumented data structures that compile your real Python code into interactive HTML visualizations.
|
|
5
|
+
Author-email: Musaib Bashir <musaibbashir.24@kgpian.iitkgp.ac.in>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/musaibbashir/granim
|
|
8
|
+
Project-URL: Repository, https://github.com/musaibbashir/granim
|
|
9
|
+
Project-URL: Issues, https://github.com/musaibbashir/granim/issues
|
|
10
|
+
Keywords: visualization,animation,algorithms,data-structures,education,linked-list,graph,tree,manim,teaching
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Education
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
23
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# granim
|
|
33
|
+
|
|
34
|
+
Write the algorithm. Get the animation.
|
|
35
|
+
|
|
36
|
+
granim instruments its data structures so that running your *normal* Python code
|
|
37
|
+
produces an interactive, self-contained HTML animation — nodes appear as you
|
|
38
|
+
build, arrows flip as you reverse, frontiers light up in parallel, and debug mode
|
|
39
|
+
shows every variable hopping between nodes.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import granim as ga
|
|
43
|
+
|
|
44
|
+
@ga.animate(debug=True)
|
|
45
|
+
def reverse(node):
|
|
46
|
+
if node is None or node.next is None:
|
|
47
|
+
return node
|
|
48
|
+
new_head = reverse(node.next)
|
|
49
|
+
node.next.next = node
|
|
50
|
+
node.next = None
|
|
51
|
+
return new_head
|
|
52
|
+
|
|
53
|
+
reverse(ga.linked_list([1, 2, 3, 4, 5]).head) # -> reverse.html, opens in browser
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That's the entire integration: one decorator, one constructor. The recursion walks
|
|
57
|
+
to null while the call stack panel fills; on the way back, each arrow visibly
|
|
58
|
+
reverses (granim classifies `node.next.next = node` as an *edge flip*, not a
|
|
59
|
+
delete+add).
|
|
60
|
+
|
|
61
|
+
## What you get
|
|
62
|
+
|
|
63
|
+
- **Automatic steps** — each loop iteration is one animation beat; same-line
|
|
64
|
+
mutations (a BFS frontier) merge into one *parallel* beat. No annotations.
|
|
65
|
+
- **Debug mode** (`debug=True`) — all locals in a side panel, plus on-canvas
|
|
66
|
+
badges: node-valued variables dock to their node, ints near an array become
|
|
67
|
+
index pointers (`lo`/`hi`/`mid` under the cells).
|
|
68
|
+
- **Five structures** — `ga.array`, `ga.matrix`, `ga.linked_list`, `ga.tree`,
|
|
69
|
+
`ga.graph`, all usable (and unit-testable) without recording. Matrix cells
|
|
70
|
+
animate values (`m[i][j] = v`) and colors (`m[i][j].state = "visited"`).
|
|
71
|
+
- **Your own classes** — `@ga.node(value="val")` instruments any class without
|
|
72
|
+
inheritance: node-valued fields become labeled edges, the value field animates,
|
|
73
|
+
everything else is plain storage. Ad-hoc fields on granim nodes work too
|
|
74
|
+
(`node.random`, `node.child` arc underneath with their name on them).
|
|
75
|
+
`@ga.container` does the same for wrappers (queues, custom lists): `head`/
|
|
76
|
+
`tail`-style fields render as floating badges and root the garbage pass, so
|
|
77
|
+
dropped nodes dim out.
|
|
78
|
+
- **Auto layout** — tidy trees (Buchheim), layered DAGs, force-directed graphs,
|
|
79
|
+
snake-wrapped lists; positions stay stable between steps instead of jumping.
|
|
80
|
+
- **One file out** — interactive player (play/pause/step/scrub/speed) in a single
|
|
81
|
+
offline HTML file. Jupyter renders inline automatically.
|
|
82
|
+
- **Zero dependencies.**
|
|
83
|
+
|
|
84
|
+
## Examples
|
|
85
|
+
|
|
86
|
+
`examples/` doubles as the acceptance suite:
|
|
87
|
+
|
|
88
|
+
| file | shows off |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `binary_search.py` | auto index badges, comparison pulses |
|
|
91
|
+
| `reverse_recursive.py` | call stack + arrows flipping on unwind |
|
|
92
|
+
| `reverse_iterative.py` | `prev`/`cur`/`nxt` badges hopping |
|
|
93
|
+
| `bst.py` | tidy tree re-flowing as nodes attach |
|
|
94
|
+
| `bfs.py` | parallel frontier steps, state coloring |
|
|
95
|
+
| `dedup.py` | unlinked nodes dim out (reachability pass) |
|
|
96
|
+
| `floyd.py` | tortoise/hare badges meeting; cycle arc |
|
|
97
|
+
| `flood_fill.py` | matrix: values + colors spreading, deep recursion |
|
|
98
|
+
| `dijkstra.py` | weighted edges, frontier/visited/done coloring |
|
|
99
|
+
| `quicksort.py` | pivot glow, swaps, sorted cells locking in |
|
|
100
|
+
| `edit_distance.py` | DP table filling, dependency-cell pulses |
|
|
101
|
+
| `custom_node.py` | `@ga.node` on a user-owned class (LeetCode 138) |
|
|
102
|
+
| `queue.py` | `@ga.container`: head/tail badges, dequeued nodes dim |
|
|
103
|
+
|
|
104
|
+
Run any of them: `PYTHONPATH=src python examples/bfs.py` → open the HTML next to it.
|
|
105
|
+
|
|
106
|
+
## Tests
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
python tests/run_tests.py # pure-python pipeline tests (pytest also works)
|
|
110
|
+
node tests/verify_html.js # replays each example's timeline, checks end state
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Design
|
|
114
|
+
|
|
115
|
+
`docs/PLAN.md` holds the rationale, `docs/SPEC.md` the contracts. The short version:
|
|
116
|
+
structures emit a small vocabulary of semantic events; a step builder groups them
|
|
117
|
+
into beats (line-scoped via a trace limited to your function); a pure compiler
|
|
118
|
+
classifies edge flips, computes layout keyframes, and serializes a timeline that
|
|
119
|
+
a dependency-free JS player animates. The trace can only ever coarsen steps —
|
|
120
|
+
animation truth comes from the structures.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# granim
|
|
2
|
+
|
|
3
|
+
Write the algorithm. Get the animation.
|
|
4
|
+
|
|
5
|
+
granim instruments its data structures so that running your *normal* Python code
|
|
6
|
+
produces an interactive, self-contained HTML animation — nodes appear as you
|
|
7
|
+
build, arrows flip as you reverse, frontiers light up in parallel, and debug mode
|
|
8
|
+
shows every variable hopping between nodes.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import granim as ga
|
|
12
|
+
|
|
13
|
+
@ga.animate(debug=True)
|
|
14
|
+
def reverse(node):
|
|
15
|
+
if node is None or node.next is None:
|
|
16
|
+
return node
|
|
17
|
+
new_head = reverse(node.next)
|
|
18
|
+
node.next.next = node
|
|
19
|
+
node.next = None
|
|
20
|
+
return new_head
|
|
21
|
+
|
|
22
|
+
reverse(ga.linked_list([1, 2, 3, 4, 5]).head) # -> reverse.html, opens in browser
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That's the entire integration: one decorator, one constructor. The recursion walks
|
|
26
|
+
to null while the call stack panel fills; on the way back, each arrow visibly
|
|
27
|
+
reverses (granim classifies `node.next.next = node` as an *edge flip*, not a
|
|
28
|
+
delete+add).
|
|
29
|
+
|
|
30
|
+
## What you get
|
|
31
|
+
|
|
32
|
+
- **Automatic steps** — each loop iteration is one animation beat; same-line
|
|
33
|
+
mutations (a BFS frontier) merge into one *parallel* beat. No annotations.
|
|
34
|
+
- **Debug mode** (`debug=True`) — all locals in a side panel, plus on-canvas
|
|
35
|
+
badges: node-valued variables dock to their node, ints near an array become
|
|
36
|
+
index pointers (`lo`/`hi`/`mid` under the cells).
|
|
37
|
+
- **Five structures** — `ga.array`, `ga.matrix`, `ga.linked_list`, `ga.tree`,
|
|
38
|
+
`ga.graph`, all usable (and unit-testable) without recording. Matrix cells
|
|
39
|
+
animate values (`m[i][j] = v`) and colors (`m[i][j].state = "visited"`).
|
|
40
|
+
- **Your own classes** — `@ga.node(value="val")` instruments any class without
|
|
41
|
+
inheritance: node-valued fields become labeled edges, the value field animates,
|
|
42
|
+
everything else is plain storage. Ad-hoc fields on granim nodes work too
|
|
43
|
+
(`node.random`, `node.child` arc underneath with their name on them).
|
|
44
|
+
`@ga.container` does the same for wrappers (queues, custom lists): `head`/
|
|
45
|
+
`tail`-style fields render as floating badges and root the garbage pass, so
|
|
46
|
+
dropped nodes dim out.
|
|
47
|
+
- **Auto layout** — tidy trees (Buchheim), layered DAGs, force-directed graphs,
|
|
48
|
+
snake-wrapped lists; positions stay stable between steps instead of jumping.
|
|
49
|
+
- **One file out** — interactive player (play/pause/step/scrub/speed) in a single
|
|
50
|
+
offline HTML file. Jupyter renders inline automatically.
|
|
51
|
+
- **Zero dependencies.**
|
|
52
|
+
|
|
53
|
+
## Examples
|
|
54
|
+
|
|
55
|
+
`examples/` doubles as the acceptance suite:
|
|
56
|
+
|
|
57
|
+
| file | shows off |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `binary_search.py` | auto index badges, comparison pulses |
|
|
60
|
+
| `reverse_recursive.py` | call stack + arrows flipping on unwind |
|
|
61
|
+
| `reverse_iterative.py` | `prev`/`cur`/`nxt` badges hopping |
|
|
62
|
+
| `bst.py` | tidy tree re-flowing as nodes attach |
|
|
63
|
+
| `bfs.py` | parallel frontier steps, state coloring |
|
|
64
|
+
| `dedup.py` | unlinked nodes dim out (reachability pass) |
|
|
65
|
+
| `floyd.py` | tortoise/hare badges meeting; cycle arc |
|
|
66
|
+
| `flood_fill.py` | matrix: values + colors spreading, deep recursion |
|
|
67
|
+
| `dijkstra.py` | weighted edges, frontier/visited/done coloring |
|
|
68
|
+
| `quicksort.py` | pivot glow, swaps, sorted cells locking in |
|
|
69
|
+
| `edit_distance.py` | DP table filling, dependency-cell pulses |
|
|
70
|
+
| `custom_node.py` | `@ga.node` on a user-owned class (LeetCode 138) |
|
|
71
|
+
| `queue.py` | `@ga.container`: head/tail badges, dequeued nodes dim |
|
|
72
|
+
|
|
73
|
+
Run any of them: `PYTHONPATH=src python examples/bfs.py` → open the HTML next to it.
|
|
74
|
+
|
|
75
|
+
## Tests
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
python tests/run_tests.py # pure-python pipeline tests (pytest also works)
|
|
79
|
+
node tests/verify_html.js # replays each example's timeline, checks end state
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Design
|
|
83
|
+
|
|
84
|
+
`docs/PLAN.md` holds the rationale, `docs/SPEC.md` the contracts. The short version:
|
|
85
|
+
structures emit a small vocabulary of semantic events; a step builder groups them
|
|
86
|
+
into beats (line-scoped via a trace limited to your function); a pure compiler
|
|
87
|
+
classifies edge flips, computes layout keyframes, and serializes a timeline that
|
|
88
|
+
a dependency-free JS player animates. The trace can only ever coarsen steps —
|
|
89
|
+
animation truth comes from the structures.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "granim-viz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Write the algorithm, get the animation - instrumented data structures that compile your real Python code into interactive HTML visualizations."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Musaib Bashir", email = "musaibbashir.24@kgpian.iitkgp.ac.in" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"visualization", "animation", "algorithms", "data-structures",
|
|
15
|
+
"education", "linked-list", "graph", "tree", "manim", "teaching",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: Education",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Education",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
30
|
+
"Topic :: Software Development :: Debuggers",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/musaibbashir/granim"
|
|
35
|
+
Repository = "https://github.com/musaibbashir/granim"
|
|
36
|
+
Issues = "https://github.com/musaibbashir/granim/issues"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["pytest", "ruff"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
"granim.render" = ["player/*"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""granim — write the algorithm, get the animation.
|
|
2
|
+
|
|
3
|
+
import granim as ga
|
|
4
|
+
|
|
5
|
+
@ga.animate(debug=True)
|
|
6
|
+
def rev(node, prev=None):
|
|
7
|
+
if node is None:
|
|
8
|
+
return prev
|
|
9
|
+
nxt = node.next
|
|
10
|
+
node.next = prev
|
|
11
|
+
return rev(nxt, node)
|
|
12
|
+
|
|
13
|
+
rev(ga.linked_list([1, 2, 3, 4, 5]).head) # -> rev.html
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import sys
|
|
19
|
+
import webbrowser
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from .core.recorder import GranimError, Index, Recorder, active
|
|
23
|
+
from .core.trace import Trace
|
|
24
|
+
from .structures.array import Array
|
|
25
|
+
from .structures.graph import Graph, GraphNode
|
|
26
|
+
from .structures.linked_list import LinkedList, ListNode
|
|
27
|
+
from .structures.custom import container, node
|
|
28
|
+
from .structures.matrix import Matrix
|
|
29
|
+
from .structures.tracked import Tracked
|
|
30
|
+
from .structures.tree import Tree, TreeNode
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
__all__ = [
|
|
34
|
+
"animate", "record", "array", "matrix", "linked_list", "tree", "graph",
|
|
35
|
+
"node", "container", "index", "GranimError", "ListNode", "TreeNode",
|
|
36
|
+
"GraphNode",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
array = Array
|
|
40
|
+
matrix = Matrix
|
|
41
|
+
linked_list = LinkedList
|
|
42
|
+
tree = Tree
|
|
43
|
+
graph = Graph
|
|
44
|
+
index = Index
|
|
45
|
+
record = Recorder
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def animate(fn=None, *, debug=False, theme="dark", out=None, show=None,
|
|
49
|
+
speed=1.0, title=None):
|
|
50
|
+
"""SPEC §2.1. Decorate a function; calling it records, compiles, saves, and
|
|
51
|
+
opens the animation. Returns the function's result unchanged."""
|
|
52
|
+
if fn is None:
|
|
53
|
+
return functools.partial(animate, debug=debug, theme=theme, out=out,
|
|
54
|
+
show=show, speed=speed, title=title)
|
|
55
|
+
|
|
56
|
+
@functools.wraps(fn)
|
|
57
|
+
def wrapper(*args, **kwargs):
|
|
58
|
+
rec = active()
|
|
59
|
+
if rec is not None:
|
|
60
|
+
if rec._owner is wrapper: # recursion through the decorated name
|
|
61
|
+
return fn(*args, **kwargs)
|
|
62
|
+
raise GranimError("nested recording: another @animate function is running")
|
|
63
|
+
|
|
64
|
+
rec = Recorder(debug=debug, theme=theme, title=title or fn.__name__,
|
|
65
|
+
speed=speed, owner=wrapper)
|
|
66
|
+
from .structures.base import is_node
|
|
67
|
+
rec.ext_roots = [a._id for a in (*args, *kwargs.values()) if is_node(a)]
|
|
68
|
+
result, err = None, None
|
|
69
|
+
with rec:
|
|
70
|
+
try:
|
|
71
|
+
with Trace(rec, fn.__code__):
|
|
72
|
+
result = fn(*args, **kwargs)
|
|
73
|
+
except Exception as e: # finalize + save, then re-raise (SPEC §2.1)
|
|
74
|
+
err = e
|
|
75
|
+
rec.error = f"{type(e).__name__}: {e}"
|
|
76
|
+
if is_node(result):
|
|
77
|
+
rec.ext_roots.append(result._id)
|
|
78
|
+
|
|
79
|
+
path = _out_path(out, fn)
|
|
80
|
+
rec.save(path)
|
|
81
|
+
if err is not None:
|
|
82
|
+
sys.stderr.write(f"granim: saved partial animation to {path}\n")
|
|
83
|
+
raise err
|
|
84
|
+
_present(rec, path, show, out)
|
|
85
|
+
return result._v if isinstance(result, Tracked) else result
|
|
86
|
+
|
|
87
|
+
return wrapper
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _out_path(out, fn) -> Path:
|
|
91
|
+
if out is not None:
|
|
92
|
+
return Path(out)
|
|
93
|
+
src = Path(fn.__code__.co_filename)
|
|
94
|
+
base = src.parent if src.is_file() else Path.cwd()
|
|
95
|
+
return base / f"{fn.__name__}.html"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _present(rec, path, show, out) -> None:
|
|
99
|
+
in_jupyter = "ipykernel" in sys.modules
|
|
100
|
+
if show is None:
|
|
101
|
+
show = out is None
|
|
102
|
+
if not show:
|
|
103
|
+
return
|
|
104
|
+
if in_jupyter:
|
|
105
|
+
rec.show()
|
|
106
|
+
else:
|
|
107
|
+
webbrowser.open(path.resolve().as_uri())
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Event model. SPEC §3: a small, closed vocabulary; everything else is derived."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
KINDS = frozenset({
|
|
7
|
+
"node_add", "node_remove", "edge_set", "read", "compare",
|
|
8
|
+
"value_set", "state_set", "var_set",
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
# Kinds that change structure (drive layout keyframes). edge_flip is assigned by
|
|
12
|
+
# the compiler, never emitted.
|
|
13
|
+
STRUCTURAL = frozenset({"node_add", "node_remove", "edge_set"})
|
|
14
|
+
# Kinds eligible for parallel batch-merging (SPEC §5 rule 2).
|
|
15
|
+
BATCHABLE = frozenset({"state_set", "node_add"})
|
|
16
|
+
PASSIVE = frozenset({"read", "compare"})
|
|
17
|
+
|
|
18
|
+
REPR_MAX = 60
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class Event:
|
|
23
|
+
seq: int
|
|
24
|
+
kind: str
|
|
25
|
+
payload: dict
|
|
26
|
+
depth: int
|
|
27
|
+
line: int | None
|
|
28
|
+
frame: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def safe_repr(value) -> str:
|
|
32
|
+
from ..structures.base import NodeBase
|
|
33
|
+
from ..structures.tracked import Tracked
|
|
34
|
+
|
|
35
|
+
if isinstance(value, Tracked):
|
|
36
|
+
value = value._v
|
|
37
|
+
if isinstance(value, NodeBase):
|
|
38
|
+
r = f"Node({value._value!r})"
|
|
39
|
+
else:
|
|
40
|
+
try:
|
|
41
|
+
r = repr(value)
|
|
42
|
+
except Exception:
|
|
43
|
+
r = f"<{type(value).__name__}>"
|
|
44
|
+
return r if len(r) <= REPR_MAX else r[: REPR_MAX - 1] + "…"
|