Skip to content
Merged
6 changes: 6 additions & 0 deletions Doc/library/pdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
insert ::

Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ pdb
argument. If given, this is printed to the console just before debugging
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
--

Expand Down
33 changes: 29 additions & 4 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,24 @@ def lookupmodule(self, filename):
return fullname
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):
# The script has to run in __main__ namespace (or imports from
# __main__ will break).
Expand Down Expand Up @@ -1635,29 +1653,33 @@ def help():
def main():
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:
print(_usage)
sys.exit(2)

commands = []
run_as_module = False
for opt, optarg in opts:
if opt in ['-h', '--help']:
print(_usage)
sys.exit()
elif opt in ['-c', '--command']:
commands.append(optarg)
elif opt in ['-m']:
run_as_module = True

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')
sys.exit(1)

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.
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
# modified by the script being debugged. It's a bad idea when it was
Expand All @@ -1667,7 +1689,10 @@ def main():
pdb.rcLines.extend(commands)
while True:
try:
pdb._runscript(mainpyfile)
if run_as_module:
pdb._runmodule(mainpyfile)
else:
pdb._runscript(mainpyfile)
if pdb._user_requested_quit:
break
print("The program finished and will be restarted")
Expand Down
161 changes: 144 additions & 17 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -938,26 +938,47 @@ def test_pdb_issue_20766():
pdb 2: <built-in function default_int_handler>
"""


class PdbTestCase(unittest.TestCase):
def tearDown(self):
support.unlink(support.TESTFN)

def run_pdb(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)
def _run_pdb(self, pdb_args, commands):
self.addCleanup(support.rmtree, '__pycache__')
cmd = [sys.executable, '-m', 'pdb', filename]
stdout = stderr = None
with subprocess.Popen(cmd, stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as proc:
cmd = [sys.executable, '-m', 'pdb'] + pdb_args
with subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as proc:
stdout, stderr = proc.communicate(str.encode(commands))
stdout = stdout and bytes.decode(stdout)
stderr = stderr and bytes.decode(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):
file_content = textwrap.dedent(file_content)

Expand Down Expand Up @@ -1034,7 +1055,7 @@ def bar():
with open('bar.py', 'w') as f:
f.write(textwrap.dedent(bar))
self.addCleanup(support.unlink, 'bar.py')
stdout, stderr = self.run_pdb(script, commands)
stdout, stderr = self.run_pdb_script(script, commands)
self.assertTrue(
any('main.py(5)foo()->None' in l for l in stdout.splitlines()),
'Fail to step into the caller after a return')
Expand Down Expand Up @@ -1071,7 +1092,7 @@ def test_issue16180(self):
script = "def f: pass\n"
commands = ''
expected = "SyntaxError:"
stdout, stderr = self.run_pdb(script, commands)
stdout, stderr = self.run_pdb_script(script, commands)
self.assertIn(expected, stdout,
'\n\nExpected:\n{}\nGot:\n{}\n'
'Fail to handle a syntax error in the debuggee.'
Expand Down Expand Up @@ -1119,13 +1140,119 @@ def test_header(self):
pdb.set_trace(header=header)
self.assertEqual(stdout.getvalue(), header + '\n')

def tearDown(self):
support.unlink(support.TESTFN)
def test_run_module(self):
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):
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)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support to run modules with pdb