[Python-checkins] bpo-44554: refactor pdb targets (and internal tweaks) (GH-26992)

miss-islington webhook-mailer at python.org
Sun Jul 18 21:01:05 EDT 2021


https://github.com/python/cpython/commit/2c2055884420f22afb4d2045bbdab7aa1394cb63
commit: 2c2055884420f22afb4d2045bbdab7aa1394cb63
branch: main
author: Jason R. Coombs <jaraco at jaraco.com>
committer: miss-islington <31488909+miss-islington at users.noreply.github.com>
date: 2021-07-18T18:00:35-07:00
summary:

bpo-44554: refactor pdb targets (and internal tweaks) (GH-26992)



- Refactor module/script handling to share an interface (check method).
- Import functools and adjust tests for the new line number for find_function.
- Use cached_property for details.
- Add blurb.

Automerge-Triggered-By: GH:jaraco

files:
A Misc/NEWS.d/next/Library/2021-07-02-18-17-56.bpo-44554.aBUmJo.rst
M Lib/pdb.py
M Lib/test/test_pdb.py
M Lib/test/test_pyclbr.py

diff --git a/Lib/pdb.py b/Lib/pdb.py
index 72ebd711ea976..e769ad7d26b18 100755
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -80,9 +80,12 @@
 import signal
 import inspect
 import tokenize
+import functools
 import traceback
 import linecache
 
+from typing import Union
+
 
 class Restart(Exception):
     """Causes a debugger to be restarted for the debugged python program."""
@@ -128,6 +131,77 @@ def __repr__(self):
         return self
 
 
+class ScriptTarget(str):
+    def __new__(cls, val):
+        # Mutate self to be the "real path".
+        res = super().__new__(cls, os.path.realpath(val))
+
+        # Store the original path for error reporting.
+        res.orig = val
+
+        return res
+
+    def check(self):
+        if not os.path.exists(self):
+            print('Error:', self.orig, 'does not exist')
+            sys.exit(1)
+
+        # Replace pdb's dir with script's dir in front of module search path.
+        sys.path[0] = os.path.dirname(self)
+
+    @property
+    def filename(self):
+        return self
+
+    @property
+    def namespace(self):
+        return dict(
+            __name__='__main__',
+            __file__=self,
+            __builtins__=__builtins__,
+        )
+
+    @property
+    def code(self):
+        with io.open(self) as fp:
+            return f"exec(compile({fp.read()!r}, {self!r}, 'exec'))"
+
+
+class ModuleTarget(str):
+    def check(self):
+        pass
+
+    @functools.cached_property
+    def _details(self):
+        import runpy
+        return runpy._get_module_details(self)
+
+    @property
+    def filename(self):
+        return self.code.co_filename
+
+    @property
+    def code(self):
+        name, spec, code = self._details
+        return code
+
+    @property
+    def _spec(self):
+        name, spec, code = self._details
+        return spec
+
+    @property
+    def namespace(self):
+        return dict(
+            __name__='__main__',
+            __file__=os.path.normcase(os.path.abspath(self.filename)),
+            __package__=self._spec.parent,
+            __loader__=self._spec.loader,
+            __spec__=self._spec,
+            __builtins__=__builtins__,
+        )
+
+
 # Interaction prompt line will separate file and call info from code
 # text using value of line_prefix string.  A newline and arrow may
 # be to your liking.  You can set it once pdb is imported using the
@@ -1538,49 +1612,26 @@ def lookupmodule(self, filename):
                 return fullname
         return None
 
-    def _runmodule(self, module_name):
+    def _run(self, target: Union[ModuleTarget, ScriptTarget]):
+        # When bdb sets tracing, a number of call and line events happen
+        # BEFORE debugger even reaches user's code (and the exact sequence of
+        # events depends on python version). Take special measures to
+        # avoid stopping before reaching the main script (see user_line and
+        # user_call for details).
         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__": mod_spec.parent,
-            "__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).
-        #
-        # So we clear up the __main__ and set several special variables
-        # (this gets rid of pdb's globals and cleans old variables on restarts).
+
+        self.mainpyfile = self.canonic(target.filename)
+
+        # The target has to run in __main__ namespace (or imports from
+        # __main__ will break). Clear __main__ and replace with
+        # the target namespace.
         import __main__
         __main__.__dict__.clear()
-        __main__.__dict__.update({"__name__"    : "__main__",
-                                  "__file__"    : filename,
-                                  "__builtins__": __builtins__,
-                                 })
+        __main__.__dict__.update(target.namespace)
+
+        self.run(target.code)
 
