Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 34 additions & 28 deletions docs/entry-exit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -201,38 +226,19 @@ session I would put the trepan debugger commands in a file, say
Calling the debugger from pytest
================================

Install `pytest-trepan <https://pypi.python.org/pypi/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
=============================================================
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down Expand Up @@ -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"]
Expand Down
4 changes: 3 additions & 1 deletion trepan/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -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:
Expand Down
195 changes: 105 additions & 90 deletions trepan/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions trepan/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
}


Expand Down
Loading