Skip to content

Commit db6b7a4

Browse files
authored
Supoprt 3.7+ breakpoint() call (#63)
1 parent e9cec1f commit db6b7a4

6 files changed

Lines changed: 158 additions & 127 deletions

File tree

docs/entry-exit.rst

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,34 @@ program is called, sometimes the differences matter. Also the debugger
156156
adds overhead and slows down your program.
157157

158158
Another possibility then is to add statements into your program to call
159+
the debugger at the spot in the program you want.
160+
161+
Python 3.7 and later
162+
--------------------
163+
164+
In Python 3.7 and later, a ``breakpoint()`` builtin was added. Add a ``breakpoint()`` function call to your python code, then then set set environment variable ``PYTHONBREAKPOINT`` to ``trepan.api.debug`` before running the program.
165+
166+
For example, here is some Python code:
167+
168+
.. code:: python
169+
170+
# Code run here trepan3k/trepan3k doesn't even see at all.
171+
# work, work, work...
172+
debugger() # Get thee to thyne debugger!
173+
174+
175+
Now run Python with ``PYTHONBREAKPOINT`` set to ``trepan.api``:
176+
177+
.. code:: shell
178+
179+
PYTHONBREAKPOINT=trepan.api.debug_for_remote_access python test/example/gcd-breakpoint.py 3 5
180+
181+
Before Python 3.7
182+
-----------------
183+
184+
Before Python 3.7 you still add statements into your program to call
159185
the debugger at the spot in the program you want. To do this,
160-
``import trepan.api`` and make a call to *trepan.api.debug()*. For
186+
``import trepan.api`` and make a call to ``trepan.api.debug()``. For
161187
example:
162188

163189
.. code:: python
@@ -188,11 +214,10 @@ inside the *debug()* call:
188214
foo() # Note there's no statement following foo()
189215
190216
If you want a startup profile to get run, you can pass a list of file
191-
names in option `start_opts`. For example, let's say I want to set the
217+
names in option ``start_opts``. For example, let's say I want to set the
192218
formatting style and automatic source code listing in my debugger
193219
session I would put the trepan debugger commands in a file, say
194-
`/home/rocky/trepan-startup` and then list that file like this:
195-
220+
``/home/rocky/trepan-startup`` and then list that file like this:
196221

197222
.. code:: python
198223
@@ -201,38 +226,19 @@ session I would put the trepan debugger commands in a file, say
201226
Calling the debugger from pytest
202227
================================
203228

204-
Install `pytest-trepan <https://pypi.python.org/pypi/pytest-trepan>`_::
205-
206-
pip install pytest-trepan
229+
The only thing needed here is to ensure you add the ``-s`` option and add an explicit
230+
``breakpoint()`` or ``debug()`` function call.
207231

208-
After installing, to set a breakpoint to enter the trepan debugger::
232+
To set a breakpoint to enter the trepan debugger::
209233

210234
import pytest
235+
from trepan.api import debug
211236
def test_function():
212237
...
213-
pytest.trepan() # get thee to thyne debugger!
238+
debug() # get thee to thyne debugger!
214239
x = 1
215240
...
216241

217-
The above will look like it is stopped at the *pytest.trepan()*
218-
call. This is most useful when this is the last statement of a
219-
scope. If you want to stop instead before ``x = 1`` pass ``immediate=False`` or just ``False``::
220-
221-
import pytest
222-
def test_function():
223-
...
224-
pytest.trepan(immediate=False)
225-
# same as py.trepan(False)
226-
x = 1
227-
...
228-
229-
You can also pass as keyword arguments any parameter accepted by *trepan.api.debug()*.
230-
231-
To have the debugger entered on error, use the ``--trepan`` option::
232-
233-
$ py.test --trepan ...
234-
235-
236242

237243
Set up an exception handler to enter the debugger on a signal
238244
=============================================================

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies = [
1717
"pyficache >= 2.3.0",
1818
"xdis >= 6.1.1,<6.2.0",
1919
"pygments >= 2.2.0",
20-
"spark_parser >= 1.8.9, <1.9.1",
20+
"spark_parser >= 1.8.9, <1.9.2",
2121
"tracer > 1.9.0",
2222
"term-background >= 1.0.1",
2323
]
@@ -46,6 +46,7 @@ classifiers = [
4646
"Programming Language :: Python :: 3.10",
4747
"Programming Language :: Python :: 3.11",
4848
"Programming Language :: Python :: 3.12",
49+
"Programming Language :: Python :: 3.13",
4950
"Programming Language :: Python :: Implementation :: PyPy",
5051
]
5152
dynamic = ["version"]

trepan/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
# The name of the debugger we are currently going by.
4040
__title__ = package
4141

42-
42+
os.environ["PYTHONBREAKPOINT"] = "trepan.api.debug"
4343
def main(dbg=None, sys_argv=list(sys.argv)):
4444
"""Routine which gets run if we were invoked directly"""
4545

@@ -247,8 +247,10 @@ def write_wrapper(*args, **kwargs):
247247
pass
248248

249249
dbg.core.execution_status = "Terminated"
250+
dbg.core.processor.event = "finished"
250251
dbg.intf[-1].msg("The program finished - quit or restart")
251252
dbg.core.processor.process_commands()
253+
252254
except DebuggerQuit:
253255
break
254256
except DebuggerRestart:

trepan/api.py

Lines changed: 105 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -34,101 +34,15 @@
3434
# functions below. It also doesn't work once we add the exception handling
3535
# we see below. So for now, we'll live with the code duplication.
3636

37+
import os
3738
import sys
38-
from typing import Callable, Optional
39+
from typing import Callable, Literal, Optional
3940

4041
from trepan.debugger import Trepan, debugger_obj
42+
from trepan.interfaces.server import ServerInterface
4143
from trepan.post_mortem import post_mortem_excepthook, uncaught_exception
4244

43-
44-
def debugger_on_post_mortem():
45-
"""Call debugger on an exception that terminates a program"""
46-
sys.excepthook = post_mortem_excepthook
47-
return
48-
49-
50-
def run_eval(
51-
expression,
52-
debug_opts: Optional[dict] = None,
53-
start_opts: Optional[dict] = None,
54-
globals_: Optional[dict] = None,
55-
locals_: Optional[dict] = None,
56-
tb_fn: Optional[Callable] = None,
57-
):
58-
"""Evaluate the expression (given as a string) under debugger
59-
control starting with the statement after the place that
60-
this appears in your program.
61-
62-
This is a wrapper to Debugger.run_eval(), so see that.
63-
64-
When run_eval() returns, it returns the value of the expression.
65-
Otherwise, this function is similar to run().
66-
"""
67-
68-
dbg = Trepan(opts=debug_opts)
69-
try:
70-
return dbg.run_eval(
71-
expression, start_opts=start_opts, globals_=globals_, locals_=locals_
72-
)
73-
except Exception:
74-
dbg.core.trace_hook_suspend = True
75-
if start_opts and "tb_fn" in start_opts:
76-
tb_fn = start_opts["tb_fn"]
77-
uncaught_exception(dbg, tb_fn)
78-
finally:
79-
dbg.core.trace_hook_suspend = False
80-
return
81-
82-
83-
def run_call(
84-
func: Callable,
85-
*args,
86-
debug_opts: Optional[dict] = None,
87-
start_opts: Optional[dict] = None,
88-
**kwds,
89-
):
90-
"""Call the function (a function or method object, not a string)
91-
with the given arguments starting with the statement after
92-
the place that this appears in your program.
93-
94-
When run_call() returns, it returns whatever the function call
95-
returned. The debugger prompt appears as soon as the function is
96-
entered."""
97-
98-
dbg = Trepan(opts=debug_opts)
99-
try:
100-
return dbg.run_call(func, *args, **kwds)
101-
except Exception:
102-
uncaught_exception(dbg)
103-
pass
104-
return
105-
106-
107-
def run_exec(statement, debug_opts=None, start_opts=None, globals_=None, locals_=None):
108-
"""Execute the statement (given as a string) under debugger
109-
control starting with the statement subsequent to the place that
110-
this run_call appears in your program.
111-
112-
This is a wrapper to Debugger.run_exec(), so see that.
113-
114-
The debugger prompt appears before any code is executed;
115-
you can set breakpoints and type 'continue', or you can step
116-
through the statement using 'step' or 'next'
117-
118-
The optional globals_ and locals_ arguments specify the environment
119-
in which the code is executed; by default the dictionary of the
120-
module __main__ is used."""
121-
122-
dbg = Trepan(opts=debug_opts)
123-
try:
124-
return dbg.run_exec(
125-
statement, start_opts=start_opts, globals_=globals_, locals_=locals_
126-
)
127-
except Exception:
128-
uncaught_exception(dbg)
129-
pass
130-
return
131-
45+
DEFAULT_DEBUG_PORT: Literal = 1955
13246

13347
def debug(
13448
dbg_opts={},
@@ -276,6 +190,107 @@ def debug(
276190
return
277191

278192

193+
def debug_for_remote_access():
194+
"""Enter the debugger in a mode that allows connection to it
195+
outside of the process being debugged.
196+
"""
197+
connection_opts = {'IO': 'TCP', 'PORT': os.getenv('TREPAN3K_TCP_PORT', DEFAULT_DEBUG_PORT)}
198+
intf = ServerInterface(connection_opts=connection_opts)
199+
dbg_opts = {'interface': intf}
200+
print(f'Starting {connection_opts["IO"]} server listening on {connection_opts["PORT"]}.', file=sys.stderr)
201+
print(f'Use `python3 -m trepan.client --port {connection_opts["PORT"]}` to enter debugger.', file=sys.stderr)
202+
debug(dbg_opts=dbg_opts, step_ignore=0, level=1)
203+
204+
205+
def debugger_on_post_mortem():
206+
"""Call debugger on an exception that terminates a program"""
207+
sys.excepthook = post_mortem_excepthook
208+
return
209+
210+
211+
def run_eval(
212+
expression,
213+
debug_opts: Optional[dict] = None,
214+
start_opts: Optional[dict] = None,
215+
globals_: Optional[dict] = None,
216+
locals_: Optional[dict] = None,
217+
tb_fn: Optional[Callable] = None,
218+
):
219+
"""Evaluate the expression (given as a string) under debugger
220+
control starting with the statement after the place that
221+
this appears in your program.
222+
223+
This is a wrapper to Debugger.run_eval(), so see that.
224+
225+
When run_eval() returns, it returns the value of the expression.
226+
Otherwise, this function is similar to run().
227+
"""
228+
229+
dbg = Trepan(opts=debug_opts)
230+
try:
231+
return dbg.run_eval(
232+
expression, start_opts=start_opts, globals_=globals_, locals_=locals_
233+
)
234+
except Exception:
235+
dbg.core.trace_hook_suspend = True
236+
if start_opts and "tb_fn" in start_opts:
237+
tb_fn = start_opts["tb_fn"]
238+
uncaught_exception(dbg, tb_fn)
239+
finally:
240+
dbg.core.trace_hook_suspend = False
241+
return
242+
243+
244+
def run_call(
245+
func: Callable,
246+
*args,
247+
debug_opts: Optional[dict] = None,
248+
start_opts: Optional[dict] = None,
249+
**kwds,
250+
):
251+
"""Call the function (a function or method object, not a string)
252+
with the given arguments starting with the statement after
253+
the place that this appears in your program.
254+
255+
When run_call() returns, it returns whatever the function call
256+
returned. The debugger prompt appears as soon as the function is
257+
entered."""
258+
259+
dbg = Trepan(opts=debug_opts)
260+
try:
261+
return dbg.run_call(func, *args, **kwds)
262+
except Exception:
263+
uncaught_exception(dbg)
264+
pass
265+
return
266+
267+
268+
def run_exec(statement, debug_opts=None, start_opts=None, globals_=None, locals_=None):
269+
"""Execute the statement (given as a string) under debugger
270+
control starting with the statement subsequent to the place that
271+
this run_call appears in your program.
272+
273+
This is a wrapper to Debugger.run_exec(), so see that.
274+
275+
The debugger prompt appears before any code is executed;
276+
you can set breakpoints and type 'continue', or you can step
277+
through the statement using 'step' or 'next'
278+
279+
The optional globals_ and locals_ arguments specify the environment
280+
in which the code is executed; by default the dictionary of the
281+
module __main__ is used."""
282+
283+
dbg = Trepan(opts=debug_opts)
284+
try:
285+
return dbg.run_exec(
286+
statement, start_opts=start_opts, globals_=globals_, locals_=locals_
287+
)
288+
except Exception:
289+
uncaught_exception(dbg)
290+
pass
291+
return
292+
293+
279294
def stop(opts=None):
280295
if isinstance(debugger_obj, Trepan):
281296
return debugger_obj.stop(opts)

trepan/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
33
#
4-
# Copyright (C) 2009, 2013-2017, 2021, 2023 Rocky Bernstein
4+
# Copyright (C) 2009, 2013-2017, 2021, 2023-2024 Rocky Bernstein
55
#
66
# This program is free software; you can redistribute it and/or modify
77
# it under the terms of the GNU General Public License as published by
@@ -23,10 +23,10 @@
2323
from optparse import OptionParser
2424

2525
# Our local modules
26+
from trepan.api import DEFAULT_DEBUG_PORT
2627
from trepan.interfaces import client as Mclient, comcodes as Mcomcodes
2728
from trepan.version import __version__
2829

29-
3030
def process_options(pkg_version, sys_argv, option_list=None):
3131
"""Handle debugger options. Set `option_list' if you are writing
3232
another main program and want to extend the existing set of debugger
@@ -60,7 +60,7 @@ def process_options(pkg_version, sys_argv, option_list=None):
6060
"-P",
6161
"--port",
6262
dest="port",
63-
default=1027,
63+
default=DEFAULT_DEBUG_PORT,
6464
action="store",
6565
type="int",
6666
metavar="NUMBER",
@@ -92,7 +92,7 @@ def process_options(pkg_version, sys_argv, option_list=None):
9292
"open": True,
9393
"IO": "TCP",
9494
"HOST": "127.0.0.1",
95-
"PORT": 1027,
95+
"PORT": DEFAULT_DEBUG_PORT,
9696
}
9797

9898

0 commit comments

Comments
 (0)