Skip to content

Commit bedd2a9

Browse files
StanFromIrelandmiss-islington
authored andcommitted
gh-158563: Fix various bugs in turtle's undo handling (GH-158564)
(cherry picked from commit 9d3b0b8) Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent 06ef3d4 commit bedd2a9

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

@@ -1639,6 +1648,20 @@ def _goto(self, end):
16391648
"""Move the turtle to the end position."""
16401649
self._position = end
16411650

1651+
@contextmanager
1652+
def _undo_sequence(self):
1653+
"""Record the enclosed actions as a single undo step."""
1654+
undobuffer = self.undobuffer
1655+
if not undobuffer or undobuffer.cumulate:
1656+
yield
1657+
return
1658+
undobuffer.push(["seq"])
1659+
undobuffer.cumulate = True
1660+
try:
1661+
yield
1662+
finally:
1663+
undobuffer.cumulate = False
1664+
16421665
def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
16431666
"""To be overwritten by child class RawTurtle.
16441667
Includes no TPen references."""
@@ -1981,38 +2004,34 @@ def circle(self, radius, extent = None, steps = None):
19812004
>>> turtle.circle(50)
19822005
>>> turtle.circle(120, 180) # draw a semicircle
19832006
"""
1984-
if self.undobuffer:
1985-
self.undobuffer.push(["seq"])
1986-
self.undobuffer.cumulate = True
19872007
speed = self.speed()
19882008
if extent is None:
19892009
extent = self._fullcircle
19902010
if steps is None:
19912011
frac = abs(extent)/self._fullcircle
1992-
steps = 1+int(min(11+abs(radius)/6.0, 59.0)*frac)
2012+
steps = 1 + int(min(11 + abs(radius) / 6.0, 59.0) * frac)
19932013
w = 1.0 * extent / steps
19942014
w2 = 0.5 * w
19952015
l = 2.0 * radius * math.sin(math.radians(w2)*self._degreesPerAU)
19962016
if radius < 0:
19972017
l, w, w2 = -l, -w, -w2
19982018
tr = self._tracer()
19992019
dl = self._delay()
2000-
if speed == 0:
2001-
self._tracer(0, 0)
2002-
else:
2003-
self.speed(0)
2004-
self._rotate(w2)
2005-
for i in range(steps):
2020+
with self._undo_sequence():
2021+
if speed == 0:
2022+
self._tracer(0, 0)
2023+
else:
2024+
self.speed(0)
2025+
self._rotate(w2)
2026+
for i in range(steps):
2027+
self.speed(speed)
2028+
self._go(l)
2029+
self.speed(0)
2030+
self._rotate(w)
2031+
self._rotate(-w2)
2032+
if speed == 0:
2033+
self._tracer(tr, dl)
20062034
self.speed(speed)
2007-
self._go(l)
2008-
self.speed(0)
2009-
self._rotate(w)
2010-
self._rotate(-w2)
2011-
if speed == 0:
2012-
self._tracer(tr, dl)
2013-
self.speed(speed)
2014-
if self.undobuffer:
2015-
self.undobuffer.cumulate = False
20162035

20172036
# Three dummy methods to be implemented by the child class:
20182037

@@ -2783,16 +2802,19 @@ def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
27832802
"""
27842803
pendown = self.isdown()
27852804
was_filling = self.filling()
2786-
if pendown:
2787-
self.pen(pendown=False)
2788-
if was_filling and not fill_gap:
2789-
self.end_fill()
2790-
new_x = x if x is not None else self._position[0]
2791-
new_y = y if y is not None else self._position[1]
2792-
self._position = Vec2D(new_x, new_y)
2793-
self.pen(pendown=pendown)
2794-
if was_filling and not fill_gap:
2795-
self.begin_fill()
2805+
with self._undo_sequence():
2806+
if pendown:
2807+
self.pen(pendown=False)
2808+
if was_filling and not fill_gap:
2809+
self.end_fill()
2810+
new_x = x if x is not None else self._position[0]
2811+
new_y = y if y is not None else self._position[1]
2812+
if self.undobuffer:
2813+
self.undobuffer.push(("teleport", self._position))
2814+
self._position = Vec2D(new_x, new_y)
2815+
self.pen(pendown=pendown)
2816+
if was_filling and not fill_gap:
2817+
self.begin_fill()
27962818

