[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