Skip to content

Commit 65ec43d

Browse files
[3.14] gh-158563: Fix various bugs in turtle's undo handling (GH-158564) (#158610)
(cherry picked from commit 9d3b0b8) Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent 874218f commit 65ec43d

3 files changed

Lines changed: 105 additions & 62 deletions

File tree

‎Lib/test/test_turtle.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,34 @@ def test_dot_signature(self):
693693
self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, (0, 257, 0))
694694
self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, 0, 257, 0)
695695

696+
def test_circle_undo(self):
697+
self.turtle.circle(50, 90)
698+
self.turtle.undo()
699+
self.assertEqual(self.turtle.pos(), (0, 0))
700+
self.assertEqual(self.turtle.undobufferentries(), 0)
701+
702+
def test_undo_sequence_resets_after_exception(self):
703+
with unittest.mock.patch.object(self.turtle, "_write",
704+
side_effect=ValueError):
705+
self.assertRaises(ValueError, self.turtle.write, "spam")
706+
self.assertFalse(self.turtle.undobuffer.cumulate)
707+
708+
def test_nested_undo_sequence(self):
709+
with self.turtle._undo_sequence():
710+
self.turtle.teleport(10, 20)
711+
self.turtle.forward(10)
712+
self.assertEqual(self.turtle.undobufferentries(), 1)
713+
self.turtle.undo()
714+
self.assertEqual(self.turtle.pos(), (0, 0))
715+
716+
def test_stamp_without_undobuffer(self):
717+
shape = turtle.Shape("polygon", ((0, 0), (5, 9), (-5, 9)))
718+
self.turtle.screen._shapes = {self.turtle.shape(): shape}
719+
self.turtle.setundobuffer(None)
720+
stamp = self.turtle.stamp()
721+
self.turtle.clearstamp(stamp)
722+
self.assertEqual(self.turtle.stampItems, [])
723+
696724
class TestModuleLevel(unittest.TestCase):
697725
def test_all_signatures(self):
698726
import inspect

‎Lib/turtle.py‎

Lines changed: 73 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,15 @@ def pop(self):
896896
self.ptr = (self.ptr - 1) % self.bufsize
897897
return (item)
898898

899+
def remove(self, item):
900+
if item not in self.buffer:
901+
return
902+
index = self.buffer.index(item)
903+
self.buffer.remove(item)
904+
if index <= self.ptr:
905+
self.ptr = (self.ptr - 1) % self.bufsize
906+
self.buffer.insert((self.ptr+1) % self.bufsize, [None])
907+
899908
def nr_of_items(self):
900909
return self.bufsize - self.buffer.count([None])
901910

@@ -1642,6 +1651,20 @@ def _goto(self, end):
16421651
"""Move the turtle to the end position."""
16431652
self._position = end
16441653

1654+
@contextmanager
1655+
def _undo_sequence(self):
1656+
"""Record the enclosed actions as a single undo step."""
1657+
undobuffer = self.undobuffer
1658+
if not undobuffer or undobuffer.cumulate:
1659+
yield
1660+
return
1661+
undobuffer.push(["seq"])
1662+
undobuffer.cumulate = True
1663+
try:
1664+
yield
1665+
finally:
1666+
undobuffer.cumulate = False
1667+
16451668
def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
16461669
"""To be overwritten by child class RawTurtle.
16471670
Includes no TPen references."""
@@ -1984,38 +2007,34 @@ def circle(self, radius, extent = None, steps = None):
19842007
>>> turtle.circle(50)
19852008
>>> turtle.circle(120, 180) # draw a semicircle
19862009
"""
1987-
if self.undobuffer:
1988-
self.undobuffer.push(["seq"])
1989-
self.undobuffer.cumulate = True
19902010
speed = self.speed()
19912011
if extent is None:
19922012
extent = self._fullcircle
19932013
if steps is None:
19942014
frac = abs(extent)/self._fullcircle
1995-
steps = 1+int(min(11+abs(radius)/6.0, 59.0)*frac)
2015+
steps = 1 + int(min(11 + abs(radius) / 6.0, 59.0) * frac)
19962016
w = 1.0 * extent / steps
19972017
w2 = 0.5 * w
19982018
l = 2.0 * radius * math.sin(math.radians(w2)*self._degreesPerAU)
19992019
if radius < 0:
20002020
l, w, w2 = -l, -w, -w2
20012021
tr = self._tracer()
20022022
dl = self._delay()
2003-
if speed == 0:
2004-
self._tracer(0, 0)
2005-
else:
2006-
self.speed(0)
2007-
self._rotate(w2)
2008-
for i in range(steps):
2023+
with self._undo_sequence():
2024+
if speed == 0:
2025+
self._tracer(0, 0)
2026+
else:
2027+
self.speed(0)
2028+
self._rotate(w2)
2029+
for i in range(steps):
2030+
self.speed(speed)
2031+
self._go(l)
2032+
self.speed(0)
2033+
self._rotate(w)
2034+
self._rotate(-w2)
2035+
if speed == 0:
2036+
self._tracer(tr, dl)
20092037
self.speed(speed)
2010-
self._go(l)
2011-
self.speed(0)
2012-
self._rotate(w)
2013-
self._rotate(-w2)
2014-
if speed == 0:
2015-
self._tracer(tr, dl)
2016-
self.speed(speed)
2017-
if self.undobuffer:
2018-
self.undobuffer.cumulate = False
20192038

20202039
# Three dummy methods to be implemented by the child class:
20212040

@@ -2786,16 +2805,19 @@ def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
27862805
"""
27872806
pendown = self.isdown()
27882807
was_filling = self.filling()
2789-
if pendown:
2790-
self.pen(pendown=False)
2791-
if was_filling and not fill_gap:
2792-
self.end_fill()
2793-
new_x = x if x is not None else self._position[0]
2794-
new_y = y if y is not None else self._position[1]
2795-
self._position = Vec2D(new_x, new_y)
2796-
self.pen(pendown=pendown)
2797-
if was_filling and not fill_gap:
2798-
self.begin_fill()
2808+
with self._undo_sequence():
2809+
if pendown:
2810+
self.pen(pendown=False)
2811+
if was_filling and not fill_gap:
2812+
self.end_fill()
2813+
new_x = x if x is not None else self._position[0]
2814+
new_y = y if y is not None else self._position[1]
2815+
if self.undobuffer:
2816+
self.undobuffer.push(("teleport", self._position))
2817+
self._position = Vec2D(new_x, new_y)
2818+
self.pen(pendown=pendown)
2819+
if was_filling and not fill_gap:
2820+
self.begin_fill()
27992821

