Skip to content

Commit cf638a2

Browse files
Abseil Teamcopybara-github
authored andcommitted
Allow $PYTHONBREAKPOINT to affect runcall and post_mortem debugging
PiperOrigin-RevId: 780556187
1 parent bdad52d commit cf638a2

File tree

3 files changed

+145
-7
lines changed

3 files changed

+145
-7
lines changed

absl/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ py_test(
5555
"//absl/testing:_bazelize_command",
5656
"//absl/testing:absltest",
5757
"//absl/testing:flagsaver",
58+
"//absl/testing:parameterized",
5859
],
5960
)
6061

absl/app.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def main(argv):
2727

2828
import collections
2929
import errno
30+
import importlib
3031
import os
3132
import pdb
3233
import sys
@@ -44,10 +45,21 @@ def main(argv):
4445

4546
FLAGS = flags.FLAGS
4647

47-
flags.DEFINE_boolean('run_with_pdb', False, 'Set to true for PDB debug mode')
48-
flags.DEFINE_boolean('pdb_post_mortem', False,
49-
'Set to true to handle uncaught exceptions with PDB '
50-
'post mortem.')
48+
flags.DEFINE_boolean(
49+
'run_with_pdb',
50+
False,
51+
'Set to true for debug mode. PDB is used by default; $PYTHONBREAKPOINT '
52+
'(https://docs.python.org/3/using/cmdline.html#envvar-PYTHONBREAKPOINT) '
53+
'can be used to specify a custom debugger.',
54+
)
55+
flags.DEFINE_boolean(
56+
'pdb_post_mortem',
57+
False,
58+
'Set to true to handle uncaught exceptions with the post mortem debugger.'
59+
'PDB is used by default; $PYTHONBREAKPOINT '
60+
'(https://docs.python.org/3/using/cmdline.html#envvar-PYTHONBREAKPOINT) '
61+
'can be used to specify a custom one.',
62+
)
5163
flags.DEFINE_alias('pdb', 'pdb_post_mortem')
5264
flags.DEFINE_boolean('run_with_profiling', False,
5365
'Set to true for profiling the script. '
@@ -65,6 +77,45 @@ def main(argv):
6577
allow_hide_cpp=True)
6678

6779

