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 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.
@@ -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,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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """pybreakz - A CLI debugger for Claude Code."""
2
+
3
+ __version__ = "0.1.0"
@@ -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,2 @@
1
+ [console_scripts]
2
+ pybreakz = pybreakz.cli:main
@@ -0,0 +1 @@
1
+ pybreakz