-        # When bdb sets tracing, a number of call and line events happens
-        # BEFORE debugger even reaches user's code (and the exact sequence of
-        # events depends on python version). So we take special measures to
-        # avoid stopping before we reach the main script (see user_line and
-        # user_call for details).
-        self._wait_for_mainpyfile = True
-        self.mainpyfile = self.canonic(filename)
-        self._user_requested_quit = False
-        with io.open_code(filename) as fp:
-            statement = "exec(compile(%r, %r, 'exec'))" % \
-                        (fp.read(), self.mainpyfile)
-        self.run(statement)
 
 # Collect all command help into docstring, if not run with -OO
 
@@ -1669,6 +1720,7 @@ def help():
 To let the script run up to a given line X in the debugged file, use
 "-c 'until X'"."""
 
+
 def main():
     import getopt
 
@@ -1678,28 +1730,19 @@ def main():
         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 run_as_module and not os.path.exists(mainpyfile):
-        print('Error:', mainpyfile, 'does not exist')
-        sys.exit(1)
+    if any(opt in ['-h', '--help'] for opt, optarg in opts):
+        print(_usage)
+        sys.exit()
 
-    sys.argv[:] = args      # Hide "pdb.py" and pdb options from argument list
+    commands = [optarg for opt, optarg in opts if opt in ['-c', '--command']]
 
-    if not run_as_module:
-        mainpyfile = os.path.realpath(mainpyfile)
-        # Replace pdb's dir with script's dir in front of module search path.
-        sys.path[0] = os.path.dirname(mainpyfile)
+    module_indicated = any(opt in ['-m'] for opt, optarg in opts)
+    cls = ModuleTarget if module_indicated else ScriptTarget
+    target = cls(args[0])
+
+    target.check()
+
+    sys.argv[:] = args      # Hide "pdb.py" and pdb options from argument list
 
     # 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
@@ -1709,15 +1752,12 @@ def main():
     pdb.rcLines.extend(commands)
     while True:
         try:
-            if run_as_module:
-                pdb._runmodule(mainpyfile)
-            else:
-                pdb._runscript(mainpyfile)
+            pdb._run(target)
             if pdb._user_requested_quit:
                 break
             print("The program finished and will be restarted")
         except Restart:
-            print("Restarting", mainpyfile, "with arguments:")
+            print("Restarting", target, "with arguments:")
             print("\t" + " ".join(sys.argv[1:]))
         except SystemExit:
             # In most cases SystemExit does not warrant a post-mortem session.
@@ -1732,7 +1772,7 @@ def main():
             print("Running 'cont' or 'step' will restart the program")
             t = sys.exc_info()[2]
             pdb.interaction(None, t)
-            print("Post mortem debugger finished. The " + mainpyfile +
+            print("Post mortem debugger finished. The " + target +
                   " will be restarted")
 
 
diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py
index 17634c707b6a1..5fe75175bf72a 100644
--- a/Lib/test/test_pdb.py
+++ b/Lib/test/test_pdb.py
@@ -362,7 +362,7 @@ def test_pdb_breakpoints_preserved_across_interactive_sessions():
     1   breakpoint   keep yes   at ...test_pdb.py:...
     2   breakpoint   keep yes   at ...test_pdb.py:...
     (Pdb) break pdb.find_function
-    Breakpoint 3 at ...pdb.py:94
+    Breakpoint 3 at ...pdb.py:97
     (Pdb) break
     Num Type         Disp Enb   Where
     1   breakpoint   keep yes   at ...test_pdb.py:...
diff --git a/Lib/test/test_pyclbr.py b/Lib/test/test_pyclbr.py
index 82c1ebb5b070f..4bb9cfcad9a76 100644
--- a/Lib/test/test_pyclbr.py
+++ b/Lib/test/test_pyclbr.py
@@ -222,7 +222,11 @@ def test_others(self):
         cm('pickle', ignore=('partial', 'PickleBuffer'))
         cm('aifc', ignore=('_aifc_params',))  # set with = in module
         cm('sre_parse', ignore=('dump', 'groups', 'pos')) # from sre_constants import *; property
-        cm('pdb')
+        cm(
+            'pdb',
+            # pyclbr does not handle elegantly `typing` or properties
+            ignore=('Union', 'ModuleTarget', 'ScriptTarget'),
+        )
         cm('pydoc', ignore=('input', 'output',)) # properties
 
         # Tests for modules inside packages
diff --git a/Misc/NEWS.d/next/Library/2021-07-02-18-17-56.bpo-44554.aBUmJo.rst b/Misc/NEWS.d/next/Library/2021-07-02-18-17-56.bpo-44554.aBUmJo.rst
new file mode 100644
index 0000000000000..6ca8cdc22fa6e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-07-02-18-17-56.bpo-44554.aBUmJo.rst
@@ -0,0 +1 @@
+Refactor argument processing in :func:pdb.main to simplify detection of errors in input loading and clarify behavior around module or script invocation.



More information about the Python-checkins mailing list