Skip to content

Commit 2aa8cbf

Browse files
Fix various bugs in turtle's undo handling
1 parent eb30e3d commit 2aa8cbf

3 files changed

Lines changed: 96 additions & 62 deletions

File tree

‎Lib/test/test_turtle.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,26 @@ def test_dot_signature(self):
694694
self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, (0, 257, 0))
695695
self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, 0, 257, 0)
696696

697+
def test_circle_undo(self):
698+
self.turtle.circle(50, 90)
699+
self.turtle.undo()
700+
self.assertEqual(self.turtle.pos(), (0, 0))
701+
self.assertEqual(self.turtle.undobufferentries(), 0)
702+
703+
def test_undo_sequence_resets_after_exception(self):
704+
with unittest.mock.patch.object(self.turtle, "_write",
705+
side_effect=ValueError):
706+
self.assertRaises(ValueError, self.turtle.write, "spam")
707+
self.assertFalse(self.turtle.undobuffer.cumulate)
708+
709+
def test_stamp_without_undobuffer(self):
710+
shape = turtle.Shape("polygon", ((0, 0), (5, 9), (-5, 9)))
711+
self.turtle.screen._shapes = {self.turtle.shape(): shape}
712+
self.turtle.setundobuffer(None)
713+
stamp = self.turtle.stamp()
714+
self.turtle.clearstamp(stamp)
715+
self.assertEqual(self.turtle.stampItems, [])
716+
697717
class TestModuleLevel(unittest.TestCase):
698718
def test_all_signatures(self):
699719
import inspect

‎Lib/turtle.py‎

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

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

@@ -1643,6 +1652,19 @@ def _goto(self, end):
16431652
"""Move the turtle to the end position."""
16441653
self._position = end
16451654

1655+
@contextmanager
1656+
def _undo_sequence(self):
1657+
"""Record the enclosed actions as a single undo step."""
1658+
undobuffer = self.undobuffer
1659+
if undobuffer:
1660+
undobuffer.push(["seq"])
1661+
undobuffer.cumulate = True
1662+
try:
1663+
yield
1664+
finally:
1665+
if undobuffer:
1666+
undobuffer.cumulate = False
1667+
16461668
def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
16471669
"""To be overwritten by child class RawTurtle.
16481670
Includes no TPen references."""
@@ -1985,38 +2007,34 @@ def circle(self, radius, extent = None, steps = None):
19852007
>>> turtle.circle(50)
19862008
>>> turtle.circle(120, 180) # draw a semicircle
19872009
"""
1988-
if self.undobuffer:
1989-
self.undobuffer.push(["seq"])
1990-
self.undobuffer.cumulate = True
19912010
speed = self.speed()
19922011
if extent is None:
19932012
extent = self._fullcircle
19942013
if steps is None:
19952014
frac = abs(extent)/self._fullcircle
1996-
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)
19972016
w = 1.0 * extent / steps
19982017
w2 = 0.5 * w
19992018
l = 2.0 * radius * math.sin(math.radians(w2)*self._degreesPerAU)
20002019
if radius < 0:
20012020
l, w, w2 = -l, -w, -w2
20022021
tr = self._tracer()
20032022
dl = self._delay()
2004-
if speed == 0:
2005-
self._tracer(0, 0)
2006-
else:
2007-
self.speed(0)
2008-
self._rotate(w2)
2009-
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)
20102037
self.speed(speed)
2011-
self._go(l)
2012-
self.speed(0)
2013-
self._rotate(w)
2014-
self._rotate(-w2)
2015-
if speed == 0:
2016-
self._tracer(tr, dl)
2017-
self.speed(speed)
2018-
if self.undobuffer:
2019-
self.undobuffer.cumulate = False
20202038

20212039
# Three dummy methods to be implemented by the child class:
20222040

@@ -2787,16 +2805,19 @@ def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
27872805
"""
27882806
pendown = self.isdown()
27892807
was_filling = self.filling()
2790-
if pendown:
2791-
self.pen(pendown=False)
2792-
if was_filling and not fill_gap:
2793-
self.end_fill()
2794-
new_x = x if x is not None else self._position[0]
2795-
new_y = y if y is not None else self._position[1]
2796-
self._position = Vec2D(new_x, new_y)
2797-
self.pen(pendown=pendown)
2798-
if was_filling and not fill_gap:
2799-
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()
28002821

28012822
def clone(self):
28022823
"""Create and return a clone of the turtle.
@@ -3147,7 +3168,8 @@ def stamp(self):
31473168
screen._drawpoly(item, poly, fill=self._cc(fc),
31483169
outline=self._cc(oc), width=self._outlinewidth, top=True)
31493170
self.stampItems.append(stitem)
3150-
self.undobuffer.push(("stamp", stitem))
3171+
if self.undobuffer:
3172+
self.undobuffer.push(("stamp", stitem))
31513173
return stitem
31523174

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

31753190
def clearstamp(self, stampid):
31763191
"""Delete stamp with given stampid
@@ -3468,20 +3483,16 @@ def dot(self, size=None, *color):
34683483
color = self._colorstr(color)
34693484
# If screen were to gain a dot function, see GH #104218.
34703485
pen = self.pen()
3471-
if self.undobuffer:
3472-
self.undobuffer.push(["seq"])
3473-
self.undobuffer.cumulate = True
3474-
try:
3475-
if self.resizemode() == 'auto':
3476-
self.ht()
3477-
self.pendown()
3478-
self.pensize(size)
3479-
self.pencolor(color)
3480-
self.forward(0)
3481-
finally:
3482-
self.pen(pen)
3483-
if self.undobuffer:
3484-
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)
34853496

34863497
def _write(self, txt, align, font):
34873498
"""Performs the writing for write()
@@ -3513,15 +3524,11 @@ def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")):
35133524
>>> turtle.write('Home = ', True, align="center")
35143525
>>> turtle.write((0,0), True)
35153526
"""
3516-
if self.undobuffer:
3517-
self.undobuffer.push(["seq"])
3518-
self.undobuffer.cumulate = True
3519-
end = self._write(str(arg), align.lower(), font)
3520-
if move:
3521-
x, y = self.pos()
3522-
self.setpos(end, y)
3523-
if self.undobuffer:
3524-
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)
35253532

35263533
@contextmanager
35273534
def poly(self):
@@ -3709,6 +3716,9 @@ def _undo(self, action, data):
37093716
self.clearstamp(stitem)
37103717
elif action == "go":
37113718
self._undogoto(data)
3719+
elif action == "teleport":
3720+
self._position = data[0]
3721+
self._update()
37123722
elif action in ["wri", "dot"]:
37133723
item = data[0]
37143724
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)