Skip to content

Commit 3838b0c

Browse files
authored
Merge branch 'main' into fix-158140-independent-submodules
2 parents 541db05 + 9d3b0b8 commit 3838b0c

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
@@ -694,6 +694,34 @@ 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_nested_undo_sequence(self):
710+
with self.turtle._undo_sequence():
711+
self.turtle.teleport(10, 20)
712+
self.turtle.forward(10)
713+
self.assertEqual(self.turtle.undobufferentries(), 1)
714+
self.turtle.undo()
715+
self.assertEqual(self.turtle.pos(), (0, 0))
716+
717+
def test_stamp_without_undobuffer(self):
718+
shape = turtle.Shape("polygon", ((0, 0), (5, 9), (-5, 9)))
719+
self.turtle.screen._shapes = {self.turtle.shape(): shape}
720+
self.turtle.setundobuffer(None)
721+
stamp = self.turtle.stamp()
722+
self.turtle.clearstamp(stamp)
723+
self.assertEqual(self.turtle.stampItems, [])
724+
697725
class TestModuleLevel(unittest.TestCase):
698726
def test_all_signatures(self):
699727
import inspect

‎Lib/turtle.py‎

Lines changed: 73 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,20 @@ 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 not undobuffer or undobuffer.cumulate:
1660+
yield
1661+
return
1662+
undobuffer.push(["seq"])
1663+
undobuffer.cumulate = True
1664+
try:
1665+
yield
1666+
finally:
1667+
undobuffer.cumulate = False
1668+
16461669
def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
16471670
"""To be overwritten by child class RawTurtle.
16481671
Includes no TPen references."""
@@ -1985,38 +2008,34 @@ def circle(self, radius, extent = None, steps = None):
19852008
>>> turtle.circle(50)
19862009
>>> turtle.circle(120, 180) # draw a semicircle
19872010
"""
1988-
if self.undobuffer:
1989-
self.undobuffer.push(["seq"])
1990-
self.undobuffer.cumulate = True
19912011
speed = self.speed()
19922012
if extent is None:
19932013
extent = self._fullcircle
19942014
if steps is None:
19952015
frac = abs(extent)/self._fullcircle
1996-
steps = 1+int(min(11+abs(radius)/6.0, 59.0)*frac)
2016+
steps = 1 + int(min(11 + abs(radius) / 6.0, 59.0) * frac)
19972017
w = 1.0 * extent / steps
19982018
w2 = 0.5 * w
19992019
l = 2.0 * radius * math.sin(math.radians(w2)*self._degreesPerAU)
20002020
if radius < 0:
20012021
l, w, w2 = -l, -w, -w2
20022022
tr = self._tracer()
20032023
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):
2024+
with self._undo_sequence():
2025+
if speed == 0:
2026+
self._tracer(0, 0)
2027+
else:
2028+
self.speed(0)
2029+
self._rotate(w2)
2030+
for i in range(steps):
2031+
self.speed(speed)
2032+
self._go(l)
2033+
self.speed(0)
2034+
self._rotate(w)
2035+
self._rotate(-w2)
2036+
if speed == 0:
2037+
self._tracer(tr, dl)
20102038
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
20202039

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

@@ -2787,16 +2806,19 @@ def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
27872806
"""
27882807
pendown = self.isdown()
27892808
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()
2809+
with self._undo_sequence():
2810+
if pendown:
2811+
self.pen(pendown=False)
2812+
if was_filling and not fill_gap:
2813+
self.end_fill()
2814+
new_x = x if x is not None else self._position[0]
2815+
new_y = y if y is not None else self._position[1]
2816+
if self.undobuffer:
2817+
self.undobuffer.push(("teleport", self._position))
2818+
self._position = Vec2D(new_x, new_y)
2819+
self.pen(pendown=pendown)
2820+
if was_filling and not fill_gap:
2821+
self.begin_fill()
28002822

28012823
def clone(self):
28022824
"""Create and return a clone of the turtle.
@@ -3147,7 +3169,8 @@ def stamp(self):
31473169
screen._drawpoly(item, poly, fill=self._cc(fc),
31483170
outline=self._cc(oc), width=self._outlinewidth, top=True)
31493171
self.stampItems.append(stitem)
3150-
self.undobuffer.push(("stamp", stitem))
3172+
if self.undobuffer:
3173+
self.undobuffer.push(("stamp", stitem))
31513174
return stitem
31523175

31533176
def _clearstamp(self, stampid):
@@ -3162,15 +3185,8 @@ def _clearstamp(self, stampid):
31623185
self.stampItems.remove(stampid)
31633186
# Delete stampitem from undobuffer if necessary
31643187
# 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])
3188+
if self.undobuffer:
3189+
self.undobuffer.remove(("stamp", stampid))
31743190

31753191
def clearstamp(self, stampid):
31763192
"""Delete stamp with given stampid
@@ -3468,20 +3484,16 @@ def dot(self, size=None, *color):
34683484
color = self._colorstr(color)
34693485
# If screen were to gain a dot function, see GH #104218.
34703486
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
3487+
with self._undo_sequence():
3488+
try:
3489+
if self.resizemode() == 'auto':
3490+
self.ht()
3491+
self.pendown()
3492+
self.pensize(size)
3493+
self.pencolor(color)
3494+
self.forward(0)
3495+
finally:
3496+
self.pen(pen)
34853497

34863498
def _write(self, txt, align, font):
34873499
"""Performs the writing for write()
@@ -3513,15 +3525,11 @@ def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")):
35133525
>>> turtle.write('Home = ', True, align="center")
35143526
>>> turtle.write((0,0), True)
35153527
"""
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
3528+
with self._undo_sequence():
3529+
end = self._write(str(arg), align.lower(), font)
3530+
if move:
3531+
x, y = self.pos()
3532+
self.setpos(end, y)
35253533

35263534
@contextmanager
35273535
def poly(self):
@@ -3709,6 +3717,9 @@ def _undo(self, action, data):
37093717
self.clearstamp(stitem)
37103718
elif action == "go":
37113719
self._undogoto(data)
3720+
elif action == "teleport":
3721+
self._position = data[0]
3722+
self._update()
37123723
elif action in ["wri", "dot"]:
37133724
item = data[0]
37143725
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)