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):
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)
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."""
--- /dev/null
+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.