python conditional debug breakpoint one-liner for versions before 3.7 PEP 553 that acts similarly to Perl's $DB::single=1
Asked Answered
C

2

16

In python versions before PEP 553 breakpoint() utility, what is the recommended way to add (ideally a one-liner) code to have a breakpoint that can be ignored upon a condition (e.g. a global debug flag or args.debug flag).

In Perl, I am used to use $DB::single=1;1; single-lines, which I know I can safely leave in the code and won't affect the normal running of perl code.pl unless explicitly calling perl -d code.pl. E.g.:

my $a = 1;
$DB::single=1;1; # breakpoint line
my $b = 2;
print "$a $b\n";

If I run this code as: perl code.pl, it will run to completion. If I run this code with: perl -d code.pl, the pdb will stop at the breakpoint line (not before the next line with a my $b = 2; statement) because it contains a 1; statement after the $DB::single=1; statement;

Similarly, if I write:

my $debug = 1;
my $a = 1;
$DB::single=$debug;1; # first breakpoint line
my $b = 2;
$DB::single=$debug;1; # second breakpoint line
print "$a $b\n";
# [...] Lots more code sprinkled with more of these
$DB::single=$debug;1; # n'th breakpoint line

I can then execute perl -d code.pl, which will stop in the first breakpoint line, then in the pdb session, once I am happy that it does not need stopping anywhere else, then execute: $debug = 0, then pdb continue c, which will make it not stop at the second or other similar breakpoint lines in the code.

How can I achieve the same, ideally in single-line statements, in python (2.x and 3.x before PEP 553)?

I am aware of PEP 553 and apart from the hassle of having to explicitly set PYTHONBREAKPOINT=0 python3.7 code.py or comment out the breakpoint() lines, it is a solution to the question here.

I thought of options like:

import pdb; pdb.set_trace()
dummy=None;

The statement underneath pdb.set_trace() is so that I can achieve the same as the 1; in the same line after $DB::single=1; in Perl, which is to have the debugger stop where I placed the breakpoint, rather than the next statement. This is so that if there are large chunks of commented code or documentation in-between, the debugger does not jump to the next statement far away from the breakpoint.

Or with conditionals like:

if args.debug or debug:
    import pdb; pdb.set_trace()
    _debug=False; #args.debug=False

So that if I am done debugging for a script, I can set args.debug=False or debug=False and not have to touch all these breakpoints in the code.

Circumlunar answered 14/6, 2019 at 10:27 Comment(1)
In case you're still interested in reciving an answer, is there something missing from my post that's necessary for you to pass as acceptable?Guise
G
7

Setting a conditional breakpoint

Same as perl, python can be run with -d to set a debug flag:

$ python --help
[...]
-d     : debug output from parser; also PYTHONDEBUG=x
[...]

You can check its state during runtime via sys.flags :

$ python -d
Python 2.7.15+ (default, Nov 27 2018, 23:36:35) 
[GCC 7.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.flags
sys.flags(debug=1, py3k_warning=0, division_warning=0, ...)
#         ^ there it is, right at the front

Which allows for the following one-liner to enable debugging:

import pdb, sys; pdb.set_trace() if sys.flags[0] else None

Disabling conditional breakpoints from the debugger

Regarding this part

[...] once I am happy that it does not need stopping anywhere else, then execute [something] which will make it not stop at the second or other similar breakpoint lines in the code.

it gets a little trickier though, since python doesn't allows mutation of the flags structure, or even creating an instance of it:

>>> import sys
>>> sys.flags.debug = 0                 # no mutating ...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: readonly attribute
>>> type(sys.flags)()                   # ... and no instanciating
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: cannot create 'sys.flags' instances

But as far as I have tested, unless you run python with other flags, the following works to deactivate subsequent traces without altering your program's other behavior:

import sys; sys.flags = [0]*len(sys.flags)  # even fits onto a single line

For a slightly more robust monkey patch that you should use in case the former one leads to strange bugs, you'll need to have something like this:

def untrace():
  """Call this function in the pdb session to skip debug-only set_trace() calls"""
  import re
  import sys
  from collections import namedtuple  # has the same interface as struct sequence
  sys.flags = namedtuple(
    'sys_flags', 
    [m.group() for m in re.finditer(r'\w{2,}', repr(sys.flags)[10:-1])]
  )(0, *sys.flags[1:])

While this statement can be put on a single line, it's probably a little too much. You can either paste this function into the .py file where you plan to use it, or have some kind of utils.py from which you import it during debugging, after which a c(ontinue) should again run the rest of the program:

(Pdb) import utils; utils.untrace()
(Pdb) c
Guise answered 18/6, 2019 at 18:38 Comment(1)
I didn't include anything about how to start the session before the next line, since I don't have something useful to add. But as a side note, it might be a little more idiomatic to use ... (the Ellipsis object) as a dummy statement instead of dummy = None.Guise
E
4

Here is a simple way using a .pdbrc file in the current directory:

t.py

def my_trace():
    global debug
    if debug:
        import pdb; pdb.set_trace()

debug = True
a= 1
my_trace()
b = 2
c = 3
my_trace()
d = 4

.pdbrc:

r

Example session:

$ python t.py
--Return--
> [...]/t.py(12)<module>()
-> b = 2
(Pdb) p a
1
(Pdb) p b
*** NameError: name 'b' is not defined
(Pdb) !debug=False
(Pdb) c
$ 
Ed answered 16/6, 2019 at 17:25 Comment(4)
Thanks for your answer. It seems like pdb jumps to the import pdb; pdb.set_trace() line when I run the script, rather than staying outside of my_trace(). Any ideas why?Circumlunar
Strange, it works for me (Python 3.7.1). Did you save the .pdbrc in the current directory? It won't work without it and the r (return) commandKunlun
Ah! Apologies, I was running python 2.x instead of python 3.x. I now got it to work with the behavior that you describe when using python 3.4.3 (old Ubuntu 14.04 laptop). Last thing is to be able to have a single line equivalent to $DB::single=1;1; so the pdb pointer stops in the my_trace() line rather than in the line with a statement after the my_trace() line.Circumlunar
Seems like it is not possible to add statements at the same line as in Perl, and have the debugger respect that as separate statements. The only thing I could suggest is to add a line below my_trace(). For example a dummy statement like: debug = debug should workKunlun

© 2022 - 2024 — McMap. All rights reserved.