80+
def _get_debugger_module_with_function(function_name):
81+
"""Provides the `$PYTHONBREAKPOINT` module if it contains `function_name`.
82+
83+
Falls back to `pdb` otherwise.
84+
85+
Args:
86+
function_name: The name of the function required.
87+
88+
Returns:
89+
A debugger module providing `function_name`.
90+
"""
91+
python_breakpoint = os.getenv('PYTHONBREAKPOINT')
92+
# The special value '0' for `$PYTHONBREAKPOINT` means "do not use a debugger".
93+
# We don't respect it (if the user explicitly asks to debug) but shouldn't try
94+
# to import a module with this name.
95+
if python_breakpoint and python_breakpoint != '0':
96+
debugger_module_import = python_breakpoint.rsplit('.', 1)[0]
97+
try:
98+
debugger_module = importlib.import_module(debugger_module_import)
99+
except ImportError:
100+
logging.warning(
101+
(
102+
'Could not import $PYTHONBREAKPOINT debugger module %r, '
103+
'falling back to PDB'
104+
),
105+
debugger_module_import,
106+
)
107+
else:
108+
if hasattr(debugger_module, function_name):
109+
return debugger_module
110+
logging.warning(
111+
'$PYTHONBREAKPOINT debugger %r has no function %r, '
112+
'falling back to PDB',
113+
debugger_module_import,
114+
function_name,
115+
)
116+
return pdb
117+
118+
68119
# If main() exits via an abnormal exception, call into these
69120
# handlers before exiting.
70121
EXCEPTION_HANDLERS = []
@@ -239,9 +290,9 @@ def _register_and_parse_flags_with_usage(
239290

240291

241292
def _run_main(main, argv):
242-
"""Calls main, optionally with pdb or profiler."""
293+
"""Calls main, optionally with a debugger or profiler."""
243294
if FLAGS.run_with_pdb:
244-
sys.exit(pdb.runcall(main, argv))
295+
sys.exit(_get_debugger_module_with_function('runcall').runcall(main, argv))
245296
elif FLAGS.run_with_profiling or FLAGS.profile_file:
246297
# Avoid import overhead since most apps (including performance-sensitive
247298
# ones) won't be run with profiling.
@@ -331,7 +382,7 @@ def run(
331382
print()
332383
print(' *** Entering post-mortem debugging ***')
333384
print()
334-
pdb.post_mortem()
385+
_get_debugger_module_with_function('post_mortem').post_mortem()
335386
raise
336387
except Exception as e:
337388
_call_exception_handlers(e)

absl/tests/app_test.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
import contextlib
1818
import copy
1919
import enum
20+
import importlib
2021
import io
2122
import os
23+
import pdb
2224
import re
2325
import subprocess
2426
import sys
@@ -30,6 +32,7 @@
3032
from absl.testing import _bazelize_command
3133
from absl.testing import absltest
3234
from absl.testing import flagsaver
35+
from absl.testing import parameterized
3336
from absl.tests import app_test_helper
3437

3538

@@ -118,6 +121,89 @@ def test_register_and_parse_flags_with_usage_exits_on_second_run(self):
118121
app._register_and_parse_flags_with_usage()
119122

120123

124+
class _MyDebuggerModule:
125+
"""Imitates some aspects of the `pdb` interface."""
126+
127+
def post_mortem(self):
128+
pass
129+
130+
def runcall(self, *args, **kwargs):
131+
del args, kwargs
132+
133+
134+
class _IncompleteDebuggerModule:
135+
"""Provides `post_mortem` but not `runcall`."""
136+
137+
def post_mortem(self):
138+
pass
139+
140+
141+
def _pythonbreakpoint_variable(value):
142+
"""Returns a context manager setting the $PYTHONBREAKPOINT env. variable."""
143+
new_environment = {} if value is None else {'PYTHONBREAKPOINT': value}
144+
return mock.patch.object(os, 'environ', new_environment)
145+
146+
147+
class PythonBreakpointTest(parameterized.TestCase):
148+
149+
def setUp(self):
150+
super().setUp()
151+
152+
importlib_import_module = importlib.import_module
153+
154+
def my_import_module(module, *args, **kwargs):
155+
if module == 'my.debugger':
156+
return _MyDebuggerModule()
157+
if module == 'incomplete.debugger':
158+
return _IncompleteDebuggerModule()
159+
if module == '0':
160+
# `PYTHONBREAKPOINT=0` is a special value.
161+
raise ValueError('Should not try to import module `0`')
162+
return importlib_import_module(module, *args, **kwargs)
163+
164+
self._mock_import_module = self.enter_context(
165+
mock.patch.object(importlib, 'import_module', my_import_module)
166+
)
167+
168+
@parameterized.named_parameters(
169+
('no_variable', None),
170+
('empty_variable', ''),
171+
('explicit_pdb', 'pdb.set_trace'),
172+
('unimportable_module', 'unimportable.module.set_trace'),
173+
('opt_out', '0'),
174+
)
175+
def test_python_breakpoint_pdb(self, value):
176+
with _pythonbreakpoint_variable(value):
177+
self.assertIs(app._get_debugger_module_with_function('runcall'), pdb)
178+
self.assertIs(app._get_debugger_module_with_function('post_mortem'), pdb)
179+
180+
def test_my_debugger(self):
181+
with _pythonbreakpoint_variable('my.debugger.set_trace'):
182+
debugger_with_runcall = app._get_debugger_module_with_function('runcall')
183+
self.assertIsInstance(debugger_with_runcall, _MyDebuggerModule)
184+
self.assertTrue(hasattr(debugger_with_runcall, 'runcall'))
185+
186+
debugger_with_post_mortem = app._get_debugger_module_with_function(
187+
'post_mortem'
188+
)
189+
self.assertIsInstance(debugger_with_post_mortem, _MyDebuggerModule)
190+
self.assertTrue(hasattr(debugger_with_post_mortem, 'post_mortem'))
191+
192+
def test_incomplete_debugger(self):
193+
with _pythonbreakpoint_variable('incomplete.debugger.set_trace'):
194+
debugger_with_runcall = app._get_debugger_module_with_function('runcall')
195+
self.assertIs(debugger_with_runcall, pdb)
196+
self.assertTrue(hasattr(debugger_with_runcall, 'runcall'))
197+
198+
debugger_with_post_mortem = app._get_debugger_module_with_function(
199+
'post_mortem'
200+
)
201+
self.assertIsInstance(
202+
debugger_with_post_mortem, _IncompleteDebuggerModule
203+
)
204+
self.assertTrue(hasattr(debugger_with_post_mortem, 'post_mortem'))
205+
206+
121207
class FunctionalTests(absltest.TestCase):
122208
"""Functional tests that use runs app_test_helper."""
123209

0 commit comments

Comments
 (0)