Skip to content

Commit 9f1e5f1

Browse files
mariocj89ncoghlan
authored andcommitted
bpo-32206: Pdb can now run modules (GH-4752)
Add a new argument "-m" to the pdb module to allow users to run `python -m pdb -m my_module_name`. This relies on private APIs in the runpy module to work, but we can get away with that since they're both part of the standard library and can be updated together if the runpy internals get refactored.
1 parent 735ae8d commit 9f1e5f1

File tree

5 files changed

+184
-21
lines changed

5 files changed

+184
-21
lines changed

Doc/library/pdb.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ useful than quitting the debugger upon program's exit.
6161
:file:`pdb.py` now accepts a ``-c`` option that executes commands as if given
6262
in a :file:`.pdbrc` file, see :ref:`debugger-commands`.
6363

64+
.. versionadded:: 3.7
65+
:file:`pdb.py` now accepts a ``-m`` option that execute modules similar to the way
66+
``python3 -m`` does. As with a script, the debugger will pause execution just
67+
before the first line of the module.
68+
69+
6470
The typical usage to break into the debugger from a running program is to
6571
insert ::
6672

Doc/whatsnew/3.7.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,10 @@ pdb
426426
argument. If given, this is printed to the console just before debugging
427427
begins. (Contributed by Barry Warsaw in :issue:`31389`.)
428428

429+
pdb command line now accepts `-m module_name` as an alternative to
430+
script file. (Contributed by Mario Corchero in :issue:`32206`.)
431+
432+
429433
re
430434
--
431435

Lib/pdb.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,24 @@ def lookupmodule(self, filename):
15211521
return fullname
15221522
return None
15231523

1524+
def _runmodule(self, module_name):
1525+
self._wait_for_mainpyfile = True
1526+
self._user_requested_quit = False
1527+
import runpy
1528+
mod_name, mod_spec, code = runpy._get_module_details(module_name)
1529+
self.mainpyfile = self.canonic(code.co_filename)
1530+
import __main__
1531+
__main__.__dict__.clear()
1532+
__main__.__dict__.update({
1533+
"__name__": "__main__",
1534+
"__file__": self.mainpyfile,
1535+
"__package__": module_name,
1536+
"__loader__": mod_spec.loader,
1537+
"__spec__": mod_spec,
1538+
"__builtins__": __builtins__,
1539+
})
1540+
self.run(code)
1541+
15241542
def _runscript(self, filename):
15251543
# The script has to run in __main__ namespace (or imports from
15261544
# __main__ will break).
@@ -1635,29 +1653,33 @@ def help():
16351653
def main():
16361654
import getopt
16371655

1638-
opts, args = getopt.getopt(sys.argv[1:], 'hc:', ['--help', '--command='])
1656+
opts, args = getopt.getopt(sys.argv[1:], 'mhc:', ['--help', '--command='])
16391657

16401658
if not args:
16411659
print(_usage)
16421660
sys.exit(2)
16431661

16441662
commands = []
1663+
run_as_module = False
16451664
for opt, optarg in opts:
16461665
if opt in ['-h', '--help']:
16471666
print(_usage)
16481667
sys.exit()
16491668
elif opt in ['-c', '--command']:
16501669
commands.append(optarg)
1670+
elif opt in ['-m']:
1671+
run_as_module = True
16511672

16521673
mainpyfile = args[0] # Get script filename
1653-
if not os.path.exists(mainpyfile):
1674+
if not run_as_module and not os.path.exists(mainpyfile):
16541675
print('Error:', mainpyfile, 'does not exist')
16551676
sys.exit(1)
16561677

16571678
sys.argv[:] = args # Hide "pdb.py" and pdb options from argument list
16581679

16591680
# Replace pdb's dir with script's dir in front of module search path.
1660-
sys.path[0] = os.path.dirname(mainpyfile)
1681+
if not run_as_module:
1682+
sys.path[0] = os.path.dirname(mainpyfile)
16611683

16621684
# Note on saving/restoring sys.argv: it's a good idea when sys.argv was
16631685
# modified by the script being debugged. It's a bad idea when it was
@@ -1667,7 +1689,10 @@ def main():
16671689
pdb.rcLines.extend(commands)
16681690
while True:
16691691
try:
1670-
pdb._runscript(mainpyfile)
1692+
if run_as_module:
1693+
pdb._runmodule(mainpyfile)
1694+
else:
1695+
pdb._runscript(mainpyfile)
16711696
if pdb._user_requested_quit:
16721697
break
16731698
print("The program finished and will be restarted")

Lib/test/test_pdb.py

Lines changed: 144 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -938,26 +938,47 @@ def test_pdb_issue_20766():
938938
pdb 2: <built-in function default_int_handler>
939939
"""
940940

941+
941942
class PdbTestCase(unittest.TestCase):
943+
def tearDown(self):
944+
support.unlink(support.TESTFN)
942945

943-
def run_pdb(self, script, commands):
944-
"""Run 'script' lines with pdb and the pdb 'commands'."""
945-
filename = 'main.py'
946-
with open(filename, 'w') as f:
947-
f.write(textwrap.dedent(script))
948-
self.addCleanup(support.unlink, filename)
946+
def _run_pdb(self, pdb_args, commands):
949947
self.addCleanup(support.rmtree, '__pycache__')
950-
cmd = [sys.executable, '-m', 'pdb', filename]
951-
stdout = stderr = None
952-
with subprocess.Popen(cmd, stdout=subprocess.PIPE,
953-
stdin=subprocess.PIPE,
954-
stderr=subprocess.STDOUT,
955-
) as proc:
948+
cmd = [sys.executable, '-m', 'pdb'] + pdb_args
949+
with subprocess.Popen(
950+
cmd,
951+
stdout=subprocess.PIPE,
952+
stdin=subprocess.PIPE,
953+
stderr=subprocess.STDOUT,
954+
) as proc:
956955
stdout, stderr = proc.communicate(str.encode(commands))
957956
stdout = stdout and bytes.decode(stdout)
958957
stderr = stderr and bytes.decode(stderr)
959958
return stdout, stderr
960959

960+
def run_pdb_script(self, script, commands):
961+
"""Run 'script' lines with pdb and the pdb 'commands'."""
962+
filename = 'main.py'
963+
with open(filename, 'w') as f:
964+
f.write(textwrap.dedent(script))
965+
self.addCleanup(support.unlink, filename)
966+
return self._run_pdb([filename], commands)
967+
968+
def run_pdb_module(self, script, commands):
969+
"""Runs the script code as part of a module"""
970+
self.module_name = 't_main'
971+
support.rmtree(self.module_name)
972+
main_file = self.module_name + '/__main__.py'
973+
init_file = self.module_name + '/__init__.py'
974+
os.mkdir(self.module_name)
975+
with open(init_file, 'w') as f:
976+
pass
977+
with open(main_file, 'w') as f:
978+
f.write(textwrap.dedent(script))
979+
self.addCleanup(support.rmtree, self.module_name)
980+
return self._run_pdb(['-m', self.module_name], commands)
981+
961982
def _assert_find_function(self, file_content, func_name, expected):
962983
file_content = textwrap.dedent(file_content)
963984

@@ -1034,7 +1055,7 @@ def bar():
10341055
with open('bar.py', 'w') as f:
10351056
f.write(textwrap.dedent(bar))
10361057
self.addCleanup(support.unlink, 'bar.py')
1037-
stdout, stderr = self.run_pdb(script, commands)
1058+
stdout, stderr = self.run_pdb_script(script, commands)
10381059
self.assertTrue(
10391060
any('main.py(5)foo()->None' in l for l in stdout.splitlines()),
10401061
'Fail to step into the caller after a return')
@@ -1071,7 +1092,7 @@ def test_issue16180(self):
10711092
script = "def f: pass\n"
10721093
commands = ''
10731094
expected = "SyntaxError:"
1074-
stdout, stderr = self.run_pdb(script, commands)
1095+
stdout, stderr = self.run_pdb_script(script, commands)
10751096
self.assertIn(expected, stdout,
10761097
'\n\nExpected:\n{}\nGot:\n{}\n'
10771098
'Fail to handle a syntax error in the debuggee.'
@@ -1119,13 +1140,119 @@ def test_header(self):
11191140
pdb.set_trace(header=header)
11201141
self.assertEqual(stdout.getvalue(), header + '\n')
11211142

1122-
def tearDown(self):
1123-
support.unlink(support.TESTFN)
1143+
def test_run_module(self):
1144+
script = """print("SUCCESS")"""
1145+
commands = """
1146+
continue
1147+
quit
1148+
"""
1149+
stdout, stderr = self.run_pdb_module(script, commands)
1150+
self.assertTrue(any("SUCCESS" in l for l in stdout.splitlines()), stdout)
1151+
1152+
def test_module_is_run_as_main(self):
1153+
script = """
1154+
if __name__ == '__main__':
1155+
print("SUCCESS")
1156+
"""
1157+
commands = """
1158+
continue
1159+
quit
1160+
"""
1161+
stdout, stderr = self.run_pdb_module(script, commands)
1162+
self.assertTrue(any("SUCCESS" in l for l in stdout.splitlines()), stdout)
1163+
1164+
def test_breakpoint(self):
1165+
script = """
1166+
if __name__ == '__main__':
1167+
pass
1168+
print("SUCCESS")
1169+
pass
1170+
"""
1171+
commands = """
1172+
b 3
1173+
quit
1174+
"""
1175+
stdout, stderr = self.run_pdb_module(script, commands)
1176+
self.assertTrue(any("Breakpoint 1 at" in l for l in stdout.splitlines()), stdout)
1177+
self.assertTrue(all("SUCCESS" not in l for l in stdout.splitlines()), stdout)
1178+
1179+
def test_run_pdb_with_pdb(self):
1180+
commands = """
1181+
c
1182+
quit
1183+
"""
1184+
stdout, stderr = self._run_pdb(["-m", "pdb"], commands)
1185+
self.assertIn("Debug the Python program given by pyfile.", stdout.splitlines())
1186+
1187+
def test_module_without_a_main(self):
1188+
module_name = 't_main'
1189+
support.rmtree(module_name)
1190+
init_file = module_name + '/__init__.py'
1191+
os.mkdir(module_name)
1192+
with open(init_file, 'w') as f:
1193+
pass
1194+
self.addCleanup(support.rmtree, module_name)
1195+
stdout, stderr = self._run_pdb(['-m', module_name], "")
1196+
self.assertIn("ImportError: No module named t_main.__main__",
1197+
stdout.splitlines())
1198+
1199+
def test_blocks_at_first_code_line(self):
1200+
script = """
1201+
#This is a comment, on line 2
1202+
1203+
print("SUCCESS")
1204+
"""
1205+
commands = """
1206+
quit
1207+
"""
1208+
stdout, stderr = self.run_pdb_module(script, commands)
1209+
self.assertTrue(any("__main__.py(4)<module>()"
1210+
in l for l in stdout.splitlines()), stdout)
1211+
1212+
def test_relative_imports(self):
1213+
self.module_name = 't_main'
1214+
support.rmtree(self.module_name)
1215+
main_file = self.module_name + '/__main__.py'
1216+
init_file = self.module_name + '/__init__.py'
1217+
module_file = self.module_name + '/module.py'
1218+
self.addCleanup(support.rmtree, self.module_name)
1219+
os.mkdir(self.module_name)
1220+
with open(init_file, 'w') as f:
1221+
f.write(textwrap.dedent("""
1222+
top_var = "VAR from top"
1223+
"""))
1224+
with open(main_file, 'w') as f:
1225+
f.write(textwrap.dedent("""
1226+
from . import top_var
1227+
from .module import var
1228+
from . import module
1229+
pass # We'll stop here and print the vars
1230+
"""))
1231+
with open(module_file, 'w') as f:
1232+
f.write(textwrap.dedent("""
1233+
var = "VAR from module"
1234+
var2 = "second var"
1235+
"""))
1236+
commands = """
1237+
b 5
1238+
c
1239+
p top_var
1240+
p var
1241+
p module.var2
1242+
quit
1243+
"""
1244+
stdout, _ = self._run_pdb(['-m', self.module_name], commands)
1245+
self.assertTrue(any("VAR from module" in l for l in stdout.splitlines()))
1246+
self.assertTrue(any("VAR from top" in l for l in stdout.splitlines()))
1247+
self.assertTrue(any("second var" in l for l in stdout.splitlines()))
11241248

11251249

11261250
def load_tests(*args):
11271251
from test import test_pdb
1128-
suites = [unittest.makeSuite(PdbTestCase), doctest.DocTestSuite(test_pdb)]
1252+
suites = [
1253+
unittest.makeSuite(PdbTestCase),
1254+
doctest.DocTestSuite(test_pdb)
1255+
]
11291256
return unittest.TestSuite(suites)
11301257

11311258

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support to run modules with pdb

0 commit comments

Comments
 (0)