mirror of
https://github.com/python/cpython.git
synced 2025-09-27 02:39:58 +00:00
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
This commit is contained in:
parent
35b98e38b6
commit
2c20558844
4 changed files with 111 additions and 66 deletions
168
Lib/pdb.py
168
Lib/pdb.py
|
@ -80,9 +80,12 @@ import pprint
|
||||||
import signal
|
import signal
|
||||||
import inspect
|
import inspect
|
||||||
import tokenize
|
import tokenize
|
||||||
|
import functools
|
||||||
import traceback
|
import traceback
|
||||||
import linecache
|
import linecache
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
class Restart(Exception):
|
class Restart(Exception):
|
||||||
"""Causes a debugger to be restarted for the debugged python program."""
|
"""Causes a debugger to be restarted for the debugged python program."""
|
||||||
|
@ -128,6 +131,77 @@ class _rstr(str):
|
||||||
return 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
|
# Interaction prompt line will separate file and call info from code
|
||||||
# text using value of line_prefix string. A newline and arrow may
|
# 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
|
# be to your liking. You can set it once pdb is imported using the
|
||||||
|
@ -1538,49 +1612,26 @@ class Pdb(bdb.Bdb, cmd.Cmd):
|
||||||
return fullname
|
return fullname
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _runmodule(self, module_name):
|
def _run(self, target: Union[ModuleTarget, ScriptTarget]):
|
||||||
self._wait_for_mainpyfile = True
|
# When bdb sets tracing, a number of call and line events happen
|
||||||
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).
|
|
||||||
import __main__
|
|
||||||
__main__.__dict__.clear()
|
|
||||||
__main__.__dict__.update({"__name__" : "__main__",
|
|
||||||
"__file__" : filename,
|
|
||||||
"__builtins__": __builtins__,
|
|
||||||
})
|
|
||||||
|
|
||||||
# When bdb sets tracing, a number of call and line events happens
|
|
||||||
# BEFORE debugger even reaches user's code (and the exact sequence of
|
# BEFORE debugger even reaches user's code (and the exact sequence of
|
||||||
# events depends on python version). So we take special measures to
|
# events depends on python version). Take special measures to
|
||||||
# avoid stopping before we reach the main script (see user_line and
|
# avoid stopping before reaching the main script (see user_line and
|
||||||
# user_call for details).
|
# user_call for details).
|
||||||
self._wait_for_mainpyfile = True
|
self._wait_for_mainpyfile = True
|
||||||
self.mainpyfile = self.canonic(filename)
|
|
||||||
self._user_requested_quit = False
|
self._user_requested_quit = False
|
||||||
with io.open_code(filename) as fp:
|
|
||||||
statement = "exec(compile(%r, %r, 'exec'))" % \
|
self.mainpyfile = self.canonic(target.filename)
|
||||||
(fp.read(), self.mainpyfile)
|
|
||||||
self.run(statement)
|
# 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(target.namespace)
|
||||||
|
|
||||||
|
self.run(target.code)
|
||||||
|
|
||||||
|
|
||||||
# Collect all command help into docstring, if not run with -OO
|
# Collect all command help into docstring, if not run with -OO
|
||||||
|
|
||||||
|
@ -1669,6 +1720,7 @@ To let the script run until an exception occurs, use "-c continue".
|
||||||
To let the script run up to a given line X in the debugged file, use
|
To let the script run up to a given line X in the debugged file, use
|
||||||
"-c 'until X'"."""
|
"-c 'until X'"."""
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
import getopt
|
import getopt
|
||||||
|
|
||||||
|
@ -1678,29 +1730,20 @@ def main():
|
||||||
print(_usage)
|
print(_usage)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
commands = []
|
if any(opt in ['-h', '--help'] for opt, optarg in opts):
|
||||||
run_as_module = False
|
print(_usage)
|
||||||
for opt, optarg in opts:
|
sys.exit()
|
||||||
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
|
commands = [optarg for opt, optarg in opts if opt in ['-c', '--command']]
|
||||||
if not run_as_module and not os.path.exists(mainpyfile):
|
|
||||||
print('Error:', mainpyfile, 'does not exist')
|
module_indicated = any(opt in ['-m'] for opt, optarg in opts)
|
||||||
sys.exit(1)
|
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
|
sys.argv[:] = args # Hide "pdb.py" and pdb options from argument list
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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
|
||||||
# changed by the user from the command line. There is a "restart" command
|
# changed by the user from the command line. There is a "restart" command
|
||||||
|
@ -1709,15 +1752,12 @@ def main():
|
||||||
pdb.rcLines.extend(commands)
|
pdb.rcLines.extend(commands)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if run_as_module:
|
pdb._run(target)
|
||||||
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")
|
||||||
except Restart:
|
except Restart:
|
||||||
print("Restarting", mainpyfile, "with arguments:")
|
print("Restarting", target, "with arguments:")
|
||||||
print("\t" + " ".join(sys.argv[1:]))
|
print("\t" + " ".join(sys.argv[1:]))
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
# In most cases SystemExit does not warrant a post-mortem session.
|
# 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")
|
print("Running 'cont' or 'step' will restart the program")
|
||||||
t = sys.exc_info()[2]
|
t = sys.exc_info()[2]
|
||||||
pdb.interaction(None, t)
|
pdb.interaction(None, t)
|
||||||
print("Post mortem debugger finished. The " + mainpyfile +
|
print("Post mortem debugger finished. The " + target +
|
||||||
" will be restarted")
|
" will be restarted")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,7 @@ def test_pdb_breakpoints_preserved_across_interactive_sessions():
|
||||||
1 breakpoint keep yes at ...test_pdb.py:...
|
1 breakpoint keep yes at ...test_pdb.py:...
|
||||||
2 breakpoint keep yes at ...test_pdb.py:...
|
2 breakpoint keep yes at ...test_pdb.py:...
|
||||||
(Pdb) break pdb.find_function
|
(Pdb) break pdb.find_function
|
||||||
Breakpoint 3 at ...pdb.py:94
|
Breakpoint 3 at ...pdb.py:97
|
||||||
(Pdb) break
|
(Pdb) break
|
||||||
Num Type Disp Enb Where
|
Num Type Disp Enb Where
|
||||||
1 breakpoint keep yes at ...test_pdb.py:...
|
1 breakpoint keep yes at ...test_pdb.py:...
|
||||||
|
|
|
@ -222,7 +222,11 @@ class PyclbrTest(TestCase):
|
||||||
cm('pickle', ignore=('partial', 'PickleBuffer'))
|
cm('pickle', ignore=('partial', 'PickleBuffer'))
|
||||||
cm('aifc', ignore=('_aifc_params',)) # set with = in module
|
cm('aifc', ignore=('_aifc_params',)) # set with = in module
|
||||||
cm('sre_parse', ignore=('dump', 'groups', 'pos')) # from sre_constants import *; property
|
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
|
cm('pydoc', ignore=('input', 'output',)) # properties
|
||||||
|
|
||||||
# Tests for modules inside packages
|
# Tests for modules inside packages
|
||||||
|
|
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue