loop-autopsy 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.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NATHANAEL George Steindhart, SETIAWAN Matthew Henson,
4
+ LATHIF Muhammad Akmal, KWAN Kendrick Matthew, HANDAYA Clement Steward
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,230 @@
1
+ Metadata-Version: 2.4
2
+ Name: loop-autopsy
3
+ Version: 0.1.0
4
+ Summary: Stops runaway loops in Jupyter and shows the evidence for why they never ended.
5
+ Author: NATHANAEL George Steindhart, SETIAWAN Matthew Henson, LATHIF Muhammad Akmal, KWAN Kendrick Matthew, HANDAYA Clement Steward
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Geoeoeo217/loop-autopsy
8
+ Project-URL: Repository, https://github.com/Geoeoeo217/loop-autopsy
9
+ Project-URL: Documentation, https://github.com/Geoeoeo217/loop-autopsy#readme
10
+ Keywords: jupyter,ipython,education,infinite-loop,debugging,beginners
11
+ Classifier: Framework :: IPython
12
+ Classifier: Framework :: Jupyter
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Education
17
+ Classifier: Topic :: Software Development :: Debuggers
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: ipython>=7.0
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # loop-autopsy
27
+
28
+ **Stops runaway loops in Jupyter and shows the evidence for why they never ended.**
29
+
30
+ ```python
31
+ %load_ext loop_autopsy
32
+
33
+ count = 1
34
+ while count <= 5:
35
+ print(count)
36
+ ```
37
+
38
+ Instead of freezing the notebook, the loop is stopped after 3 rounds and you get:
39
+
40
+ ![A loop-autopsy report for a stuck loop](https://raw.githubusercontent.com/Geoeoeo217/loop-autopsy/main/docs/report.png)
41
+
42
+ ```
43
+ Loop autopsy — stuck loop (line 2)
44
+ while count <= 5:
45
+
46
+ Nothing changed between rounds: `count = 1` stayed exactly the same, so the
47
+ condition `count <= 5` is True every time and the loop can never end.
48
+
49
+ Last rounds checked:
50
+ round count (same)
51
+ ----- ------------
52
+ 1 1
53
+ 2 1
54
+ 3 1
55
+
56
+ Hint: Add a line inside the loop that changes `count` so that `count <= 5`
57
+ eventually becomes False (for example `count += 1`).
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Motivation
63
+
64
+ Every beginner writes an infinite loop. In a notebook on a shared JupyterHub this
65
+ is worse than it sounds:
66
+
67
+ - the cell hangs with no explanation, and the only remedy is the stop button ■;
68
+ - a `print` inside the loop floods the browser with output until the tab freezes;
69
+ - the loop keeps using CPU and memory on a server shared by the whole class.
70
+
71
+ Pressing ■ stops the loop but teaches nothing. The student still does not know
72
+ *which* variable failed to change or *why* the condition never became False.
73
+
74
+ ## Innovation
75
+
76
+ loop-autopsy watches loops **while they run** and stops them only when it can
77
+ **prove** they will never end. Then it shows the evidence.
78
+
79
+ | | Stops a stuck loop? | Explains *why*? | When? |
80
+ |---|---|---|---|
81
+ | Stop button ■ | yes (you press it) | no | whenever you notice |
82
+ | Timeout decorators (e.g. `wrapt_timeout_decorator`) | yes, after a fixed time | no | after the time limit |
83
+ | Error explainers (e.g. `friendly-traceback`) | no | explains *errors*, but an infinite loop raises none | after a crash |
84
+ | Memory monitors (e.g. `jupyter-resource-usage`) | no | no | shows totals only |
85
+ | **loop-autopsy** | **yes, automatically** | **yes, with a replay table** | **during the loop, often after 3 rounds** |
86
+
87
+ How it works, in short:
88
+
89
+ 1. An IPython **AST transformer** (it edits the code's syntax tree before it runs)
90
+ adds a tiny check to every `while` and `for` loop you type in a cell.
91
+ 2. Each check takes a **fingerprint** of the loop's variables. If a deterministic
92
+ loop comes back to an *exactly identical* state, it must repeat forever: that
93
+ is a proof, not a guess.
94
+ 3. Further rules catch a counter moving *away* from its target, a `!=` condition
95
+ that jumps over its stop value (`0.1 + 0.1 + ...` never equals `1.0`), a list
96
+ that grows while you loop over it, and memory running out.
97
+ 4. Anything it cannot prove falls back to an honest time limit. No AI model, no
98
+ internet, and the same result every time.
99
+
100
+ Every rule and its numbers are listed below, and every number can be changed (see Settings).
101
+
102
+ ## What it detects
103
+
104
+ | Pattern | Example | Stopped after |
105
+ |---|---|---|
106
+ | **Stuck**: nothing changes | `while count <= 5: print(count)` | 3 rounds |
107
+ | **Cycle**: values go round in a circle | `while a < 5: a = (a + 1) % 3` | about 2 cycles |
108
+ | **Wrong direction** | `while i < 10: i -= 1` | 500 checks |
109
+ | **Jumped over** the stop value | `while x != 1.0: x += 0.1` | about 100 checks |
110
+ | **Growing list** in a `for` loop | `for n in nums: nums.append(n)` | 2,000 rounds |
111
+ | **Memory** about to run out | a loop filling a huge list | at 85% of the limit |
112
+ | **Too long** (fallback) | anything else | 60 s |
113
+
114
+ Plus two extras:
115
+
116
+ - **Press ■ yourself** and you still get the replay table, showing which
117
+ variables never changed.
118
+ - **Cell health bar**: time, memory and loops for a cell, coloured green / yellow /
119
+ red. It warns when memory passes 75% of your limit and names the biggest
120
+ variables (pandas and NumPy aware). It also mutes output after 1,000 printed
121
+ lines so the browser stays responsive.
122
+
123
+ ## Example use cases
124
+
125
+ - **Students** in an intro course: load it once at the top of every notebook and
126
+ never lose a session to a frozen loop again.
127
+ - **Instructors** on JupyterHub: load it for everyone (see below) to protect the
128
+ shared server and turn every infinite loop into a mini-lesson.
129
+ - **Teaching assistants**: the replay table shows at a glance what went wrong in
130
+ a student's loop.
131
+ - **Data notebooks**: the health bar shows which cell is slow or memory-hungry.
132
+
133
+ ## Setup
134
+
135
+ ```bash
136
+ pip install loop-autopsy
137
+ ```
138
+
139
+ The only requirement is IPython (already present in Jupyter). Python 3.9+.
140
+
141
+ In a notebook:
142
+
143
+ ```python
144
+ %load_ext loop_autopsy
145
+ ```
146
+
147
+ **For instructors**: to turn it on for every notebook, add this to the IPython
148
+ profile (`~/.ipython/profile_default/ipython_config.py`):
149
+
150
+ ```python
151
+ c.InteractiveShellApp.extensions = ["loop_autopsy"]
152
+ ```
153
+
154
+ ## Usage
155
+
156
+ | Command | Effect |
157
+ |---|---|
158
+ | `%autopsy` | status and settings |
159
+ | `%autopsy on` / `off` | start / stop watching loops |
160
+ | `%autopsy last` | show the last report again |
161
+ | `%autopsy heatmap on` / `off` / `auto` | cell health bar (`auto` = only when a cell is slow or memory-heavy) |
162
+ | `%autopsy config time_limit=120` | change a setting (see Settings below) |
163
+ | `%%autopsy off` | first line of a cell: run that cell without watching |
164
+ | `%autopsy help` | help |
165
+
166
+ Cells that use `%timeit` / `%%timeit` are never watched, so timings stay accurate.
167
+ Loops inside generators (`yield`) are left alone, because infinite generators are
168
+ normal.
169
+
170
+ Try [examples/demo.ipynb](examples/demo.ipynb) for every feature in one notebook.
171
+
172
+ ## Settings
173
+
174
+ Change any of these with `%autopsy config key=value` (for example `%autopsy config time_limit=120`).
175
+
176
+ | Setting | Default | Meaning |
177
+ |---|---|---|
178
+ | `time_limit` | 60 | seconds before a loop that cannot be proven stuck is stopped (0 = never) |
179
+ | `direction_after` | 500 | checks in a row moving away from the stop value before "wrong direction" |
180
+ | `jump_after` | 100 | checks in a row after jumping over a `!=` stop value |
181
+ | `growth_after` | 2000 | rounds before a growing list in a `for` loop is reported |
182
+ | `memory_stop` | 0.85 | stop a loop when memory reaches this fraction of the limit |
183
+ | `memory_warn` | 0.75 | health bar warning at this fraction of the limit |
184
+ | `max_output_lines` | 1000 | printed lines per cell before output is muted (0 = off) |
185
+ | `replay_rows` | 6 | rows in the replay table |
186
+ | `max_watched` | 25 | variables watched per loop |
187
+ | `heatmap` | `auto` | `on`, `off` or `auto` |
188
+ | `time_yellow` / `time_red` | 1.0 / 10.0 | seconds for a yellow / red health bar |
189
+ | `mem_yellow` / `mem_red` | 0.50 / 0.80 | memory fraction for a yellow / red health bar |
190
+
191
+ The memory limit is read from JupyterHub's `MEM_LIMIT` setting, then the container's
192
+ memory limit, then the computer's total RAM.
193
+
194
+ ## Limitations (honest list)
195
+
196
+ - No tool can detect *every* infinite loop (the halting problem). loop-autopsy
197
+ only reports a stuck, cycling, wrong-direction or jumped-over loop when it can
198
+ prove it; everything else gets the time limit.
199
+ - Loops that call your own functions or modules such as `random` cannot be proven
200
+ stuck (those calls might change hidden state), so they rely on the time limit.
201
+ - Only code typed in notebook cells is watched, not code inside imported modules.
202
+ - Watching adds a small cost to each loop round (roughly 0.1–0.2 µs per round on
203
+ simple loops). Use `%%autopsy off` for heavy number crunching.
204
+ - A bare `except:` inside a loop can catch the stop signal, just as it catches ■.
205
+
206
+ ## Development
207
+
208
+ ```bash
209
+ git clone https://github.com/Geoeoeo217/loop-autopsy
210
+ cd loop-autopsy
211
+ pip install -e ".[test]"
212
+ pytest
213
+ ```
214
+
215
+ ## Team
216
+
217
+ Built by a team of five for CS1302 (Introduction to Computer Programming).
218
+
219
+ | Member | Part |
220
+ |---|---|
221
+ | NATHANAEL George Steindhart | project lead: loop rewriting (`transformer.py`), run-time glue (`monitor.py`), magics, packaging |
222
+ | SETIAWAN Matthew Henson | detection rules: stuck, cycle, wrong direction, jumped over, time limit (`detectors.py`) |
223
+ | LATHIF Muhammad Akmal | memory and growing-list rules (`growth.py`) |
224
+ | KWAN Kendrick Matthew | replay table and reports (`report.py`) |
225
+ | HANDAYA Clement Steward | cell health bar and output guard (`heatmap.py`) |
226
+
227
+
228
+ ## License
229
+
230
+ MIT
@@ -0,0 +1,205 @@
1
+ # loop-autopsy
2
+
3
+ **Stops runaway loops in Jupyter and shows the evidence for why they never ended.**
4
+
5
+ ```python
6
+ %load_ext loop_autopsy
7
+
8
+ count = 1
9
+ while count <= 5:
10
+ print(count)
11
+ ```
12
+
13
+ Instead of freezing the notebook, the loop is stopped after 3 rounds and you get:
14
+
15
+ ![A loop-autopsy report for a stuck loop](https://raw.githubusercontent.com/Geoeoeo217/loop-autopsy/main/docs/report.png)
16
+
17
+ ```
18
+ Loop autopsy — stuck loop (line 2)
19
+ while count <= 5:
20
+
21
+ Nothing changed between rounds: `count = 1` stayed exactly the same, so the
22
+ condition `count <= 5` is True every time and the loop can never end.
23
+
24
+ Last rounds checked:
25
+ round count (same)
26
+ ----- ------------
27
+ 1 1
28
+ 2 1
29
+ 3 1
30
+
31
+ Hint: Add a line inside the loop that changes `count` so that `count <= 5`
32
+ eventually becomes False (for example `count += 1`).
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Motivation
38
+
39
+ Every beginner writes an infinite loop. In a notebook on a shared JupyterHub this
40
+ is worse than it sounds:
41
+
42
+ - the cell hangs with no explanation, and the only remedy is the stop button ■;
43
+ - a `print` inside the loop floods the browser with output until the tab freezes;
44
+ - the loop keeps using CPU and memory on a server shared by the whole class.
45
+
46
+ Pressing ■ stops the loop but teaches nothing. The student still does not know
47
+ *which* variable failed to change or *why* the condition never became False.
48
+
49
+ ## Innovation
50
+
51
+ loop-autopsy watches loops **while they run** and stops them only when it can
52
+ **prove** they will never end. Then it shows the evidence.
53
+
54
+ | | Stops a stuck loop? | Explains *why*? | When? |
55
+ |---|---|---|---|
56
+ | Stop button ■ | yes (you press it) | no | whenever you notice |
57
+ | Timeout decorators (e.g. `wrapt_timeout_decorator`) | yes, after a fixed time | no | after the time limit |
58
+ | Error explainers (e.g. `friendly-traceback`) | no | explains *errors*, but an infinite loop raises none | after a crash |
59
+ | Memory monitors (e.g. `jupyter-resource-usage`) | no | no | shows totals only |
60
+ | **loop-autopsy** | **yes, automatically** | **yes, with a replay table** | **during the loop, often after 3 rounds** |
61
+
62
+ How it works, in short:
63
+
64
+ 1. An IPython **AST transformer** (it edits the code's syntax tree before it runs)
65
+ adds a tiny check to every `while` and `for` loop you type in a cell.
66
+ 2. Each check takes a **fingerprint** of the loop's variables. If a deterministic
67
+ loop comes back to an *exactly identical* state, it must repeat forever: that
68
+ is a proof, not a guess.
69
+ 3. Further rules catch a counter moving *away* from its target, a `!=` condition
70
+ that jumps over its stop value (`0.1 + 0.1 + ...` never equals `1.0`), a list
71
+ that grows while you loop over it, and memory running out.
72
+ 4. Anything it cannot prove falls back to an honest time limit. No AI model, no
73
+ internet, and the same result every time.
74
+
75
+ Every rule and its numbers are listed below, and every number can be changed (see Settings).
76
+
77
+ ## What it detects
78
+
79
+ | Pattern | Example | Stopped after |
80
+ |---|---|---|
81
+ | **Stuck**: nothing changes | `while count <= 5: print(count)` | 3 rounds |
82
+ | **Cycle**: values go round in a circle | `while a < 5: a = (a + 1) % 3` | about 2 cycles |
83
+ | **Wrong direction** | `while i < 10: i -= 1` | 500 checks |
84
+ | **Jumped over** the stop value | `while x != 1.0: x += 0.1` | about 100 checks |
85
+ | **Growing list** in a `for` loop | `for n in nums: nums.append(n)` | 2,000 rounds |
86
+ | **Memory** about to run out | a loop filling a huge list | at 85% of the limit |
87
+ | **Too long** (fallback) | anything else | 60 s |
88
+
89
+ Plus two extras:
90
+
91
+ - **Press ■ yourself** and you still get the replay table, showing which
92
+ variables never changed.
93
+ - **Cell health bar**: time, memory and loops for a cell, coloured green / yellow /
94
+ red. It warns when memory passes 75% of your limit and names the biggest
95
+ variables (pandas and NumPy aware). It also mutes output after 1,000 printed
96
+ lines so the browser stays responsive.
97
+
98
+ ## Example use cases
99
+
100
+ - **Students** in an intro course: load it once at the top of every notebook and
101
+ never lose a session to a frozen loop again.
102
+ - **Instructors** on JupyterHub: load it for everyone (see below) to protect the
103
+ shared server and turn every infinite loop into a mini-lesson.
104
+ - **Teaching assistants**: the replay table shows at a glance what went wrong in
105
+ a student's loop.
106
+ - **Data notebooks**: the health bar shows which cell is slow or memory-hungry.
107
+
108
+ ## Setup
109
+
110
+ ```bash
111
+ pip install loop-autopsy
112
+ ```
113
+
114
+ The only requirement is IPython (already present in Jupyter). Python 3.9+.
115
+
116
+ In a notebook:
117
+
118
+ ```python
119
+ %load_ext loop_autopsy
120
+ ```
121
+
122
+ **For instructors**: to turn it on for every notebook, add this to the IPython
123
+ profile (`~/.ipython/profile_default/ipython_config.py`):
124
+
125
+ ```python
126
+ c.InteractiveShellApp.extensions = ["loop_autopsy"]
127
+ ```
128
+
129
+ ## Usage
130
+
131
+ | Command | Effect |
132
+ |---|---|
133
+ | `%autopsy` | status and settings |
134
+ | `%autopsy on` / `off` | start / stop watching loops |
135
+ | `%autopsy last` | show the last report again |
136
+ | `%autopsy heatmap on` / `off` / `auto` | cell health bar (`auto` = only when a cell is slow or memory-heavy) |
137
+ | `%autopsy config time_limit=120` | change a setting (see Settings below) |
138
+ | `%%autopsy off` | first line of a cell: run that cell without watching |
139
+ | `%autopsy help` | help |
140
+
141
+ Cells that use `%timeit` / `%%timeit` are never watched, so timings stay accurate.
142
+ Loops inside generators (`yield`) are left alone, because infinite generators are
143
+ normal.
144
+
145
+ Try [examples/demo.ipynb](examples/demo.ipynb) for every feature in one notebook.
146
+
147
+ ## Settings
148
+
149
+ Change any of these with `%autopsy config key=value` (for example `%autopsy config time_limit=120`).
150
+
151
+ | Setting | Default | Meaning |
152
+ |---|---|---|
153
+ | `time_limit` | 60 | seconds before a loop that cannot be proven stuck is stopped (0 = never) |
154
+ | `direction_after` | 500 | checks in a row moving away from the stop value before "wrong direction" |
155
+ | `jump_after` | 100 | checks in a row after jumping over a `!=` stop value |
156
+ | `growth_after` | 2000 | rounds before a growing list in a `for` loop is reported |
157
+ | `memory_stop` | 0.85 | stop a loop when memory reaches this fraction of the limit |
158
+ | `memory_warn` | 0.75 | health bar warning at this fraction of the limit |
159
+ | `max_output_lines` | 1000 | printed lines per cell before output is muted (0 = off) |
160
+ | `replay_rows` | 6 | rows in the replay table |
161
+ | `max_watched` | 25 | variables watched per loop |
162
+ | `heatmap` | `auto` | `on`, `off` or `auto` |
163
+ | `time_yellow` / `time_red` | 1.0 / 10.0 | seconds for a yellow / red health bar |
164
+ | `mem_yellow` / `mem_red` | 0.50 / 0.80 | memory fraction for a yellow / red health bar |
165
+
166
+ The memory limit is read from JupyterHub's `MEM_LIMIT` setting, then the container's
167
+ memory limit, then the computer's total RAM.
168
+
169
+ ## Limitations (honest list)
170
+
171
+ - No tool can detect *every* infinite loop (the halting problem). loop-autopsy
172
+ only reports a stuck, cycling, wrong-direction or jumped-over loop when it can
173
+ prove it; everything else gets the time limit.
174
+ - Loops that call your own functions or modules such as `random` cannot be proven
175
+ stuck (those calls might change hidden state), so they rely on the time limit.
176
+ - Only code typed in notebook cells is watched, not code inside imported modules.
177
+ - Watching adds a small cost to each loop round (roughly 0.1–0.2 µs per round on
178
+ simple loops). Use `%%autopsy off` for heavy number crunching.
179
+ - A bare `except:` inside a loop can catch the stop signal, just as it catches ■.
180
+
181
+ ## Development
182
+
183
+ ```bash
184
+ git clone https://github.com/Geoeoeo217/loop-autopsy
185
+ cd loop-autopsy
186
+ pip install -e ".[test]"
187
+ pytest
188
+ ```
189
+
190
+ ## Team
191
+
192
+ Built by a team of five for CS1302 (Introduction to Computer Programming).
193
+
194
+ | Member | Part |
195
+ |---|---|
196
+ | NATHANAEL George Steindhart | project lead: loop rewriting (`transformer.py`), run-time glue (`monitor.py`), magics, packaging |
197
+ | SETIAWAN Matthew Henson | detection rules: stuck, cycle, wrong direction, jumped over, time limit (`detectors.py`) |
198
+ | LATHIF Muhammad Akmal | memory and growing-list rules (`growth.py`) |
199
+ | KWAN Kendrick Matthew | replay table and reports (`report.py`) |
200
+ | HANDAYA Clement Steward | cell health bar and output guard (`heatmap.py`) |
201
+
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "loop-autopsy"
7
+ version = "0.1.0"
8
+ description = "Stops runaway loops in Jupyter and shows the evidence for why they never ended."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "NATHANAEL George Steindhart" },
14
+ { name = "SETIAWAN Matthew Henson" },
15
+ { name = "LATHIF Muhammad Akmal" },
16
+ { name = "KWAN Kendrick Matthew" },
17
+ { name = "HANDAYA Clement Steward" },
18
+ ]
19
+ keywords = ["jupyter", "ipython", "education", "infinite-loop", "debugging", "beginners"]
20
+ classifiers = [
21
+ "Framework :: IPython",
22
+ "Framework :: Jupyter",
23
+ "Intended Audience :: Education",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3",
26
+ "Topic :: Education",
27
+ "Topic :: Software Development :: Debuggers",
28
+ ]
29
+ dependencies = ["ipython>=7.0"]
30
+
31
+ [project.optional-dependencies]
32
+ test = ["pytest"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/Geoeoeo217/loop-autopsy"
36
+ Repository = "https://github.com/Geoeoeo217/loop-autopsy"
37
+ Documentation = "https://github.com/Geoeoeo217/loop-autopsy#readme"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ pythonpath = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,57 @@
1
+ """loop-autopsy: stops runaway loops in Jupyter and shows the evidence for why
2
+ they never ended.
3
+
4
+ Usage in a notebook::
5
+
6
+ %load_ext loop_autopsy
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ from .config import Finding, LoopStopped, Settings # noqa: F401
14
+
15
+ _LOADED: dict = {}
16
+
17
+
18
+ def load_ipython_extension(ip) -> None:
19
+ from .heatmap import CellHealth
20
+ from .magics import AutopsyMagics
21
+ from .monitor import Monitor
22
+ from .report import display_report
23
+ from .transformer import RUNTIME_NAME, LoopTransformer
24
+
25
+ if id(ip) in _LOADED:
26
+ print("loop-autopsy is already loaded. Use %autopsy help for options.")
27
+ return
28
+ settings = Settings()
29
+ monitor = Monitor(ip, settings)
30
+ transformer = LoopTransformer(monitor.register, lambda: settings.max_watched, monitor.is_paused)
31
+ health = CellHealth(ip, settings, monitor.stats, show=display_report)
32
+
33
+ ip.user_ns[RUNTIME_NAME] = monitor
34
+ ip.user_ns_hidden[RUNTIME_NAME] = monitor # keep it out of %who / %whos
35
+ ip.ast_transformers.append(transformer)
36
+ ip.events.register("pre_run_cell", monitor.pre_run_cell)
37
+ health.register()
38
+ monitor.install_exception_handler()
39
+ ip.register_magics(AutopsyMagics(ip, monitor))
40
+ _LOADED[id(ip)] = (monitor, transformer, health)
41
+ print(f"loop-autopsy {__version__} is watching your loops. Type %autopsy help for options.")
42
+
43
+
44
+ def unload_ipython_extension(ip) -> None:
45
+ entry = _LOADED.pop(id(ip), None)
46
+ if entry is None:
47
+ return
48
+ monitor, transformer, health = entry
49
+ monitor.enabled = False # functions instrumented earlier keep working, unwatched
50
+ if transformer in ip.ast_transformers:
51
+ ip.ast_transformers.remove(transformer)
52
+ try:
53
+ ip.events.unregister("pre_run_cell", monitor.pre_run_cell)
54
+ except ValueError:
55
+ pass
56
+ health.unregister()
57
+ monitor.remove_exception_handler()
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass, field, fields
5
+ from typing import Any, Optional, Tuple
6
+
7
+
8
+ class LoopStopped(BaseException):
9
+ """Raised inside a watched loop to stop it.
10
+
11
+ It derives from BaseException (like KeyboardInterrupt) so that a normal
12
+ ``except Exception:`` block inside the student's loop cannot swallow it.
13
+ """
14
+
15
+ def __init__(self, finding: "Finding"):
16
+ super().__init__(finding.title)
17
+ self.finding = finding
18
+
19
+
20
+ @dataclass
21
+ class Finding:
22
+ """What a detection rule proved about a loop."""
23
+
24
+ kind: str # stuck | cycle | wrong_direction | jumped_over | growing_iterable | memory | time_limit
25
+ title: str
26
+ explanation: str
27
+ hint: str
28
+ culprits: Tuple[str, ...] = ()
29
+
30
+
31
+ @dataclass
32
+ class LoopInfo:
33
+ """Static facts about one loop, collected when the cell is transformed."""
34
+
35
+ loop_id: int
36
+ kind: str # "while" | "for"
37
+ lineno: int
38
+ header: str
39
+ test_src: str = ""
40
+ cond_names: Tuple[str, ...] = ()
41
+ watched: Tuple[str, ...] = ()
42
+ compare: Optional[tuple] = None # (left, op, right); side = ("name", n) | ("const", v)
43
+ iter_name: Optional[str] = None
44
+ receivers: Tuple[str, ...] = () # names used as ``name.method(...)``; checked at run time
45
+ repeat_safe: bool = True
46
+ waits_for_user: bool = False
47
+
48
+
49
+ @dataclass
50
+ class LoopState:
51
+ """Run-time record of one execution of a loop."""
52
+
53
+ info: LoopInfo
54
+ started: float
55
+ replay_rows: int = 6
56
+ rounds: int = 0
57
+ checks: int = 0
58
+ stride: int = 1
59
+ next_check: int = 1
60
+ last_check: float = 0.0
61
+ history: deque = field(default_factory=deque)
62
+ scratch: dict = field(default_factory=dict)
63
+ stopped: Optional[Finding] = None
64
+
65
+ def __post_init__(self):
66
+ self.history = deque(maxlen=max(1, self.replay_rows))
67
+ self.last_check = self.started
68
+
69
+
70
+ @dataclass
71
+ class Settings:
72
+ """User-adjustable settings (``%autopsy config key=value``)."""
73
+
74
+ time_limit: float = 60.0
75
+ direction_after: int = 500
76
+ jump_after: int = 100
77
+ growth_after: int = 2000
78
+ memory_stop: float = 0.85
79
+ memory_warn: float = 0.75
80
+ max_output_lines: int = 1000
81
+ replay_rows: int = 6
82
+ max_watched: int = 25
83
+ heatmap: str = "auto" # on | off | auto
84
+ time_yellow: float = 1.0
85
+ time_red: float = 10.0
86
+ mem_yellow: float = 0.50
87
+ mem_red: float = 0.80
88
+
89
+ def update(self, key: str, raw: str) -> Any:
90
+ """Set ``key`` from a string such as ``"30"``; returns the new value."""
91
+ names = {f.name: f for f in fields(self)}
92
+ if key not in names:
93
+ raise KeyError(f"unknown setting {key!r}; choose from: {', '.join(names)}")
94
+ current = getattr(self, key)
95
+ if isinstance(current, str):
96
+ value: Any = raw.strip().strip("'\"").lower()
97
+ if key == "heatmap" and value not in ("on", "off", "auto"):
98
+ raise ValueError("heatmap must be on, off or auto")
99
+ elif isinstance(current, int) and not isinstance(current, bool):
100
+ value = int(float(raw))
101
+ else:
102
+ value = float(raw)
103
+ if isinstance(value, (int, float)) and value < 0:
104
+ raise ValueError(f"{key} cannot be negative")
105
+ setattr(self, key, value)
106
+ return value
107
+
108
+ def as_dict(self) -> dict:
109
+ return {f.name: getattr(self, f.name) for f in fields(self)}