27972819
def clone(self):
27982820
"""Create and return a clone of the turtle.
@@ -3143,7 +3165,8 @@ def stamp(self):
31433165
screen._drawpoly(item, poly, fill=self._cc(fc),
31443166
outline=self._cc(oc), width=self._outlinewidth, top=True)
31453167
self.stampItems.append(stitem)
3146-
self.undobuffer.push(("stamp", stitem))
3168+
if self.undobuffer:
3169+
self.undobuffer.push(("stamp", stitem))
31473170
return stitem
31483171

31493172
def _clearstamp(self, stampid):
@@ -3158,15 +3181,8 @@ def _clearstamp(self, stampid):
31583181
self.stampItems.remove(stampid)
31593182
# Delete stampitem from undobuffer if necessary
31603183
# if clearstamp is called directly.
3161-
item = ("stamp", stampid)
3162-
buf = self.undobuffer
3163-
if item not in buf.buffer:
3164-
return
3165-
index = buf.buffer.index(item)
3166-
buf.buffer.remove(item)
3167-
if index <= buf.ptr:
3168-
buf.ptr = (buf.ptr - 1) % buf.bufsize
3169-
buf.buffer.insert((buf.ptr+1)%buf.bufsize, [None])
3184+
if self.undobuffer:
3185+
self.undobuffer.remove(("stamp", stampid))
31703186

31713187
def clearstamp(self, stampid):
31723188
"""Delete stamp with given stampid
@@ -3464,20 +3480,16 @@ def dot(self, size=None, *color):
34643480
color = self._colorstr(color)
34653481
# If screen were to gain a dot function, see GH #104218.
34663482
pen = self.pen()
3467-
if self.undobuffer:
3468-
self.undobuffer.push(["seq"])
3469-
self.undobuffer.cumulate = True
3470-
try:
3471-
if self.resizemode() == 'auto':
3472-
self.ht()
3473-
self.pendown()
3474-
self.pensize(size)
3475-
self.pencolor(color)
3476-
self.forward(0)
3477-
finally:
3478-
self.pen(pen)
3479-
if self.undobuffer:
3480-
self.undobuffer.cumulate = False
3483+
with self._undo_sequence():
3484+
try:
3485+
if self.resizemode() == 'auto':
3486+
self.ht()
3487+
self.pendown()
3488+
self.pensize(size)
3489+
self.pencolor(color)
3490+
self.forward(0)
3491+
finally:
3492+
self.pen(pen)
34813493

34823494
def _write(self, txt, align, font):
34833495
"""Performs the writing for write()
@@ -3509,15 +3521,11 @@ def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")):
35093521
>>> turtle.write('Home = ', True, align="center")
35103522
>>> turtle.write((0,0), True)
35113523
"""
3512-
if self.undobuffer:
3513-
self.undobuffer.push(["seq"])
3514-
self.undobuffer.cumulate = True
3515-
end = self._write(str(arg), align.lower(), font)
3516-
if move:
3517-
x, y = self.pos()
3518-
self.setpos(end, y)
3519-
if self.undobuffer:
3520-
self.undobuffer.cumulate = False
3524+
with self._undo_sequence():
3525+
end = self._write(str(arg), align.lower(), font)
3526+
if move:
3527+
x, y = self.pos()
3528+
self.setpos(end, y)
35213529

35223530
@contextmanager
35233531
def poly(self):
@@ -3705,6 +3713,9 @@ def _undo(self, action, data):
37053713
self.clearstamp(stitem)
37063714
elif action == "go":
37073715
self._undogoto(data)
3716+
elif action == "teleport":
3717+
self._position = data[0]
3718+
self._update()
37083719
elif action in ["wri", "dot"]:
37093720
item = data[0]
37103721
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)