diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index a572ff4b9e91cc..a93557195fb8e4 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -490,6 +490,30 @@ def test_teleport(self): self.assertTrue(tpen.isdown()) +class TestTurtleScreen(unittest.TestCase): + 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): with patch_screen(): diff --git a/Lib/turtle.py b/Lib/turtle.py index a94ec10ea7460d..173cfc50b579da 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -483,6 +483,7 @@ def __init__(self, cv): 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) @@ -552,7 +553,16 @@ def _delete(self, item): 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 index 00000000000000..96a0938c02d226 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst @@ -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.