28002822
def clone(self):
28012823
"""Create and return a clone of the turtle.
@@ -3146,7 +3168,8 @@ def stamp(self):
31463168
screen._drawpoly(item, poly, fill=self._cc(fc),
31473169
outline=self._cc(oc), width=self._outlinewidth, top=True)
31483170
self.stampItems.append(stitem)
3149-
self.undobuffer.push(("stamp", stitem))
3171+
if self.undobuffer:
3172+
self.undobuffer.push(("stamp", stitem))
31503173
return stitem
31513174

31523175
def _clearstamp(self, stampid):
@@ -3161,15 +3184,8 @@ def _clearstamp(self, stampid):
31613184
self.stampItems.remove(stampid)
31623185
# Delete stampitem from undobuffer if necessary
31633186
# if clearstamp is called directly.
3164-
item = ("stamp", stampid)
3165-
buf = self.undobuffer
3166-
if item not in buf.buffer:
3167-
return
3168-
index = buf.buffer.index(item)
3169-
buf.buffer.remove(item)
3170-
if index <= buf.ptr:
3171-
buf.ptr = (buf.ptr - 1) % buf.bufsize
3172-
buf.buffer.insert((buf.ptr+1)%buf.bufsize, [None])
3187+
if self.undobuffer:
3188+
self.undobuffer.remove(("stamp", stampid))
31733189

31743190
def clearstamp(self, stampid):
31753191
"""Delete stamp with given stampid
@@ -3467,20 +3483,16 @@ def dot(self, size=None, *color):
34673483
color = self._colorstr(color)
34683484
# If screen were to gain a dot function, see GH #104218.
34693485
pen = self.pen()
3470-
if self.undobuffer:
3471-
self.undobuffer.push(["seq"])
3472-
self.undobuffer.cumulate = True
3473-
try:
3474-
if self.resizemode() == 'auto':
3475-
self.ht()
3476-
self.pendown()
3477-
self.pensize(size)
3478-
self.pencolor(color)
3479-
self.forward(0)
3480-
finally:
3481-
self.pen(pen)
3482-
if self.undobuffer:
3483-
self.undobuffer.cumulate = False
3486+
with self._undo_sequence():
3487+
try:
3488+
if self.resizemode() == 'auto':
3489+
self.ht()
3490+
self.pendown()
3491+
self.pensize(size)
3492+
self.pencolor(color)
3493+
self.forward(0)
3494+
finally:
3495+
self.pen(pen)
34843496

34853497
def _write(self, txt, align, font):
34863498
"""Performs the writing for write()
@@ -3512,15 +3524,11 @@ def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")):
35123524
>>> turtle.write('Home = ', True, align="center")
35133525
>>> turtle.write((0,0), True)
35143526
"""
3515-
if self.undobuffer:
3516-
self.undobuffer.push(["seq"])
3517-
self.undobuffer.cumulate = True
3518-
end = self._write(str(arg), align.lower(), font)
3519-
if move:
3520-
x, y = self.pos()
3521-
self.setpos(end, y)
3522-
if self.undobuffer:
3523-
self.undobuffer.cumulate = False
3527+
with self._undo_sequence():
3528+
end = self._write(str(arg), align.lower(), font)
3529+
if move:
3530+
x, y = self.pos()
3531+
self.setpos(end, y)
35243532

35253533
@contextmanager
35263534
def poly(self):
@@ -3708,6 +3716,9 @@ def _undo(self, action, data):
37083716
self.clearstamp(stitem)
37093717
elif action == "go":
37103718
self._undogoto(data)
3719+
elif action == "teleport":
3720+
self._position = data[0]
3721+
self._update()
37113722
elif action in ["wri", "dot"]:
37123723
item = data[0]
37133724
self.screen._delete(item)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`turtle.undo` after :func:`turtle.teleport` and after an exception
2+
in :func:`turtle.circle`, :func:`turtle.dot` or :func:`turtle.write`. Fix
3+
:func:`turtle.stamp`, :func:`turtle.clearstamp`, :func:`turtle.clear` and
4+
:func:`turtle.reset` when the undo buffer is disabled.

0 commit comments

Comments
 (0)