]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626)
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 30 Jun 2026 09:19:43 +0000 (12:19 +0300)
committerGitHub <noreply@github.com>
Tue, 30 Jun 2026 09:19:43 +0000 (12:19 +0300)
TurtleScreenBase._update() redraws with cv.update(), which also reprocesses
input events, so a handler that moves the turtle (such as
screen.ondrag(turtle.goto)) reenters _update() for every queued event until
the interpreter crashes.  A reentrant _update() now only flushes drawing with
update_idletasks().

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Lib/test/test_turtle.py
Lib/turtle.py
Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst [new file with mode: 0644]

index 12d2eed874148cebaa8a31a551f38a4d50d47027..c49ce9cdb6f6d955d0f8217d1d77203bc65a4dd7 100644 (file)
@@ -577,6 +577,28 @@ class TestTurtleScreen(unittest.TestCase):
             s.update.assert_not_called()
         s.update.assert_called_once()
 
+    def test_update_is_not_reentrant(self):
+        # ondrag(goto) reenters _update() while cv.update() processes events;
+        # without a guard this recurses without bound (gh-50966).
+        s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
+        depth = max_depth = 0
+
+        def reenter():
+            nonlocal depth, max_depth
+            depth += 1
+            max_depth = max(max_depth, depth)
+            if depth < 50:
+                s._update()  # as an event handler would
+            depth -= 1
+
+        s.cv.update.reset_mock()  # ignore calls made during construction
+        s.cv.update.side_effect = reenter
+        s._update()
+        # cv.update() runs once; reentrant calls only flush idle tasks.
+        self.assertEqual(s.cv.update.call_count, 1)
+        self.assertEqual(max_depth, 1)
+        self.assertTrue(s.cv.update_idletasks.called)
+
 
 class TestTurtle(unittest.TestCase):
     def setUp(self):
index ae5be9fe7039d5eb09d89a5992f019057f089552..af2c7448a0af17f0f3c4976539c23c4ccf9488f9 100644 (file)
@@ -486,6 +486,7 @@ class TurtleScreenBase(object):
         self.canvwidth = w
         self.canvheight = h
         self.xscale = self.yscale = 1.0
+        self._updating = False
 
     def _createpoly(self):
         """Create an invisible polygon item on canvas self.cv)
@@ -555,7 +556,16 @@ class TurtleScreenBase(object):
     def _update(self):
         """Redraw graphics items on canvas
         """
-        self.cv.update()
+        if self._updating:
+            # Reentrant call (e.g. a drag handler moving the turtle,
+            # gh-50966): flush drawing without reprocessing input.
+            self.cv.update_idletasks()
+            return
+        self._updating = True
+        try:
+            self.cv.update()
+        finally:
+            self._updating = False
 
     def _delay(self, delay):
         """Delay subsequent canvas actions for delay ms."""
diff --git a/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst
new file mode 100644 (file)
index 0000000..96a0938
--- /dev/null
@@ -0,0 +1,3 @@
+Fix unbounded recursion in :mod:`turtle` when a mouse event handler that moves
+the turtle is reentered while the screen is being redrawn, for example with
+``screen.ondrag(turtle.goto)``.  This could previously crash the interpreter.