]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-59013: Set breakpoint on the first executable line of function when using `break...
authorTian Gao <gaogaotiantian@hotmail.com>
Wed, 31 Jan 2024 13:03:05 +0000 (05:03 -0800)
committerGitHub <noreply@github.com>
Wed, 31 Jan 2024 13:03:05 +0000 (13:03 +0000)
Lib/pdb.py
Lib/test/test_pdb.py
Misc/NEWS.d/next/Library/2023-11-27-19-54-43.gh-issue-59013.chpQ0e.rst [new file with mode: 0644]

index 6f7719eb9ba6c5b69cdea9b74d8d5f43f0dbfc3c..0754e8b628cf570ffb12134c49f9f6a936edb9f0 100755 (executable)
@@ -97,17 +97,47 @@ class Restart(Exception):
 __all__ = ["run", "pm", "Pdb", "runeval", "runctx", "runcall", "set_trace",
            "post_mortem", "help"]
 
+
+def find_first_executable_line(code):
+    """ Try to find the first executable line of the code object.
+
+    Equivalently, find the line number of the instruction that's
+    after RESUME
+
+    Return code.co_firstlineno if no executable line is found.
+    """
+    prev = None
+    for instr in dis.get_instructions(code):
+        if prev is not None and prev.opname == 'RESUME':
+            if instr.positions.lineno is not None:
+                return instr.positions.lineno
+            return code.co_firstlineno
+        prev = instr
+    return code.co_firstlineno
+
 def find_function(funcname, filename):
     cre = re.compile(r'def\s+%s\s*[(]' % re.escape(funcname))
     try:
         fp = tokenize.open(filename)
     except OSError:
         return None
+    funcdef = ""
+    funcstart = None
     # consumer of this info expects the first line to be 1
     with fp:
         for lineno, line in enumerate(fp, start=1):
             if cre.match(line):
-                return funcname, filename, lineno
+                funcstart, funcdef = lineno, line
+            elif funcdef:
+                funcdef += line
+
+            if funcdef:
+                try:
+                    funccode = compile(funcdef, filename, 'exec').co_consts[0]
+                except SyntaxError:
+                    continue
+                lineno_offset = find_first_executable_line(funccode)
+                return funcname, filename, funcstart + lineno_offset - 1
     return None
 
 def lasti2lineno(code, lasti):
@@ -975,7 +1005,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
                     #use co_name to identify the bkpt (function names
                     #could be aliased, but co_name is invariant)
                     funcname = code.co_name
-                    lineno = self._find_first_executable_line(code)
+                    lineno = find_first_executable_line(code)
                     filename = code.co_filename
                 except:
                     # last thing to try
@@ -1078,23 +1108,6 @@ class Pdb(bdb.Bdb, cmd.Cmd):
             return 0
         return lineno
 
-    def _find_first_executable_line(self, code):
-        """ Try to find the first executable line of the code object.
-
-        Equivalently, find the line number of the instruction that's
-        after RESUME
-
-        Return code.co_firstlineno if no executable line is found.
-        """
-        prev = None
-        for instr in dis.get_instructions(code):
-            if prev is not None and prev.opname == 'RESUME':
-                if instr.positions.lineno is not None:
-                    return instr.positions.lineno
-                return code.co_firstlineno
-            prev = instr
-        return code.co_firstlineno
-
     def do_enable(self, arg):
         """enable bpnumber [bpnumber ...]
 
index c64df62c761471d73ae35f5b3471c42b75a7d30a..b2283cff6cb4622bdba27cd3109299675157af70 100644 (file)
@@ -2661,7 +2661,7 @@ def quux():
     pass
 """.encode(),
             'bœr',
-            ('bœr', 4),
+            ('bœr', 5),
         )
 
     def test_find_function_found_with_encoding_cookie(self):
@@ -2678,7 +2678,7 @@ def quux():
     pass
 """.encode('iso-8859-15'),
             'bœr',
-            ('bœr', 5),
+            ('bœr', 6),
         )
 
     def test_find_function_found_with_bom(self):
@@ -2688,9 +2688,34 @@ def bœr():
     pass
 """.encode(),
             'bœr',
-            ('bœr', 1),
+            ('bœr', 2),
         )
 
+    def test_find_function_first_executable_line(self):
+        code = textwrap.dedent("""\
+            def foo(): pass
+
+            def bar():
+                pass  # line 4
+
+            def baz():
+                # comment
+                pass  # line 8
+
+            def mul():
+                # code on multiple lines
+                code = compile(   # line 12
+                    'def f()',
+                    '<string>',
+                    'exec',
+                )
+        """).encode()
+
+        self._assert_find_function(code, 'foo', ('foo', 1))
+        self._assert_find_function(code, 'bar', ('bar', 4))
+        self._assert_find_function(code, 'baz', ('baz', 8))
+        self._assert_find_function(code, 'mul', ('mul', 12))
+
     def test_issue7964(self):
         # open the file as binary so we can force \r\n newline
         with open(os_helper.TESTFN, 'wb') as f:
diff --git a/Misc/NEWS.d/next/Library/2023-11-27-19-54-43.gh-issue-59013.chpQ0e.rst b/Misc/NEWS.d/next/Library/2023-11-27-19-54-43.gh-issue-59013.chpQ0e.rst
new file mode 100644 (file)
index 0000000..a2be2fb
--- /dev/null
@@ -0,0 +1 @@
+Set breakpoint on the first executable line of the function, instead of the line of function definition when the user do ``break func`` using :mod:`pdb`