Skip to content

Commit 6f103fa

Browse files
gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626)
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>
1 parent 11d42bd commit 6f103fa

3 files changed

Lines changed: 36 additions & 1 deletion

File tree

Lib/test/test_turtle.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,28 @@ def test_no_animation_calls_update_at_exit(self):
577577
s.update.assert_not_called()
578578
s.update.assert_called_once()
579579

580+
def test_update_is_not_reentrant(self):
581+
# ondrag(goto) reenters _update() while cv.update() processes events;
582+
# without a guard this recurses without bound (gh-50966).
583+
s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
584+
depth = max_depth = 0
585+
586+
def reenter():
587+
nonlocal depth, max_depth
588+
depth += 1
589+
max_depth = max(max_depth, depth)
590+
if depth < 50:
591+
s._update() # as an event handler would
592+
depth -= 1
593+
594+
s.cv.update.reset_mock() # ignore calls made during construction
595+
s.cv.update.side_effect = reenter
596+
s._update()
597+
# cv.update() runs once; reentrant calls only flush idle tasks.
598+
self.assertEqual(s.cv.update.call_count, 1)
599+
self.assertEqual(max_depth, 1)
600+
self.assertTrue(s.cv.update_idletasks.called)
601+
580602

581603
class TestTurtle(unittest.TestCase):
582604
def setUp(self):

Lib/turtle.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ def __init__(self, cv):
486486
self.canvwidth = w
487487
self.canvheight = h
488488
self.xscale = self.yscale = 1.0
489+
self._updating = False
489490

490491
def _createpoly(self):
491492
"""Create an invisible polygon item on canvas self.cv)
@@ -555,7 +556,16 @@ def _delete(self, item):
555556
def _update(self):
556557
"""Redraw graphics items on canvas
557558
"""
558-
self.cv.update()
559+
if self._updating:
560+
# Reentrant call (e.g. a drag handler moving the turtle,
561+
# gh-50966): flush drawing without reprocessing input.
562+
self.cv.update_idletasks()
563+
return
564+
self._updating = True
565+
try:
566+
self.cv.update()
567+
finally:
568+
self._updating = False
559569

560570
def _delay(self, delay):
561571
"""Delay subsequent canvas actions for delay ms."""
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix unbounded recursion in :mod:`turtle` when a mouse event handler that moves
2+
the turtle is reentered while the screen is being redrawn, for example with
3+
``screen.ondrag(turtle.goto)``. This could previously crash the interpreter.

0 commit comments

Comments
 (0)