pybreakz 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.
- pybreakz-0.1.0/LICENSE +21 -0
- pybreakz-0.1.0/PKG-INFO +178 -0
- pybreakz-0.1.0/README.md +156 -0
- pybreakz-0.1.0/pyproject.toml +32 -0
- pybreakz-0.1.0/setup.cfg +4 -0
- pybreakz-0.1.0/src/pybreakz/__init__.py +3 -0
- pybreakz-0.1.0/src/pybreakz/cli.py +463 -0
- pybreakz-0.1.0/src/pybreakz.egg-info/PKG-INFO +178 -0
- pybreakz-0.1.0/src/pybreakz.egg-info/SOURCES.txt +10 -0
- pybreakz-0.1.0/src/pybreakz.egg-info/dependency_links.txt +1 -0
- pybreakz-0.1.0/src/pybreakz.egg-info/entry_points.txt +2 -0
- pybreakz-0.1.0/src/pybreakz.egg-info/top_level.txt +1 -0
pybreakz-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Allan Napier
|
|
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.
|
pybreakz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pybreakz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A zero-dependency Python debugger CLI built for Claude Code
|
|
5
|
+
Author: Allan Napier
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/allannapier/py_debug_cli
|
|
8
|
+
Project-URL: Repository, https://github.com/allannapier/py_debug_cli
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# pybreakz
|
|
24
|
+
|
|
25
|
+
A zero-dependency Python debugger CLI built for Claude Code.
|
|
26
|
+
|
|
27
|
+
Run your script with breakpoints, capture locals and stack state, get clean structured output — all in one shot. No interactive session required.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install pybreakz
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or install from source:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/allannapier/py_debug_cli.git
|
|
41
|
+
cd py_debug_cli
|
|
42
|
+
pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.9+. No pip dependencies.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Basic breakpoints
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pybreakz run script.py --breakpoints 42
|
|
55
|
+
pybreakz run script.py --breakpoints 10,25,42
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Captures all locals at each breakpoint hit.
|
|
59
|
+
|
|
60
|
+
### Watch specific expressions
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pybreakz run script.py --breakpoints 42 --watch "user_id,response.status,len(items)"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Evaluates each expression in the breakpoint's local scope.
|
|
67
|
+
|
|
68
|
+
### Evaluate arbitrary expressions
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pybreakz run script.py --breakpoints 42 --eval "df.shape,type(result),items[:3]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Like `--watch` but separate section in output — useful for computed values.
|
|
75
|
+
|
|
76
|
+
### Capture on exception
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pybreakz run script.py --on-exception
|
|
80
|
+
pybreakz run script.py --on-exception --watch "self,request,data"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Captures locals, stack, and full traceback at the crash site.
|
|
84
|
+
|
|
85
|
+
### Conditional breakpoints
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pybreakz run script.py --breakpoints "42:x>100,67:status=='error'"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Only fires when the condition evaluates to True.
|
|
92
|
+
|
|
93
|
+
### Pass arguments to your script
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pybreakz run script.py --breakpoints 10 -- --input data.csv --verbose
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Anything after `--` is forwarded to the script as `sys.argv`.
|
|
100
|
+
|
|
101
|
+
### JSON output (for programmatic use)
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pybreakz run script.py --breakpoints 42 --format json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Timeout
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pybreakz run script.py --breakpoints 10 --timeout 60 # 60s limit
|
|
111
|
+
pybreakz run script.py --breakpoints 10 --timeout 0 # no limit
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Default timeout is 30 seconds.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Example output
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
════════════════════════════════════════════════════════════
|
|
122
|
+
pybreakz report → script.py
|
|
123
|
+
════════════════════════════════════════════════════════════
|
|
124
|
+
|
|
125
|
+
┌─ BREAKPOINT HIT #1 script.py:42
|
|
126
|
+
│ Source: result = process(item)
|
|
127
|
+
│
|
|
128
|
+
│ Call Stack (innermost last):
|
|
129
|
+
│ <module>() [script.py:80]
|
|
130
|
+
│ main() [script.py:60]
|
|
131
|
+
│ run_batch() [script.py:42]
|
|
132
|
+
│
|
|
133
|
+
│ Locals:
|
|
134
|
+
│ items = list[150 items]: [{'id': 1, ...}, ...]
|
|
135
|
+
│ item = {'id': 1, 'name': 'foo', 'active': True}
|
|
136
|
+
│ result = None
|
|
137
|
+
│ Watched:
|
|
138
|
+
│ item['id'] = 1
|
|
139
|
+
│ len(items) = 150
|
|
140
|
+
└────────────────────────────────────────────────────────────
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## How Claude Code uses it
|
|
146
|
+
|
|
147
|
+
Claude Code treats `pybreakz` like any other shell command. A typical debugging loop:
|
|
148
|
+
|
|
149
|
+
1. Claude reads your code and identifies suspicious lines
|
|
150
|
+
2. Calls `pybreakz run script.py --breakpoints 34,67 --watch "x,response"`
|
|
151
|
+
3. Reads the report output
|
|
152
|
+
4. Adjusts breakpoints or patches the bug
|
|
153
|
+
5. Repeats if needed
|
|
154
|
+
|
|
155
|
+
The `--on-exception` flag is especially useful as a first pass — just run it and let the crash tell Claude where to look.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Options reference
|
|
160
|
+
|
|
161
|
+
| Flag | Short | Description |
|
|
162
|
+
|------|-------|-------------|
|
|
163
|
+
| `--breakpoints LINES` | `-b` | Comma-separated line numbers, optionally with conditions (`42:x>0`) |
|
|
164
|
+
| `--watch EXPRS` | `-w` | Comma-separated expressions to evaluate at each hit |
|
|
165
|
+
| `--eval EXPRS` | `-e` | Additional expressions to evaluate (shown separately) |
|
|
166
|
+
| `--on-exception` | `-x` | Capture state on unhandled exception |
|
|
167
|
+
| `--timeout SECS` | `-t` | Script timeout in seconds (default: 30) |
|
|
168
|
+
| `--format FORMAT` | `-f` | Output format: `text` (default) or `json` |
|
|
169
|
+
| `--max-hits N` | | Max breakpoint hits to record (default: 20) |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Limitations
|
|
174
|
+
|
|
175
|
+
- Works on `.py` scripts run directly. Not designed for long-running servers or async event loops.
|
|
176
|
+
- Breakpoints must be on **executable lines** (not blank lines, comments, or `def`/`class` headers — use the first line inside the function body).
|
|
177
|
+
- `sys.settrace` has a small performance overhead — not suitable for tight performance benchmarks.
|
|
178
|
+
- Multi-threaded scripts: only the main thread is traced.
|
pybreakz-0.1.0/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# pybreakz
|
|
2
|
+
|
|
3
|
+
A zero-dependency Python debugger CLI built for Claude Code.
|
|
4
|
+
|
|
5
|
+
Run your script with breakpoints, capture locals and stack state, get clean structured output — all in one shot. No interactive session required.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pybreakz
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install from source:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/allannapier/py_debug_cli.git
|
|
19
|
+
cd py_debug_cli
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Python 3.9+. No pip dependencies.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Basic breakpoints
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pybreakz run script.py --breakpoints 42
|
|
33
|
+
pybreakz run script.py --breakpoints 10,25,42
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Captures all locals at each breakpoint hit.
|
|
37
|
+
|
|
38
|
+
### Watch specific expressions
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pybreakz run script.py --breakpoints 42 --watch "user_id,response.status,len(items)"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Evaluates each expression in the breakpoint's local scope.
|
|
45
|
+
|
|
46
|
+
### Evaluate arbitrary expressions
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pybreakz run script.py --breakpoints 42 --eval "df.shape,type(result),items[:3]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Like `--watch` but separate section in output — useful for computed values.
|
|
53
|
+
|
|
54
|
+
### Capture on exception
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pybreakz run script.py --on-exception
|
|
58
|
+
pybreakz run script.py --on-exception --watch "self,request,data"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Captures locals, stack, and full traceback at the crash site.
|
|
62
|
+
|
|
63
|
+
### Conditional breakpoints
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pybreakz run script.py --breakpoints "42:x>100,67:status=='error'"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Only fires when the condition evaluates to True.
|
|
70
|
+
|
|
71
|
+
### Pass arguments to your script
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pybreakz run script.py --breakpoints 10 -- --input data.csv --verbose
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Anything after `--` is forwarded to the script as `sys.argv`.
|
|
78
|
+
|
|
79
|
+
### JSON output (for programmatic use)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pybreakz run script.py --breakpoints 42 --format json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Timeout
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pybreakz run script.py --breakpoints 10 --timeout 60 # 60s limit
|
|
89
|
+
pybreakz run script.py --breakpoints 10 --timeout 0 # no limit
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Default timeout is 30 seconds.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Example output
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
════════════════════════════════════════════════════════════
|
|
100
|
+
pybreakz report → script.py
|
|
101
|
+
════════════════════════════════════════════════════════════
|
|
102
|
+
|
|
103
|
+
┌─ BREAKPOINT HIT #1 script.py:42
|
|
104
|
+
│ Source: result = process(item)
|
|
105
|
+
│
|
|
106
|
+
│ Call Stack (innermost last):
|
|
107
|
+
│ <module>() [script.py:80]
|
|
108
|
+
│ main() [script.py:60]
|
|
109
|
+
│ run_batch() [script.py:42]
|
|
110
|
+
│
|
|
111
|
+
│ Locals:
|
|
112
|
+
│ items = list[150 items]: [{'id': 1, ...}, ...]
|
|
113
|
+
│ item = {'id': 1, 'name': 'foo', 'active': True}
|
|
114
|
+
│ result = None
|
|
115
|
+
│ Watched:
|
|
116
|
+
│ item['id'] = 1
|
|
117
|
+
│ len(items) = 150
|
|
118
|
+
└────────────────────────────────────────────────────────────
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## How Claude Code uses it
|
|
124
|
+
|
|
125
|
+
Claude Code treats `pybreakz` like any other shell command. A typical debugging loop:
|
|
126
|
+
|
|
127
|
+
1. Claude reads your code and identifies suspicious lines
|
|
128
|
+
2. Calls `pybreakz run script.py --breakpoints 34,67 --watch "x,response"`
|
|
129
|
+
3. Reads the report output
|
|
130
|
+
4. Adjusts breakpoints or patches the bug
|
|
131
|
+
5. Repeats if needed
|
|
132
|
+
|
|
133
|
+
The `--on-exception` flag is especially useful as a first pass — just run it and let the crash tell Claude where to look.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Options reference
|
|
138
|
+
|
|
139
|
+
| Flag | Short | Description |
|
|
140
|
+
|------|-------|-------------|
|
|
141
|
+
| `--breakpoints LINES` | `-b` | Comma-separated line numbers, optionally with conditions (`42:x>0`) |
|
|
142
|
+
| `--watch EXPRS` | `-w` | Comma-separated expressions to evaluate at each hit |
|
|
143
|
+
| `--eval EXPRS` | `-e` | Additional expressions to evaluate (shown separately) |
|
|
144
|
+
| `--on-exception` | `-x` | Capture state on unhandled exception |
|
|
145
|
+
| `--timeout SECS` | `-t` | Script timeout in seconds (default: 30) |
|
|
146
|
+
| `--format FORMAT` | `-f` | Output format: `text` (default) or `json` |
|
|
147
|
+
| `--max-hits N` | | Max breakpoint hits to record (default: 20) |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Limitations
|
|
152
|
+
|
|
153
|
+
- Works on `.py` scripts run directly. Not designed for long-running servers or async event loops.
|
|
154
|
+
- Breakpoints must be on **executable lines** (not blank lines, comments, or `def`/`class` headers — use the first line inside the function body).
|
|
155
|
+
- `sys.settrace` has a small performance overhead — not suitable for tight performance benchmarks.
|
|
156
|
+
- Multi-threaded scripts: only the main thread is traced.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pybreakz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A zero-dependency Python debugger CLI built for Claude Code"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Allan Napier" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Debuggers",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
pybreakz = "pybreakz.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/allannapier/py_debug_cli"
|
|
32
|
+
Repository = "https://github.com/allannapier/py_debug_cli"
|
pybreakz-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
pybreakz - A CLI debugger for Claude Code
|
|
4
|
+
Run Python scripts with breakpoints and get structured output.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
pybreakz run script.py --breakpoints 10,25,42
|
|
8
|
+
pybreakz run script.py --breakpoints 42 --watch "x,result"
|
|
9
|
+
pybreakz run script.py --on-exception
|
|
10
|
+
pybreakz run script.py --breakpoints "42:x>10" --eval "len(items),type(r)"
|
|
11
|
+
pybreakz run script.py --breakpoints 10 -- --my-arg foo --other-arg 42
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
import ast
|
|
17
|
+
import json
|
|
18
|
+
import argparse
|
|
19
|
+
import traceback
|
|
20
|
+
import threading
|
|
21
|
+
import signal
|
|
22
|
+
import textwrap
|
|
23
|
+
from types import FrameType
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ─── Output formatting ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
SEPARATOR = "─" * 60
|
|
30
|
+
|
|
31
|
+
def fmt_value(v: Any, max_len: int = 200) -> str:
|
|
32
|
+
"""Format a value for display, truncating if too long."""
|
|
33
|
+
try:
|
|
34
|
+
if isinstance(v, str):
|
|
35
|
+
r = repr(v)
|
|
36
|
+
elif isinstance(v, (list, tuple, set)):
|
|
37
|
+
r = f"{type(v).__name__}[{len(v)} items]: {repr(v)}"
|
|
38
|
+
elif isinstance(v, dict):
|
|
39
|
+
r = f"dict[{len(v)} keys]: {repr(v)}"
|
|
40
|
+
else:
|
|
41
|
+
r = repr(v)
|
|
42
|
+
if len(r) > max_len:
|
|
43
|
+
return r[:max_len] + " ..."
|
|
44
|
+
return r
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return f"<error formatting value: {e}>"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fmt_frame(frame: FrameType) -> list[str]:
|
|
50
|
+
"""Format a call stack from a frame, filtering out pybreakz internals."""
|
|
51
|
+
_self = os.path.abspath(__file__)
|
|
52
|
+
stack = []
|
|
53
|
+
f = frame
|
|
54
|
+
while f is not None:
|
|
55
|
+
ffile = os.path.abspath(f.f_code.co_filename)
|
|
56
|
+
if ffile != _self: # skip pybreakz' own frames
|
|
57
|
+
fname = os.path.basename(ffile)
|
|
58
|
+
stack.append(f" {f.f_code.co_name}() [{fname}:{f.f_lineno}]")
|
|
59
|
+
f = f.f_back
|
|
60
|
+
stack.reverse()
|
|
61
|
+
return stack
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fmt_locals(local_vars: dict, watch: list[str], eval_exprs: list[str], frame: FrameType) -> list[str]:
|
|
65
|
+
"""Format locals, watched vars, and eval expressions."""
|
|
66
|
+
lines = []
|
|
67
|
+
|
|
68
|
+
# Filter out dunder/internal vars
|
|
69
|
+
visible = {k: v for k, v in local_vars.items() if not k.startswith("__")}
|
|
70
|
+
|
|
71
|
+
if visible:
|
|
72
|
+
lines.append("Locals:")
|
|
73
|
+
for k, v in visible.items():
|
|
74
|
+
lines.append(f" {k:<20} = {fmt_value(v)}")
|
|
75
|
+
else:
|
|
76
|
+
lines.append("Locals: (none)")
|
|
77
|
+
|
|
78
|
+
if watch:
|
|
79
|
+
lines.append("Watched:")
|
|
80
|
+
combined = {**frame.f_globals, **local_vars}
|
|
81
|
+
for expr in watch:
|
|
82
|
+
try:
|
|
83
|
+
result = eval(expr, combined)
|
|
84
|
+
lines.append(f" {expr:<20} = {fmt_value(result)}")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
lines.append(f" {expr:<20} ! {e}")
|
|
87
|
+
|
|
88
|
+
if eval_exprs:
|
|
89
|
+
lines.append("Evaluated:")
|
|
90
|
+
combined = {**frame.f_globals, **local_vars}
|
|
91
|
+
for expr in eval_exprs:
|
|
92
|
+
try:
|
|
93
|
+
result = eval(expr, combined)
|
|
94
|
+
lines.append(f" {expr:<20} = {fmt_value(result)}")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
lines.append(f" {expr:<20} ! {e}")
|
|
97
|
+
|
|
98
|
+
return lines
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ─── Breakpoint parsing ───────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def parse_breakpoints(bp_str: str) -> dict[int, Optional[str]]:
|
|
104
|
+
"""
|
|
105
|
+
Parse breakpoint specs like "10,25,42" or "42:x>10,67:name=='foo'"
|
|
106
|
+
Returns dict of {line: condition_or_None}
|
|
107
|
+
"""
|
|
108
|
+
result = {}
|
|
109
|
+
for part in bp_str.split(","):
|
|
110
|
+
part = part.strip()
|
|
111
|
+
if ":" in part:
|
|
112
|
+
line_s, condition = part.split(":", 1)
|
|
113
|
+
result[int(line_s.strip())] = condition.strip()
|
|
114
|
+
else:
|
|
115
|
+
result[int(part)] = None
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ─── Tracer ──────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
class Debugger:
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
script_path: str,
|
|
125
|
+
breakpoints: dict[int, Optional[str]],
|
|
126
|
+
watch: list[str],
|
|
127
|
+
eval_exprs: list[str],
|
|
128
|
+
on_exception: bool,
|
|
129
|
+
timeout: int,
|
|
130
|
+
output_format: str,
|
|
131
|
+
max_hits: int,
|
|
132
|
+
):
|
|
133
|
+
self.script_path = os.path.abspath(script_path)
|
|
134
|
+
self.breakpoints = breakpoints # {line: condition}
|
|
135
|
+
self.watch = watch
|
|
136
|
+
self.eval_exprs = eval_exprs
|
|
137
|
+
self.on_exception = on_exception
|
|
138
|
+
self.timeout = timeout
|
|
139
|
+
self.output_format = output_format
|
|
140
|
+
self.max_hits = max_hits
|
|
141
|
+
|
|
142
|
+
self.hits: list[dict] = []
|
|
143
|
+
self.hit_count = 0
|
|
144
|
+
self.exception_info: Optional[dict] = None
|
|
145
|
+
self.script_output: list[str] = []
|
|
146
|
+
|
|
147
|
+
def _check_condition(self, condition: str, frame: FrameType) -> bool:
|
|
148
|
+
try:
|
|
149
|
+
combined = {**frame.f_globals, **frame.f_locals}
|
|
150
|
+
return bool(eval(condition, combined))
|
|
151
|
+
except Exception:
|
|
152
|
+
return False # Skip if condition errors
|
|
153
|
+
|
|
154
|
+
def trace_calls(self, frame: FrameType, event: str, arg: Any):
|
|
155
|
+
"""sys.settrace handler."""
|
|
156
|
+
filename = os.path.abspath(frame.f_code.co_filename)
|
|
157
|
+
|
|
158
|
+
if filename != self.script_path:
|
|
159
|
+
return self.trace_calls # Still trace into calls from script
|
|
160
|
+
|
|
161
|
+
if event == "line":
|
|
162
|
+
lineno = frame.f_lineno
|
|
163
|
+
if lineno in self.breakpoints:
|
|
164
|
+
condition = self.breakpoints[lineno]
|
|
165
|
+
if condition is None or self._check_condition(condition, frame):
|
|
166
|
+
if self.hit_count < self.max_hits:
|
|
167
|
+
self._record_hit(frame, lineno)
|
|
168
|
+
self.hit_count += 1
|
|
169
|
+
|
|
170
|
+
return self.trace_calls
|
|
171
|
+
|
|
172
|
+
def _record_hit(self, frame: FrameType, lineno: int):
|
|
173
|
+
stack = fmt_frame(frame)
|
|
174
|
+
local_lines = fmt_locals(
|
|
175
|
+
dict(frame.f_locals), self.watch, self.eval_exprs, frame
|
|
176
|
+
)
|
|
177
|
+
self.hits.append({
|
|
178
|
+
"file": os.path.basename(self.script_path),
|
|
179
|
+
"line": lineno,
|
|
180
|
+
"stack": stack,
|
|
181
|
+
"locals": local_lines,
|
|
182
|
+
"source_line": self._get_source_line(lineno),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
def _get_source_line(self, lineno: int) -> str:
|
|
186
|
+
try:
|
|
187
|
+
with open(self.script_path) as f:
|
|
188
|
+
lines = f.readlines()
|
|
189
|
+
if 1 <= lineno <= len(lines):
|
|
190
|
+
return lines[lineno - 1].rstrip()
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
def _get_context_lines(self, lineno: int, context: int = 3) -> list[tuple[int, str, bool]]:
|
|
196
|
+
"""Get source lines around a line number. Returns (lineno, text, is_target)."""
|
|
197
|
+
try:
|
|
198
|
+
with open(self.script_path) as f:
|
|
199
|
+
lines = f.readlines()
|
|
200
|
+
result = []
|
|
201
|
+
start = max(1, lineno - context)
|
|
202
|
+
end = min(len(lines), lineno + context)
|
|
203
|
+
for i in range(start, end + 1):
|
|
204
|
+
result.append((i, lines[i-1].rstrip(), i == lineno))
|
|
205
|
+
return result
|
|
206
|
+
except Exception:
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
def run(self) -> int:
|
|
210
|
+
"""Run the script and return exit code."""
|
|
211
|
+
# Set up timeout
|
|
212
|
+
if self.timeout > 0:
|
|
213
|
+
def _timeout_handler():
|
|
214
|
+
print(f"\n[pybreakz] TIMEOUT: Script exceeded {self.timeout}s", file=sys.stderr)
|
|
215
|
+
os.kill(os.getpid(), signal.SIGTERM)
|
|
216
|
+
timer = threading.Timer(self.timeout, _timeout_handler)
|
|
217
|
+
timer.daemon = True
|
|
218
|
+
timer.start()
|
|
219
|
+
|
|
220
|
+
# Prepare script environment
|
|
221
|
+
script_globals = {
|
|
222
|
+
"__name__": "__main__",
|
|
223
|
+
"__file__": self.script_path,
|
|
224
|
+
"__builtins__": __builtins__,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
exit_code = 0
|
|
228
|
+
sys.settrace(self.trace_calls)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
with open(self.script_path) as f:
|
|
232
|
+
source = f.read()
|
|
233
|
+
code = compile(source, self.script_path, "exec")
|
|
234
|
+
exec(code, script_globals)
|
|
235
|
+
|
|
236
|
+
except SystemExit as e:
|
|
237
|
+
exit_code = e.code if isinstance(e.code, int) else 0
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
exit_code = 1
|
|
241
|
+
if self.on_exception:
|
|
242
|
+
tb = sys.exc_info()[2]
|
|
243
|
+
# Walk to innermost frame
|
|
244
|
+
while tb.tb_next:
|
|
245
|
+
tb = tb.tb_next
|
|
246
|
+
frame = tb.tb_frame
|
|
247
|
+
# Filter pybreakz frames from traceback text
|
|
248
|
+
raw_tb = traceback.format_exc()
|
|
249
|
+
_self = os.path.abspath(__file__)
|
|
250
|
+
filtered_lines = []
|
|
251
|
+
skip_next = False
|
|
252
|
+
for line in raw_tb.splitlines():
|
|
253
|
+
if _self in line:
|
|
254
|
+
skip_next = True
|
|
255
|
+
continue
|
|
256
|
+
if skip_next and line.startswith(" "):
|
|
257
|
+
skip_next = False
|
|
258
|
+
continue
|
|
259
|
+
skip_next = False
|
|
260
|
+
filtered_lines.append(line)
|
|
261
|
+
self.exception_info = {
|
|
262
|
+
"type": type(e).__name__,
|
|
263
|
+
"message": str(e),
|
|
264
|
+
"file": os.path.basename(frame.f_code.co_filename),
|
|
265
|
+
"line": tb.tb_lineno,
|
|
266
|
+
"traceback": "\n".join(filtered_lines),
|
|
267
|
+
"locals": fmt_locals(dict(frame.f_locals), self.watch, self.eval_exprs, frame),
|
|
268
|
+
"stack": fmt_frame(frame),
|
|
269
|
+
"source_line": self._get_source_line(tb.tb_lineno),
|
|
270
|
+
}
|
|
271
|
+
else:
|
|
272
|
+
# Still print the traceback for the user
|
|
273
|
+
traceback.print_exc()
|
|
274
|
+
|
|
275
|
+
finally:
|
|
276
|
+
sys.settrace(None)
|
|
277
|
+
if self.timeout > 0:
|
|
278
|
+
timer.cancel()
|
|
279
|
+
|
|
280
|
+
return exit_code
|
|
281
|
+
|
|
282
|
+
def print_report(self):
|
|
283
|
+
"""Print the debug report to stdout."""
|
|
284
|
+
if self.output_format == "json":
|
|
285
|
+
self._print_json()
|
|
286
|
+
else:
|
|
287
|
+
self._print_text()
|
|
288
|
+
|
|
289
|
+
def _print_text(self):
|
|
290
|
+
script_name = os.path.basename(self.script_path)
|
|
291
|
+
print(f"\n{'═' * 60}")
|
|
292
|
+
print(f" pybreakz report → {script_name}")
|
|
293
|
+
print(f"{'═' * 60}")
|
|
294
|
+
|
|
295
|
+
if not self.hits and not self.exception_info:
|
|
296
|
+
print("\n No breakpoints hit and no exception caught.")
|
|
297
|
+
print(" (Check your line numbers — they must be executable lines)\n")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
# Breakpoint hits
|
|
301
|
+
for i, hit in enumerate(self.hits, 1):
|
|
302
|
+
cond = self.breakpoints.get(hit["line"])
|
|
303
|
+
cond_str = f" [condition: {cond}]" if cond else ""
|
|
304
|
+
print(f"\n┌─ BREAKPOINT HIT #{i} {hit['file']}:{hit['line']}{cond_str}")
|
|
305
|
+
print(f"│ Source: {hit['source_line']}")
|
|
306
|
+
print("│")
|
|
307
|
+
print("│ Call Stack (innermost last):")
|
|
308
|
+
for s in hit["stack"]:
|
|
309
|
+
print(f"│{s}")
|
|
310
|
+
print("│")
|
|
311
|
+
for line in hit["locals"]:
|
|
312
|
+
print(f"│ {line}")
|
|
313
|
+
print(f"└{SEPARATOR}")
|
|
314
|
+
|
|
315
|
+
# Exception info
|
|
316
|
+
if self.exception_info:
|
|
317
|
+
ei = self.exception_info
|
|
318
|
+
print(f"\n┌─ EXCEPTION {ei['type']}: {ei['message']}")
|
|
319
|
+
print(f"│ Location: {ei['file']}:{ei['line']}")
|
|
320
|
+
print(f"│ Source: {ei['source_line']}")
|
|
321
|
+
print("│")
|
|
322
|
+
print("│ Call Stack:")
|
|
323
|
+
for s in ei["stack"]:
|
|
324
|
+
print(f"│{s}")
|
|
325
|
+
print("│")
|
|
326
|
+
for line in ei["locals"]:
|
|
327
|
+
print(f"│ {line}")
|
|
328
|
+
print("│")
|
|
329
|
+
print("│ Full Traceback:")
|
|
330
|
+
for line in ei["traceback"].splitlines():
|
|
331
|
+
print(f"│ {line}")
|
|
332
|
+
print(f"└{SEPARATOR}")
|
|
333
|
+
|
|
334
|
+
print()
|
|
335
|
+
|
|
336
|
+
def _print_json(self):
|
|
337
|
+
output = {
|
|
338
|
+
"script": self.script_path,
|
|
339
|
+
"breakpoint_hits": self.hits,
|
|
340
|
+
"exception": self.exception_info,
|
|
341
|
+
}
|
|
342
|
+
print(json.dumps(output, indent=2, default=str))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
348
|
+
parser = argparse.ArgumentParser(
|
|
349
|
+
prog="pybreakz",
|
|
350
|
+
description="Debug Python scripts with breakpoints. Designed for Claude Code.",
|
|
351
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
352
|
+
epilog=textwrap.dedent("""
|
|
353
|
+
Examples:
|
|
354
|
+
pybreakz run script.py --breakpoints 42
|
|
355
|
+
pybreakz run script.py --breakpoints 10,25,42 --watch "user_id,result"
|
|
356
|
+
pybreakz run script.py --breakpoints "42:x>0,67:name=='foo'"
|
|
357
|
+
pybreakz run script.py --on-exception --watch "self,request"
|
|
358
|
+
pybreakz run script.py --breakpoints 10 --eval "len(items),type(r)"
|
|
359
|
+
pybreakz run script.py --breakpoints 42 --format json
|
|
360
|
+
pybreakz run script.py --breakpoints 10 -- --my-script-arg value
|
|
361
|
+
"""),
|
|
362
|
+
)
|
|
363
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
364
|
+
|
|
365
|
+
run_p = subparsers.add_parser("run", help="Run a script with debug tracing")
|
|
366
|
+
run_p.add_argument("script", help="Python script to run")
|
|
367
|
+
run_p.add_argument(
|
|
368
|
+
"--breakpoints", "-b",
|
|
369
|
+
help='Line numbers to break at. e.g. "10,25" or "42:x>0,67:done==True"',
|
|
370
|
+
)
|
|
371
|
+
run_p.add_argument(
|
|
372
|
+
"--watch", "-w",
|
|
373
|
+
help='Comma-separated expressions to evaluate at each breakpoint. e.g. "x,len(items)"',
|
|
374
|
+
)
|
|
375
|
+
run_p.add_argument(
|
|
376
|
+
"--eval", "-e", dest="eval_exprs",
|
|
377
|
+
help='Extra expressions to evaluate at breakpoints. e.g. "df.shape,type(result)"',
|
|
378
|
+
)
|
|
379
|
+
run_p.add_argument(
|
|
380
|
+
"--on-exception", "-x",
|
|
381
|
+
action="store_true",
|
|
382
|
+
help="Capture locals and stack on unhandled exception",
|
|
383
|
+
)
|
|
384
|
+
run_p.add_argument(
|
|
385
|
+
"--timeout", "-t",
|
|
386
|
+
type=int, default=30,
|
|
387
|
+
help="Timeout in seconds (default: 30, 0 = no timeout)",
|
|
388
|
+
)
|
|
389
|
+
run_p.add_argument(
|
|
390
|
+
"--format", "-f", dest="output_format",
|
|
391
|
+
choices=["text", "json"], default="text",
|
|
392
|
+
help="Output format (default: text)",
|
|
393
|
+
)
|
|
394
|
+
run_p.add_argument(
|
|
395
|
+
"--max-hits",
|
|
396
|
+
type=int, default=20,
|
|
397
|
+
help="Max breakpoint hits to record (default: 20)",
|
|
398
|
+
)
|
|
399
|
+
return parser, run_p
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def main():
|
|
403
|
+
parser, run_p = build_parser()
|
|
404
|
+
# We use parse_known_args so that script passthrough args (after --) don't
|
|
405
|
+
# interfere with pybreakz' own flags.
|
|
406
|
+
args, extra = parser.parse_known_args()
|
|
407
|
+
|
|
408
|
+
if args.command is None:
|
|
409
|
+
parser.print_help()
|
|
410
|
+
sys.exit(0)
|
|
411
|
+
|
|
412
|
+
if args.command == "run":
|
|
413
|
+
# Validate script exists
|
|
414
|
+
if not os.path.exists(args.script):
|
|
415
|
+
print(f"[pybreakz] Error: Script not found: {args.script}", file=sys.stderr)
|
|
416
|
+
sys.exit(1)
|
|
417
|
+
|
|
418
|
+
# Parse breakpoints
|
|
419
|
+
breakpoints = {}
|
|
420
|
+
if args.breakpoints:
|
|
421
|
+
try:
|
|
422
|
+
breakpoints = parse_breakpoints(args.breakpoints)
|
|
423
|
+
except ValueError as e:
|
|
424
|
+
print(f"[pybreakz] Error parsing breakpoints: {e}", file=sys.stderr)
|
|
425
|
+
sys.exit(1)
|
|
426
|
+
|
|
427
|
+
if not breakpoints and not args.on_exception:
|
|
428
|
+
print("[pybreakz] Warning: No breakpoints set and --on-exception not used.", file=sys.stderr)
|
|
429
|
+
print("[pybreakz] Use --breakpoints LINE or --on-exception", file=sys.stderr)
|
|
430
|
+
|
|
431
|
+
# Parse watch/eval
|
|
432
|
+
watch = [w.strip() for w in args.watch.split(",")] if args.watch else []
|
|
433
|
+
eval_exprs = [e.strip() for e in args.eval_exprs.split(",")] if args.eval_exprs else []
|
|
434
|
+
|
|
435
|
+
# Inject script args into sys.argv (extra = anything after --)
|
|
436
|
+
script_args = extra
|
|
437
|
+
if script_args and script_args[0] == "--":
|
|
438
|
+
script_args = script_args[1:]
|
|
439
|
+
sys.argv = [args.script] + script_args
|
|
440
|
+
|
|
441
|
+
# Also add script dir to path so local imports work
|
|
442
|
+
script_dir = os.path.dirname(os.path.abspath(args.script))
|
|
443
|
+
if script_dir not in sys.path:
|
|
444
|
+
sys.path.insert(0, script_dir)
|
|
445
|
+
|
|
446
|
+
debugger = Debugger(
|
|
447
|
+
script_path=args.script,
|
|
448
|
+
breakpoints=breakpoints,
|
|
449
|
+
watch=watch,
|
|
450
|
+
eval_exprs=eval_exprs,
|
|
451
|
+
on_exception=args.on_exception,
|
|
452
|
+
timeout=args.timeout,
|
|
453
|
+
output_format=args.output_format,
|
|
454
|
+
max_hits=args.max_hits,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
exit_code = debugger.run()
|
|
458
|
+
debugger.print_report()
|
|
459
|
+
sys.exit(exit_code)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
if __name__ == "__main__":
|
|
463
|
+
main()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pybreakz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A zero-dependency Python debugger CLI built for Claude Code
|
|
5
|
+
Author: Allan Napier
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/allannapier/py_debug_cli
|
|
8
|
+
Project-URL: Repository, https://github.com/allannapier/py_debug_cli
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# pybreakz
|
|
24
|
+
|
|
25
|
+
A zero-dependency Python debugger CLI built for Claude Code.
|
|
26
|
+
|
|
27
|
+
Run your script with breakpoints, capture locals and stack state, get clean structured output — all in one shot. No interactive session required.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install pybreakz
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or install from source:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/allannapier/py_debug_cli.git
|
|
41
|
+
cd py_debug_cli
|
|
42
|
+
pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.9+. No pip dependencies.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Basic breakpoints
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pybreakz run script.py --breakpoints 42
|
|
55
|
+
pybreakz run script.py --breakpoints 10,25,42
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Captures all locals at each breakpoint hit.
|
|
59
|
+
|
|
60
|
+
### Watch specific expressions
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pybreakz run script.py --breakpoints 42 --watch "user_id,response.status,len(items)"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Evaluates each expression in the breakpoint's local scope.
|
|
67
|
+
|
|
68
|
+
### Evaluate arbitrary expressions
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pybreakz run script.py --breakpoints 42 --eval "df.shape,type(result),items[:3]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Like `--watch` but separate section in output — useful for computed values.
|
|
75
|
+
|
|
76
|
+
### Capture on exception
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pybreakz run script.py --on-exception
|
|
80
|
+
pybreakz run script.py --on-exception --watch "self,request,data"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Captures locals, stack, and full traceback at the crash site.
|
|
84
|
+
|
|
85
|
+
### Conditional breakpoints
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pybreakz run script.py --breakpoints "42:x>100,67:status=='error'"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Only fires when the condition evaluates to True.
|
|
92
|
+
|
|
93
|
+
### Pass arguments to your script
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pybreakz run script.py --breakpoints 10 -- --input data.csv --verbose
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Anything after `--` is forwarded to the script as `sys.argv`.
|
|
100
|
+
|
|
101
|
+
### JSON output (for programmatic use)
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pybreakz run script.py --breakpoints 42 --format json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Timeout
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pybreakz run script.py --breakpoints 10 --timeout 60 # 60s limit
|
|
111
|
+
pybreakz run script.py --breakpoints 10 --timeout 0 # no limit
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Default timeout is 30 seconds.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Example output
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
════════════════════════════════════════════════════════════
|
|
122
|
+
pybreakz report → script.py
|
|
123
|
+
════════════════════════════════════════════════════════════
|
|
124
|
+
|
|
125
|
+
┌─ BREAKPOINT HIT #1 script.py:42
|
|
126
|
+
│ Source: result = process(item)
|
|
127
|
+
│
|
|
128
|
+
│ Call Stack (innermost last):
|
|
129
|
+
│ <module>() [script.py:80]
|
|
130
|
+
│ main() [script.py:60]
|
|
131
|
+
│ run_batch() [script.py:42]
|
|
132
|
+
│
|
|
133
|
+
│ Locals:
|
|
134
|
+
│ items = list[150 items]: [{'id': 1, ...}, ...]
|
|
135
|
+
│ item = {'id': 1, 'name': 'foo', 'active': True}
|
|
136
|
+
│ result = None
|
|
137
|
+
│ Watched:
|
|
138
|
+
│ item['id'] = 1
|
|
139
|
+
│ len(items) = 150
|
|
140
|
+
└────────────────────────────────────────────────────────────
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## How Claude Code uses it
|
|
146
|
+
|
|
147
|
+
Claude Code treats `pybreakz` like any other shell command. A typical debugging loop:
|
|
148
|
+
|
|
149
|
+
1. Claude reads your code and identifies suspicious lines
|
|
150
|
+
2. Calls `pybreakz run script.py --breakpoints 34,67 --watch "x,response"`
|
|
151
|
+
3. Reads the report output
|
|
152
|
+
4. Adjusts breakpoints or patches the bug
|
|
153
|
+
5. Repeats if needed
|
|
154
|
+
|
|
155
|
+
The `--on-exception` flag is especially useful as a first pass — just run it and let the crash tell Claude where to look.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Options reference
|
|
160
|
+
|
|
161
|
+
| Flag | Short | Description |
|
|
162
|
+
|------|-------|-------------|
|
|
163
|
+
| `--breakpoints LINES` | `-b` | Comma-separated line numbers, optionally with conditions (`42:x>0`) |
|
|
164
|
+
| `--watch EXPRS` | `-w` | Comma-separated expressions to evaluate at each hit |
|
|
165
|
+
| `--eval EXPRS` | `-e` | Additional expressions to evaluate (shown separately) |
|
|
166
|
+
| `--on-exception` | `-x` | Capture state on unhandled exception |
|
|
167
|
+
| `--timeout SECS` | `-t` | Script timeout in seconds (default: 30) |
|
|
168
|
+
| `--format FORMAT` | `-f` | Output format: `text` (default) or `json` |
|
|
169
|
+
| `--max-hits N` | | Max breakpoint hits to record (default: 20) |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Limitations
|
|
174
|
+
|
|
175
|
+
- Works on `.py` scripts run directly. Not designed for long-running servers or async event loops.
|
|
176
|
+
- Breakpoints must be on **executable lines** (not blank lines, comments, or `def`/`class` headers — use the first line inside the function body).
|
|
177
|
+
- `sys.settrace` has a small performance overhead — not suitable for tight performance benchmarks.
|
|
178
|
+
- Multi-threaded scripts: only the main thread is traced.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pybreakz/__init__.py
|
|
5
|
+
src/pybreakz/cli.py
|
|
6
|
+
src/pybreakz.egg-info/PKG-INFO
|
|
7
|
+
src/pybreakz.egg-info/SOURCES.txt
|
|
8
|
+
src/pybreakz.egg-info/dependency_links.txt
|
|
9
|
+
src/pybreakz.egg-info/entry_points.txt
|
|
10
|
+
src/pybreakz.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pybreakz
|