[Python-checkins] bpo-32206: Pdb can now run modules (GH-4752)

Nick Coghlan webhook-mailer at python.org
Sat Jan 6 02:53:10 EST 2018


https://github.com/python/cpython/commit/9f1e5f1b7f074e026843a5d70834233a95a6bf9d
commit: 9f1e5f1b7f074e026843a5d70834233a95a6bf9d
branch: master
author: Mario Corchero <mariocj89 at gmail.com>
committer: Nick Coghlan <ncoghlan at gmail.com>
date: 2018-01-06T17:53:05+10:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2017-12-07-13-14-40.bpo-32206.obm4OM.rst
M Doc/library/pdb.rst
M Doc/whatsnew/3.7.rst
M Lib/pdb.py
M Lib/test/test_pdb.py

diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst
index 4f3148fb5c3..e81c195782d 100644
--- a/Doc/library/pdb.rst
+++ b/Doc/library/pdb.rst
@@ -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 ::
 
diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst
index 1311e9e2016..9785d599dd3 100644
--- a/Doc/whatsnew/3.7.rst
+++ b/Doc/whatsnew/3.7.rst
@@ -426,6 +426,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
 --
 
diff --git a/Lib/pdb.py b/Lib/pdb.py
index 8dd4dedb220..d1a74bb7d7b 100755
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -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).
@@ -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
@@ -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")
diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py
index 71d8203fc56..0cd235e98ae 100644
--- a/Lib/test/test_pdb.py
+++ b/Lib/test/test_pdb.py
@@ -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)
 
@@ -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')
@@ -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.'
@@ -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)
 
 
diff --git a/Misc/NEWS.d/next/Library/2017-12-07-13-14-40.bpo-32206.obm4OM.rst b/Misc/NEWS.d/next/Library/2017-12-07-13-14-40.bpo-32206.obm4OM.rst
new file mode 100644
index 00000000000..20d7eeace87
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-12-07-13-14-40.bpo-32206.obm4OM.rst
@@ -0,0 +1 @@
+Add support to run modules with pdb



More information about the Python-checkins mailing list