Skip to content

Commit 6921cca

Browse files
[3.13] gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626) (GH-152667)
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(). (cherry picked from commit 6f103fa) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 218d26d commit 6921cca

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

Lib/test/test_turtle.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,30 @@ def test_teleport(self):
490490
self.assertTrue(tpen.isdown())
491491

492492

493+
class TestTurtleScreen(unittest.TestCase):
494+
def test_update_is_not_reentrant(self):
495+
# ondrag(goto) reenters _update() while cv.update() processes events;
496+
# without a guard this recurses without bound (gh-50966).
497+
s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
498+
depth = max_depth = 0
499+
500+
def reenter():
501+
nonlocal depth, max_depth
502+
depth += 1
503+
max_depth = max(max_depth, depth)
504+
if depth < 50:
505+
s._update() # as an event handler would
506+
depth -= 1
507+
508+
s.cv.update.reset_mock() # ignore calls made during construction
509+
s.cv.update.side_effect = reenter
510+
s._update()
511+
# cv.update() runs once; reentrant calls only flush idle tasks.
512+
self.assertEqual(s.cv.update.call_count, 1)
513+
self.assertEqual(max_depth, 1)
514+
self.assertTrue(s.cv.update_idletasks.called)
515+
516+
493517
class TestTurtle(unittest.TestCase):
494518
def setUp(self):
495519
with patch_screen():

Lib/turtle.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ def __init__(self, cv):
483483
self.canvwidth = w
484484
self.canvheight = h
485485
self.xscale = self.yscale = 1.0
486+
self._updating = False
486487

487488
def _createpoly(self):
488489
"""Create an invisible polygon item on canvas self.cv)
@@ -552,7 +553,16 @@ def _delete(self, item):
552553
def _update(self):
553554
"""Redraw graphics items on canvas
554555
"""
555-
self.cv.update()
556+
if self._updating:
557+
# Reentrant call (e.g. a drag handler moving the turtle,
558+
# gh-50966): flush drawing without reprocessing input.
559+
self.cv.update_idletasks()
560+
return
561+
self._updating = True
562+
try:
563+
self.cv.update()
564+
finally:
565+
self._updating = False
556566

557567
def _delay(self, delay):
558568
"""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)