From e329e84e7b96d258495a7a5db3404086e6a1e0dd Mon Sep 17 00:00:00 2001 From: rocky Date: Sat, 9 Nov 2024 15:18:01 -0500 Subject: [PATCH] Supoprt 3.7+ breakpoint() call --- docs/entry-exit.rst | 62 ++++++------ pyproject.toml | 3 +- trepan/__main__.py | 4 +- trepan/api.py | 195 +++++++++++++++++++----------------- trepan/client.py | 8 +- trepan/processor/cmdproc.py | 13 ++- 6 files changed, 158 insertions(+), 127 deletions(-) diff --git a/docs/entry-exit.rst b/docs/entry-exit.rst index 56d9ddd6..2f563407 100644 --- a/docs/entry-exit.rst +++ b/docs/entry-exit.rst @@ -156,8 +156,34 @@ program is called, sometimes the differences matter. Also the debugger adds overhead and slows down your program. Another possibility then is to add statements into your program to call +the debugger at the spot in the program you want. + +Python 3.7 and later +-------------------- + +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. + +For example, here is some Python code: + +.. code:: python + + # Code run here trepan3k/trepan3k doesn't even see at all. + # work, work, work... + debugger() # Get thee to thyne debugger! + + +Now run Python with ``PYTHONBREAKPOINT`` set to ``trepan.api``: + +.. code:: shell + + PYTHONBREAKPOINT=trepan.api.debug_for_remote_access python test/example/gcd-breakpoint.py 3 5 + +Before Python 3.7 +----------------- + +Before Python 3.7 you still add statements into your program to call the debugger at the spot in the program you want. To do this, -``import trepan.api`` and make a call to *trepan.api.debug()*. For +``import trepan.api`` and make a call to ``trepan.api.debug()``. For example: .. code:: python @@ -188,11 +214,10 @@ inside the *debug()* call: foo() # Note there's no statement following foo() If you want a startup profile to get run, you can pass a list of file -names in option `start_opts`. For example, let's say I want to set the +names in option ``start_opts``. For example, let's say I want to set the formatting style and automatic source code listing in my debugger session I would put the trepan debugger commands in a file, say -`/home/rocky/trepan-startup` and then list that file like this: - +``/home/rocky/trepan-startup`` and then list that file like this: .. code:: python @@ -201,38 +226,19 @@ session I would put the trepan debugger commands in a file, say Calling the debugger from pytest ================================ -Install `pytest-trepan `_:: - - pip install pytest-trepan +The only thing needed here is to ensure you add the ``-s`` option and add an explicit +``breakpoint()`` or ``debug()`` function call. -After installing, to set a breakpoint to enter the trepan debugger:: +To set a breakpoint to enter the trepan debugger:: import pytest + from trepan.api import debug def test_function(): ... - pytest.trepan() # get thee to thyne debugger! + debug() # get thee to thyne debugger! x = 1 ... -The above will look like it is stopped at the *pytest.trepan()* -call. This is most useful when this is the last statement of a -scope. If you want to stop instead before ``x = 1`` pass ``immediate=False`` or just ``False``:: - - import pytest - def test_function(): - ... - pytest.trepan(immediate=False) - # same as py.trepan(False) - x = 1 - ... - -You can also pass as keyword arguments any parameter accepted by *trepan.api.debug()*. - -To have the debugger entered on error, use the ``--trepan`` option:: - - $ py.test --trepan ... - - Set up an exception handler to enter the debugger on a signal ============================================================= diff --git a/pyproject.toml b/pyproject.toml index 55821a2b..9f0fb89c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "pyficache >= 2.3.0", "xdis >= 6.1.1,<6.2.0", "pygments >= 2.2.0", - "spark_parser >= 1.8.9, <1.9.1", + "spark_parser >= 1.8.9, <1.9.2", "tracer > 1.9.0", "term-background >= 1.0.1", ] @@ -46,6 +46,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version"] diff --git a/trepan/__main__.py b/trepan/__main__.py index 4c14f02b..bc43ff15 100755 --- a/trepan/__main__.py +++ b/trepan/__main__.py @@ -39,7 +39,7 @@ # The name of the debugger we are currently going by. __title__ = package - +os.environ["PYTHONBREAKPOINT"] = "trepan.api.debug" def main(dbg=None, sys_argv=list(sys.argv)): """Routine which gets run if we were invoked directly""" @@ -247,8 +247,10 @@ def write_wrapper(*args, **kwargs): pass dbg.core.execution_status = "Terminated" + dbg.core.processor.event = "finished" dbg.intf[-1].msg("The program finished - quit or restart") dbg.core.processor.process_commands() + except DebuggerQuit: break except DebuggerRestart: diff --git a/trepan/api.py b/trepan/api.py index f1fd17b8..29616952 100644 --- a/trepan/api.py +++ b/trepan/api.py @@ -34,101 +34,15 @@ # functions below. It also doesn't work once we add the exception handling # we see below. So for now, we'll live with the code duplication. +import os import sys -from typing import Callable, Optional +from typing import Callable, Literal, Optional from trepan.debugger import Trepan, debugger_obj +from trepan.interfaces.server import ServerInterface from trepan.post_mortem import post_mortem_excepthook, uncaught_exception - -def debugger_on_post_mortem(): - """Call debugger on an exception that terminates a program""" - sys.excepthook = post_mortem_excepthook - return - - -def run_eval( - expression, - debug_opts: Optional[dict] = None, - start_opts: Optional[dict] = None, - globals_: Optional[dict] = None, - locals_: Optional[dict] = None, - tb_fn: Optional[Callable] = None, -): - """Evaluate the expression (given as a string) under debugger - control starting with the statement after the place that - this appears in your program. - - This is a wrapper to Debugger.run_eval(), so see that. - - When run_eval() returns, it returns the value of the expression. - Otherwise, this function is similar to run(). - """ - - dbg = Trepan(opts=debug_opts) - try: - return dbg.run_eval( - expression, start_opts=start_opts, globals_=globals_, locals_=locals_ - ) - except Exception: - dbg.core.trace_hook_suspend = True - if start_opts and "tb_fn" in start_opts: - tb_fn = start_opts["tb_fn"] - uncaught_exception(dbg, tb_fn) - finally: - dbg.core.trace_hook_suspend = False - return - - -def run_call( - func: Callable, - *args, - debug_opts: Optional[dict] = None, - start_opts: Optional[dict] = None, - **kwds, -): - """Call the function (a function or method object, not a string) - with the given arguments starting with the statement after - the place that this appears in your program. - - When run_call() returns, it returns whatever the function call - returned. The debugger prompt appears as soon as the function is - entered.""" - - dbg = Trepan(opts=debug_opts) - try: - return dbg.run_call(func, *args, **kwds) - except Exception: - uncaught_exception(dbg) - pass - return - - -def run_exec(statement, debug_opts=None, start_opts=None, globals_=None, locals_=None): - """Execute the statement (given as a string) under debugger - control starting with the statement subsequent to the place that - this run_call appears in your program. - - This is a wrapper to Debugger.run_exec(), so see that. - - The debugger prompt appears before any code is executed; - you can set breakpoints and type 'continue', or you can step - through the statement using 'step' or 'next' - - The optional globals_ and locals_ arguments specify the environment - in which the code is executed; by default the dictionary of the - module __main__ is used.""" - - dbg = Trepan(opts=debug_opts) - try: - return dbg.run_exec( - statement, start_opts=start_opts, globals_=globals_, locals_=locals_ - ) - except Exception: - uncaught_exception(dbg) - pass - return - +DEFAULT_DEBUG_PORT: Literal = 1955 def debug( dbg_opts=None, @@ -276,6 +190,107 @@ def debug( return +def debug_for_remote_access(): + """Enter the debugger in a mode that allows connection to it + outside of the process being debugged. + """ + connection_opts = {'IO': 'TCP', 'PORT': os.getenv('TREPAN3K_TCP_PORT', DEFAULT_DEBUG_PORT)} + intf = ServerInterface(connection_opts=connection_opts) + dbg_opts = {'interface': intf} + print(f'Starting {connection_opts["IO"]} server listening on {connection_opts["PORT"]}.', file=sys.stderr) + print(f'Use `python3 -m trepan.client --port {connection_opts["PORT"]}` to enter debugger.', file=sys.stderr) + debug(dbg_opts=dbg_opts, step_ignore=0, level=1) + + +def debugger_on_post_mortem(): + """Call debugger on an exception that terminates a program""" + sys.excepthook = post_mortem_excepthook + return + + +def run_eval( + expression, + debug_opts: Optional[dict] = None, + start_opts: Optional[dict] = None, + globals_: Optional[dict] = None, + locals_: Optional[dict] = None, + tb_fn: Optional[Callable] = None, +): + """Evaluate the expression (given as a string) under debugger + control starting with the statement after the place that + this appears in your program. + + This is a wrapper to Debugger.run_eval(), so see that. + + When run_eval() returns, it returns the value of the expression. + Otherwise, this function is similar to run(). + """ + + dbg = Trepan(opts=debug_opts) + try: + return dbg.run_eval( + expression, start_opts=start_opts, globals_=globals_, locals_=locals_ + ) + except Exception: + dbg.core.trace_hook_suspend = True + if start_opts and "tb_fn" in start_opts: + tb_fn = start_opts["tb_fn"] + uncaught_exception(dbg, tb_fn) + finally: + dbg.core.trace_hook_suspend = False + return + + +def run_call( + func: Callable, + *args, + debug_opts: Optional[dict] = None, + start_opts: Optional[dict] = None, + **kwds, +): + """Call the function (a function or method object, not a string) + with the given arguments starting with the statement after + the place that this appears in your program. + + When run_call() returns, it returns whatever the function call + returned. The debugger prompt appears as soon as the function is + entered.""" + + dbg = Trepan(opts=debug_opts) + try: + return dbg.run_call(func, *args, **kwds) + except Exception: + uncaught_exception(dbg) + pass + return + + +def run_exec(statement, debug_opts=None, start_opts=None, globals_=None, locals_=None): + """Execute the statement (given as a string) under debugger + control starting with the statement subsequent to the place that + this run_call appears in your program. + + This is a wrapper to Debugger.run_exec(), so see that. + + The debugger prompt appears before any code is executed; + you can set breakpoints and type 'continue', or you can step + through the statement using 'step' or 'next' + + The optional globals_ and locals_ arguments specify the environment + in which the code is executed; by default the dictionary of the + module __main__ is used.""" + + dbg = Trepan(opts=debug_opts) + try: + return dbg.run_exec( + statement, start_opts=start_opts, globals_=globals_, locals_=locals_ + ) + except Exception: + uncaught_exception(dbg) + pass + return + + def stop(opts=None): if isinstance(debugger_obj, Trepan): return debugger_obj.stop(opts) diff --git a/trepan/client.py b/trepan/client.py index 40ccc0ce..8087c4b5 100755 --- a/trepan/client.py +++ b/trepan/client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# Copyright (C) 2009, 2013-2017, 2021, 2023 Rocky Bernstein +# Copyright (C) 2009, 2013-2017, 2021, 2023-2024 Rocky Bernstein # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,10 +23,10 @@ from optparse import OptionParser # Our local modules +from trepan.api import DEFAULT_DEBUG_PORT from trepan.interfaces import client as Mclient, comcodes as Mcomcodes from trepan.version import __version__ - def process_options(pkg_version, sys_argv, option_list=None): """Handle debugger options. Set `option_list' if you are writing 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): "-P", "--port", dest="port", - default=1027, + default=DEFAULT_DEBUG_PORT, action="store", type="int", metavar="NUMBER", @@ -92,7 +92,7 @@ def process_options(pkg_version, sys_argv, option_list=None): "open": True, "IO": "TCP", "HOST": "127.0.0.1", - "PORT": 1027, + "PORT": DEFAULT_DEBUG_PORT, } diff --git a/trepan/processor/cmdproc.py b/trepan/processor/cmdproc.py index 3944f72d..2f1a7fde 100644 --- a/trepan/processor/cmdproc.py +++ b/trepan/processor/cmdproc.py @@ -38,6 +38,7 @@ import trepan.lib.file as Mfile import trepan.lib.thred as Mthread import trepan.misc as Mmisc +from trepan.interfaces.script import ScriptInterface from trepan.lib.bytecode import is_class_def, is_def_stmt from trepan.processor.complete import completer from trepan.processor.print import print_location @@ -547,7 +548,7 @@ def process_commands(self): self.last_command = "" else: if self.debugger.intf[-1].output: - self.debugger.intf[-1].output.writeline("Leaving") + self.debugger.intf[-1].msg("Leaving") raise SystemExit pass break @@ -560,7 +561,7 @@ def process_commands(self): while frame: del frame.f_trace frame = frame.f_back - self.debugger.intf[-1].output.writeline("Fast continue...") + self.debugger.intf[-1].msg("Fast continue...") remove_hook(self.core.trace_dispatch, True) return @@ -702,6 +703,8 @@ def setup(self): pass if self.event in ["exception", "c_exception"]: exc_type, exc_value, exc_traceback = self.event_arg + elif self.event == "finished": + self.frame = exc_traceback = None else: _, _, exc_traceback = ( None, @@ -749,7 +752,11 @@ def queue_startfile(self, cmdfile): expanded_cmdfile = osp.expanduser(cmdfile) is_readable = Mfile.readable(expanded_cmdfile) if is_readable: - self.cmd_queue.append("source " + expanded_cmdfile) + script_intf = ScriptInterface( + expanded_cmdfile, out=self.debugger.intf[-1].output + ) + self.debugger.intf.append(script_intf) + # self.cmd_queue.append("source " + expanded_cmdfile) elif is_readable is None: self.errmsg(f"source file '{expanded_cmdfile}' doesn't exist") else: