Commit 9f1e5f1b authored by Mario Corchero's avatar Mario Corchero Committed by Nick Coghlan

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.
parent 735ae8d1
...@@ -61,6 +61,12 @@ useful than quitting the debugger upon program's exit. ...@@ -61,6 +61,12 @@ useful than quitting the debugger upon program's exit.
:file:`pdb.py` now accepts a ``-c`` option that executes commands as if given :file:`pdb.py` now accepts a ``-c`` option that executes commands as if given
in a :file:`.pdbrc` file, see :ref:`debugger-commands`. in a :file:`.pdbrc` file, see :ref:`debugger-commands`.
.. versionadded:: 3.7
:file:`pdb.py` now accepts a ``-m`` option that execute modules similar to the way
``python3 -m`` does. As with a script, the debugger will pause execution just
before the first line of the module.
The typical usage to break into the debugger from a running program is to The typical usage to break into the debugger from a running program is to
insert :: insert ::
......
...@@ -426,6 +426,10 @@ pdb ...@@ -426,6 +426,10 @@ pdb
argument. If given, this is printed to the console just before debugging argument. If given, this is printed to the console just before debugging
begins. (Contributed by Barry Warsaw in :issue:`31389`.) begins. (Contributed by Barry Warsaw in :issue:`31389`.)
pdb command line now accepts `-m module_name` as an alternative to
script file. (Contributed by Mario Corchero in :issue:`32206`.)
re re
-- --
......
...@@ -1521,6 +1521,24 @@ class Pdb(bdb.Bdb, cmd.Cmd): ...@@ -1521,6 +1521,24 @@ class Pdb(bdb.Bdb, cmd.Cmd):
return fullname return fullname
return None return None
def _runmodule(self, module_name):
self._wait_for_mainpyfile = True
self._user_requested_quit = False
import runpy
mod_name, mod_spec, code = runpy._get_module_details(module_name)
self.mainpyfile = self.canonic(code.co_filename)
import __main__
__main__.__dict__.clear()
__main__.__dict__.update({
"__name__": "__main__",
"__file__": self.mainpyfile,
"__package__": module_name,
"__loader__": mod_spec.loader,
"__spec__": mod_spec,
"__builtins__": __builtins__,
})
self.run(code)
def _runscript(self, filename): def _runscript(self, filename):
# The script has to run in __main__ namespace (or imports from # The script has to run in __main__ namespace (or imports from
# __main__ will break). # __main__ will break).
...@@ -1635,29 +1653,33 @@ To let the script run up to a given line X in the debugged file, use ...@@ -1635,29 +1653,33 @@ To let the script run up to a given line X in the debugged file, use
def main(): def main():
import getopt import getopt
opts, args = getopt.getopt(sys.argv[1:], 'hc:', ['--help', '--command=']) opts, args = getopt.getopt(sys.argv[1:], 'mhc:', ['--help', '--command='])
if not args: if not args:
print(_usage) print(_usage)
sys.exit(2) sys.exit(2)
commands = [] commands = []
run_as_module = False
for opt, optarg in opts: for opt, optarg in opts:
if opt in ['-h', '--help']: if opt in ['-h', '--help']:
print(_usage) print(_usage)
sys.exit() sys.exit()
elif opt in ['-c', '--command']: elif opt in ['-c', '--command']:
commands.append(optarg) commands.append(optarg)
elif opt in ['-m']:
run_as_module = True
mainpyfile = args[0] # Get script filename mainpyfile = args[0] # Get script filename
if not os.path.exists(mainpyfile): if not run_as_module and not os.path.exists(mainpyfile):
print('Error:', mainpyfile, 'does not exist') print('Error:', mainpyfile, 'does not exist')
sys.exit(1) sys.exit(1)
sys.argv[:] = args # Hide "pdb.py" and pdb options from argument list sys.argv[:] = args # Hide "pdb.py" and pdb options from argument list
# Replace pdb's dir with script's dir in front of module search path. # Replace pdb's dir with script's dir in front of module search path.
sys.path[0] = os.path.dirname(mainpyfile) if not run_as_module:
sys.path[0] = os.path.dirname(mainpyfile)
# Note on saving/restoring sys.argv: it's a good idea when sys.argv was # Note on saving/restoring sys.argv: it's a good idea when sys.argv was
# modified by the script being debugged. It's a bad idea when it was # modified by the script being debugged. It's a bad idea when it was
...@@ -1667,7 +1689,10 @@ def main(): ...@@ -1667,7 +1689,10 @@ def main():
pdb.rcLines.extend(commands) pdb.rcLines.extend(commands)
while True: while True:
try: try:
pdb._runscript(mainpyfile) if run_as_module:
pdb._runmodule(mainpyfile)
else:
pdb._runscript(mainpyfile)
if pdb._user_requested_quit: if pdb._user_requested_quit:
break break
print("The program finished and will be restarted") print("The program finished and will be restarted")
......
...@@ -938,26 +938,47 @@ def test_pdb_issue_20766(): ...@@ -938,26 +938,47 @@ def test_pdb_issue_20766():
pdb 2: <built-in function default_int_handler> pdb 2: <built-in function default_int_handler>
""" """
class PdbTestCase(unittest.TestCase): class PdbTestCase(unittest.TestCase):
def tearDown(self):
support.unlink(support.TESTFN)
def run_pdb(self, script, commands): def _run_pdb(self, pdb_args, commands):
"""Run 'script' lines with pdb and the pdb 'commands'."""
filename = 'main.py'
with open(filename, 'w') as f:
f.write(textwrap.dedent(script))
self.addCleanup(support.unlink, filename)
self.addCleanup(support.rmtree, '__pycache__') self.addCleanup(support.rmtree, '__pycache__')
cmd = [sys.executable, '-m', 'pdb', filename] cmd = [sys.executable, '-m', 'pdb'] + pdb_args
stdout = stderr = None with subprocess.Popen(
with subprocess.Popen(cmd, stdout=subprocess.PIPE, cmd,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stdin=subprocess.PIPE,
) as proc: stderr=subprocess.STDOUT,
) as proc:
stdout, stderr = proc.communicate(str.encode(commands)) stdout, stderr = proc.communicate(str.encode(commands))
stdout = stdout and bytes.decode(stdout) stdout = stdout and bytes.decode(stdout)
stderr = stderr and bytes.decode(stderr) stderr = stderr and bytes.decode(stderr)
return stdout, stderr return stdout, stderr
def run_pdb_script(self, script, commands):
"""Run 'script' lines with pdb and the pdb 'commands'."""
filename = 'main.py'
with open(filename, 'w') as f:
f.write(textwrap.dedent(script))
self.addCleanup(support.unlink, filename)
return self._run_pdb([filename], commands)
def run_pdb_module(self, script, commands):
"""Runs the script code as part of a module"""
self.module_name = 't_main'
support.rmtree(self.module_name)
main_file = self.module_name + '/__main__.py'
init_file = self.module_name + '/__init__.py'
os.mkdir(self.module_name)
with open(init_file, 'w') as f:
pass
with open(main_file, 'w') as f:
f.write(textwrap.dedent(script))
self.addCleanup(support.rmtree, self.module_name)
return self._run_pdb(['-m', self.module_name], commands)
def _assert_find_function(self, file_content, func_name, expected): def _assert_find_function(self, file_content, func_name, expected):
file_content = textwrap.dedent(file_content) file_content = textwrap.dedent(file_content)
...@@ -1034,7 +1055,7 @@ class PdbTestCase(unittest.TestCase): ...@@ -1034,7 +1055,7 @@ class PdbTestCase(unittest.TestCase):
with open('bar.py', 'w') as f: with open('bar.py', 'w') as f:
f.write(textwrap.dedent(bar)) f.write(textwrap.dedent(bar))
self.addCleanup(support.unlink, 'bar.py') self.addCleanup(support.unlink, 'bar.py')
stdout, stderr = self.run_pdb(script, commands) stdout, stderr = self.run_pdb_script(script, commands)
self.assertTrue( self.assertTrue(
any('main.py(5)foo()->None' in l for l in stdout.splitlines()), any('main.py(5)foo()->None' in l for l in stdout.splitlines()),
'Fail to step into the caller after a return') 'Fail to step into the caller after a return')
...@@ -1071,7 +1092,7 @@ class PdbTestCase(unittest.TestCase): ...@@ -1071,7 +1092,7 @@ class PdbTestCase(unittest.TestCase):
script = "def f: pass\n" script = "def f: pass\n"
commands = '' commands = ''
expected = "SyntaxError:" expected = "SyntaxError:"
stdout, stderr = self.run_pdb(script, commands) stdout, stderr = self.run_pdb_script(script, commands)
self.assertIn(expected, stdout, self.assertIn(expected, stdout,
'\n\nExpected:\n{}\nGot:\n{}\n' '\n\nExpected:\n{}\nGot:\n{}\n'
'Fail to handle a syntax error in the debuggee.' 'Fail to handle a syntax error in the debuggee.'
...@@ -1119,13 +1140,119 @@ class PdbTestCase(unittest.TestCase): ...@@ -1119,13 +1140,119 @@ class PdbTestCase(unittest.TestCase):
pdb.set_trace(header=header) pdb.set_trace(header=header)
self.assertEqual(stdout.getvalue(), header + '\n') self.assertEqual(stdout.getvalue(), header + '\n')
def tearDown(self): def test_run_module(self):
support.unlink(support.TESTFN) script = """print("SUCCESS")"""
commands = """
continue
quit
"""
stdout, stderr = self.run_pdb_module(script, commands)
self.assertTrue(any("SUCCESS" in l for l in stdout.splitlines()), stdout)
def test_module_is_run_as_main(self):
script = """
if __name__ == '__main__':
print("SUCCESS")
"""
commands = """
continue
quit
"""
stdout, stderr = self.run_pdb_module(script, commands)
self.assertTrue(any("SUCCESS" in l for l in stdout.splitlines()), stdout)
def test_breakpoint(self):
script = """
if __name__ == '__main__':
pass
print("SUCCESS")
pass
"""
commands = """
b 3
quit
"""
stdout, stderr = self.run_pdb_module(script, commands)
self.assertTrue(any("Breakpoint 1 at" in l for l in stdout.splitlines()), stdout)
self.assertTrue(all("SUCCESS" not in l for l in stdout.splitlines()), stdout)
def test_run_pdb_with_pdb(self):
commands = """
c
quit
"""
stdout, stderr = self._run_pdb(["-m", "pdb"], commands)
self.assertIn("Debug the Python program given by pyfile.", stdout.splitlines())
def test_module_without_a_main(self):
module_name = 't_main'
support.rmtree(module_name)
init_file = module_name + '/__init__.py'
os.mkdir(module_name)
with open(init_file, 'w') as f:
pass
self.addCleanup(support.rmtree, module_name)
stdout, stderr = self._run_pdb(['-m', module_name], "")
self.assertIn("ImportError: No module named t_main.__main__",
stdout.splitlines())
def test_blocks_at_first_code_line(self):
script = """
#This is a comment, on line 2
print("SUCCESS")
"""
commands = """
quit
"""
stdout, stderr = self.run_pdb_module(script, commands)
self.assertTrue(any("__main__.py(4)<module>()"
in l for l in stdout.splitlines()), stdout)
def test_relative_imports(self):
self.module_name = 't_main'
support.rmtree(self.module_name)
main_file = self.module_name + '/__main__.py'
init_file = self.module_name + '/__init__.py'
module_file = self.module_name + '/module.py'
self.addCleanup(support.rmtree, self.module_name)
os.mkdir(self.module_name)
with open(init_file, 'w') as f:
f.write(textwrap.dedent("""
top_var = "VAR from top"
"""))
with open(main_file, 'w') as f:
f.write(textwrap.dedent("""
from . import top_var
from .module import var
from . import module
pass # We'll stop here and print the vars
"""))
with open(module_file, 'w') as f:
f.write(textwrap.dedent("""
var = "VAR from module"
var2 = "second var"
"""))
commands = """
b 5
c
p top_var
p var
p module.var2
quit
"""
stdout, _ = self._run_pdb(['-m', self.module_name], commands)
self.assertTrue(any("VAR from module" in l for l in stdout.splitlines()))
self.assertTrue(any("VAR from top" in l for l in stdout.splitlines()))
self.assertTrue(any("second var" in l for l in stdout.splitlines()))
def load_tests(*args): def load_tests(*args):
from test import test_pdb from test import test_pdb
suites = [unittest.makeSuite(PdbTestCase), doctest.DocTestSuite(test_pdb)] suites = [
unittest.makeSuite(PdbTestCase),
doctest.DocTestSuite(test_pdb)
]
return unittest.TestSuite(suites) return unittest.TestSuite(suites)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment