]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-111051: Check if file is modifed during debugging in `pdb` (#111052)
authorTian Gao <gaogaotiantian@hotmail.com>
Thu, 25 Jan 2024 16:48:50 +0000 (08:48 -0800)
committerGitHub <noreply@github.com>
Thu, 25 Jan 2024 16:48:50 +0000 (16:48 +0000)
Lib/pdb.py
Lib/test/test_pdb.py
Misc/NEWS.d/next/Library/2023-10-19-02-08-12.gh-issue-111051.8h1Dpk.rst [new file with mode: 0644]

index 68f810620f882634071d11a77b05ec401d993ee3..6f7719eb9ba6c5b69cdea9b74d8d5f43f0dbfc3c 100755 (executable)
@@ -233,6 +233,8 @@ class Pdb(bdb.Bdb, cmd.Cmd):
     # but in case there are recursions, we stop at 999.
     MAX_CHAINED_EXCEPTION_DEPTH = 999
 
+    _file_mtime_table = {}
+
     def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
                  nosigint=False, readrc=True):
         bdb.Bdb.__init__(self, skip=skip)
@@ -437,6 +439,20 @@ class Pdb(bdb.Bdb, cmd.Cmd):
             except KeyboardInterrupt:
                 self.message('--KeyboardInterrupt--')
 
+    def _validate_file_mtime(self):
+        """Check if the source file of the current frame has been modified since
+        the last time we saw it. If so, give a warning."""
+        try:
+            filename = self.curframe.f_code.co_filename
+            mtime = os.path.getmtime(filename)
+        except Exception:
+            return
+        if (filename in self._file_mtime_table and
+            mtime != self._file_mtime_table[filename]):
+            self.message(f"*** WARNING: file '{filename}' was edited, "
+                         "running stale code until the program is rerun")
+        self._file_mtime_table[filename] = mtime
+
     # Called before loop, handles display expressions
     # Set up convenience variable containers
     def preloop(self):
@@ -681,6 +697,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
         a breakpoint command list definition.
         """
         if not self.commands_defining:
+            self._validate_file_mtime()
             return cmd.Cmd.onecmd(self, line)
         else:
             return self.handle_command_def(line)
@@ -2021,6 +2038,10 @@ class Pdb(bdb.Bdb, cmd.Cmd):
         __main__.__dict__.clear()
         __main__.__dict__.update(target.namespace)
 
+        # Clear the mtime table for program reruns, assume all the files
+        # are up to date.
+        self._file_mtime_table.clear()
+
         self.run(target.code)
 
     def _format_exc(self, exc: BaseException):
index 03487aa6ffd81f4f2d668980ac8bf485999ff05b..c64df62c761471d73ae35f5b3471c42b75a7d30a 100644 (file)
@@ -3056,6 +3056,87 @@ def bœr():
         self.assertTrue(any("__main__.py(4)<module>()"
                             in l for l in stdout.splitlines()), stdout)
 
+    def test_file_modified_after_execution(self):
+        script = """
+            print("hello")
+        """
+
+        commands = """
+            filename = $_frame.f_code.co_filename
+            f = open(filename, "w")
+            f.write("print('goodbye')")
+            f.close()
+            ll
+        """
+
+        stdout, stderr = self.run_pdb_script(script, commands)
+        self.assertIn("WARNING:", stdout)
+        self.assertIn("was edited", stdout)
+
+    def test_file_modified_after_execution_with_multiple_instances(self):
+        script = """
+            import pdb; pdb.Pdb().set_trace()
+            with open(__file__, "w") as f:
+                f.write("print('goodbye')\\n" * 5)
+            import pdb; pdb.Pdb().set_trace()
+        """
+
+        commands = """
+            continue
+            continue
+        """
+
+        filename = 'main.py'
+        with open(filename, 'w') as f:
+            f.write(textwrap.dedent(script))
+        self.addCleanup(os_helper.unlink, filename)
+        self.addCleanup(os_helper.rmtree, '__pycache__')
+        cmd = [sys.executable, filename]
+        with subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stdin=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                env = {**os.environ, 'PYTHONIOENCODING': 'utf-8'},
+        ) as proc:
+            stdout, _ = proc.communicate(str.encode(commands))
+        stdout = stdout and bytes.decode(stdout)
+
+        self.assertEqual(proc.returncode, 0)
+        self.assertIn("WARNING:", stdout)
+        self.assertIn("was edited", stdout)
+
+    def test_file_modified_after_execution_with_restart(self):
+        script = """
+            import random
+            # Any code with a source to step into so this script is not checked
+            # for changes when it's being changed
+            random.randint(1, 4)
+            print("hello")
+        """
+
+        commands = """
+            ll
+            n
+            s
+            filename = $_frame.f_back.f_code.co_filename
+            def change_file(content, filename):
+                with open(filename, "w") as f:
+                    f.write(f"print({content})")
+
+            change_file('world', filename)
+            restart
+            ll
+        """
+
+        stdout, stderr = self.run_pdb_script(script, commands)
+        # Make sure the code is running correctly and the file is edited
+        self.assertIn("hello", stdout)
+        self.assertIn("world", stdout)
+        # The file was edited, but restart should clear the state and consider
+        # the file as up to date
+        self.assertNotIn("WARNING:", stdout)
+
     def test_relative_imports(self):
         self.module_name = 't_main'
         os_helper.rmtree(self.module_name)
diff --git a/Misc/NEWS.d/next/Library/2023-10-19-02-08-12.gh-issue-111051.8h1Dpk.rst b/Misc/NEWS.d/next/Library/2023-10-19-02-08-12.gh-issue-111051.8h1Dpk.rst
new file mode 100644 (file)
index 0000000..adb3241
--- /dev/null
@@ -0,0 +1 @@
+Added check for file modification during debugging with :mod:`pdb`