gh-126349: Add 'fill', 'poly', and 'no_animation' context managers to turtle (#126350)

Co-authored-by: Marie Roald <roald.marie@gmail.com>
Co-authored-by: Yngve Mardal Moe <yngve.m.moe@gmail.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: Daniel Hollas <danekhollas@gmail.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Erlend E. Aasland <erlend@python.org>
This commit is contained in:
Marie Roald 2025-01-18 11:27:22 +01:00 committed by GitHub
parent 4dade055f4
commit d3adf02c90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 277 additions and 16 deletions

View file

@ -213,6 +213,31 @@ useful when working with learners for whom typing is not a skill.
use turtle graphics with a learner.
Automatically begin and end filling
-----------------------------------
Starting with Python 3.14, you can use the :func:`fill` :term:`context manager`
instead of :func:`begin_fill` and :func:`end_fill` to automatically begin and
end fill. Here is an example::
with fill():
for i in range(4):
forward(100)
right(90)
forward(200)
The code above is equivalent to::
begin_fill()
for i in range(4):
forward(100)
right(90)
end_fill()
forward(200)
Use the ``turtle`` module namespace
-----------------------------------
@ -351,6 +376,7 @@ Pen control
Filling
| :func:`filling`
| :func:`fill`
| :func:`begin_fill`
| :func:`end_fill`
@ -381,6 +407,7 @@ Using events
| :func:`ondrag`
Special Turtle methods
| :func:`poly`
| :func:`begin_poly`
| :func:`end_poly`
| :func:`get_poly`
@ -403,6 +430,7 @@ Window control
| :func:`setworldcoordinates`
Animation control
| :func:`no_animation`
| :func:`delay`
| :func:`tracer`
| :func:`update`
@ -1275,6 +1303,29 @@ Filling
... else:
... turtle.pensize(3)
.. function:: fill()
Fill the shape drawn in the ``with turtle.fill():`` block.
.. doctest::
:skipif: _tkinter is None
>>> turtle.color("black", "red")
>>> with turtle.fill():
... turtle.circle(80)
Using :func:`!fill` is equivalent to adding the :func:`begin_fill` before the
fill-block and :func:`end_fill` after the fill-block:
.. doctest::
:skipif: _tkinter is None
>>> turtle.color("black", "red")
>>> turtle.begin_fill()
>>> turtle.circle(80)
>>> turtle.end_fill()
.. versionadded:: next
.. function:: begin_fill()
@ -1648,6 +1699,23 @@ Using events
Special Turtle methods
----------------------
.. function:: poly()
Record the vertices of a polygon drawn in the ``with turtle.poly():`` block.
The first and last vertices will be connected.
.. doctest::
:skipif: _tkinter is None
>>> with turtle.poly():
... turtle.forward(100)
... turtle.right(60)
... turtle.forward(100)
.. versionadded:: next
.. function:: begin_poly()
Start recording the vertices of a polygon. Current turtle position is first
@ -1926,6 +1994,23 @@ Window control
Animation control
-----------------
.. function:: no_animation()
Temporarily disable turtle animation. The code written inside the
``no_animation`` block will not be animated;
once the code block is exited, the drawing will appear.
.. doctest::
:skipif: _tkinter is None
>>> with screen.no_animation():
... for dist in range(2, 400, 2):
... fd(dist)
... rt(90)
.. versionadded:: next
.. function:: delay(delay=None)
:param delay: positive integer

View file

@ -660,6 +660,14 @@ tkinter
(Contributed by Zhikang Yan in :gh:`126899`.)
turtle
------
* Add context managers for :func:`turtle.fill`, :func:`turtle.poly`
and :func:`turtle.no_animation`.
(Contributed by Marie Roald and Yngve Mardal Moe in :gh:`126350`.)
unicodedata
-----------

View file

@ -1,9 +1,9 @@
import os
import pickle
import re
import tempfile
import unittest
import unittest.mock
import tempfile
from test import support
from test.support import import_helper
from test.support import os_helper
@ -54,6 +54,21 @@ visible = False
"""
def patch_screen():
"""Patch turtle._Screen for testing without a display.
We must patch the _Screen class itself instead of the _Screen
instance because instantiating it requires a display.
"""
return unittest.mock.patch(
"turtle._Screen.__new__",
**{
"return_value.__class__": turtle._Screen,
"return_value.mode.return_value": "standard",
},
)
class TurtleConfigTest(unittest.TestCase):
def get_cfg_file(self, cfg_str):
@ -513,7 +528,7 @@ class TestTurtleScreen(unittest.TestCase):
turtle.TurtleScreen.save(screen, file_path, overwrite=True)
with open(file_path) as f:
assert f.read() == "postscript"
self.assertEqual(f.read(), "postscript")
def test_save(self) -> None:
screen = unittest.mock.Mock()
@ -524,7 +539,98 @@ class TestTurtleScreen(unittest.TestCase):
turtle.TurtleScreen.save(screen, file_path)
with open(file_path) as f:
assert f.read() == "postscript"
self.assertEqual(f.read(), "postscript")
def test_no_animation_sets_tracer_0(self):
s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
with s.no_animation():
self.assertEqual(s.tracer(), 0)
def test_no_animation_resets_tracer_to_old_value(self):
s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
for tracer in [0, 1, 5]:
s.tracer(tracer)
with s.no_animation():
pass
self.assertEqual(s.tracer(), tracer)
def test_no_animation_calls_update_at_exit(self):
s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
s.update = unittest.mock.MagicMock()
with s.no_animation():
s.update.assert_not_called()
s.update.assert_called_once()
class TestTurtle(unittest.TestCase):
def setUp(self):
with patch_screen():
self.turtle = turtle.Turtle()
def test_begin_end_fill(self):
self.assertFalse(self.turtle.filling())
self.turtle.begin_fill()
self.assertTrue(self.turtle.filling())
self.turtle.end_fill()
self.assertFalse(self.turtle.filling())
def test_fill(self):
# The context manager behaves like begin_fill and end_fill.
self.assertFalse(self.turtle.filling())
with self.turtle.fill():
self.assertTrue(self.turtle.filling())
self.assertFalse(self.turtle.filling())
def test_fill_resets_after_exception(self):
# The context manager cleans up correctly after exceptions.
try:
with self.turtle.fill():
self.assertTrue(self.turtle.filling())
raise ValueError
except ValueError:
self.assertFalse(self.turtle.filling())
def test_fill_context_when_filling(self):
# The context manager works even when the turtle is already filling.
self.turtle.begin_fill()
self.assertTrue(self.turtle.filling())
with self.turtle.fill():
self.assertTrue(self.turtle.filling())
self.assertFalse(self.turtle.filling())
def test_begin_end_poly(self):
self.assertFalse(self.turtle._creatingPoly)
self.turtle.begin_poly()
self.assertTrue(self.turtle._creatingPoly)
self.turtle.end_poly()
self.assertFalse(self.turtle._creatingPoly)
def test_poly(self):
# The context manager behaves like begin_poly and end_poly.
self.assertFalse(self.turtle._creatingPoly)
with self.turtle.poly():
self.assertTrue(self.turtle._creatingPoly)
self.assertFalse(self.turtle._creatingPoly)
def test_poly_resets_after_exception(self):
# The context manager cleans up correctly after exceptions.
try:
with self.turtle.poly():
self.assertTrue(self.turtle._creatingPoly)
raise ValueError
except ValueError:
self.assertFalse(self.turtle._creatingPoly)
def test_poly_context_when_creating_poly(self):
# The context manager works when the turtle is already creating poly.
self.turtle.begin_poly()
self.assertTrue(self.turtle._creatingPoly)
with self.turtle.poly():
self.assertTrue(self.turtle._creatingPoly)
self.assertFalse(self.turtle._creatingPoly)
class TestModuleLevel(unittest.TestCase):

View file

@ -107,6 +107,7 @@ import sys
from os.path import isfile, split, join
from pathlib import Path
from contextlib import contextmanager
from copy import deepcopy
from tkinter import simpledialog
@ -114,23 +115,24 @@ _tg_classes = ['ScrolledCanvas', 'TurtleScreen', 'Screen',
'RawTurtle', 'Turtle', 'RawPen', 'Pen', 'Shape', 'Vec2D']
_tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye',
'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas',
'getshapes', 'listen', 'mainloop', 'mode', 'numinput',
'getshapes', 'listen', 'mainloop', 'mode', 'no_animation', 'numinput',
'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer',
'register_shape', 'resetscreen', 'screensize', 'save', 'setup',
'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles', 'update',
'window_height', 'window_width']
'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles',
'update', 'window_height', 'window_width']
_tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk',
'circle', 'clear', 'clearstamp', 'clearstamps', 'clone', 'color',
'degrees', 'distance', 'dot', 'down', 'end_fill', 'end_poly', 'fd',
'fillcolor', 'filling', 'forward', 'get_poly', 'getpen', 'getscreen', 'get_shapepoly',
'getturtle', 'goto', 'heading', 'hideturtle', 'home', 'ht', 'isdown',
'isvisible', 'left', 'lt', 'onclick', 'ondrag', 'onrelease', 'pd',
'pen', 'pencolor', 'pendown', 'pensize', 'penup', 'pos', 'position',
'pu', 'radians', 'right', 'reset', 'resizemode', 'rt',
'seth', 'setheading', 'setpos', 'setposition',
'setundobuffer', 'setx', 'sety', 'shape', 'shapesize', 'shapetransform', 'shearfactor', 'showturtle',
'speed', 'st', 'stamp', 'teleport', 'tilt', 'tiltangle', 'towards',
'turtlesize', 'undo', 'undobufferentries', 'up', 'width',
'fillcolor', 'fill', 'filling', 'forward', 'get_poly', 'getpen',
'getscreen', 'get_shapepoly', 'getturtle', 'goto', 'heading',
'hideturtle', 'home', 'ht', 'isdown', 'isvisible', 'left', 'lt',
'onclick', 'ondrag', 'onrelease', 'pd', 'pen', 'pencolor', 'pendown',
'pensize', 'penup', 'poly', 'pos', 'position', 'pu', 'radians', 'right',
'reset', 'resizemode', 'rt', 'seth', 'setheading', 'setpos',
'setposition', 'setundobuffer', 'setx', 'sety', 'shape', 'shapesize',
'shapetransform', 'shearfactor', 'showturtle', 'speed', 'st', 'stamp',
'teleport', 'tilt', 'tiltangle', 'towards', 'turtlesize', 'undo',
'undobufferentries', 'up', 'width',
'write', 'xcor', 'ycor']
_tg_utilities = ['write_docstringdict', 'done']
@ -1275,6 +1277,26 @@ class TurtleScreen(TurtleScreenBase):
return self._delayvalue
self._delayvalue = int(delay)
@contextmanager
def no_animation(self):
"""Temporarily turn off auto-updating the screen.
This is useful for drawing complex shapes where even the fastest setting
is too slow. Once this context manager is exited, the drawing will
be displayed.
Example (for a TurtleScreen instance named screen
and a Turtle instance named turtle):
>>> with screen.no_animation():
... turtle.circle(50)
"""
tracer = self.tracer()
try:
self.tracer(0)
yield
finally:
self.tracer(tracer)
def _incrementudc(self):
"""Increment update counter."""
if not TurtleScreen._RUNNING:
@ -3380,6 +3402,24 @@ class RawTurtle(TPen, TNavigator):
"""
return isinstance(self._fillpath, list)
@contextmanager
def fill(self):
"""A context manager for filling a shape.
Implicitly ensures the code block is wrapped with
begin_fill() and end_fill().
Example (for a Turtle instance named turtle):
>>> turtle.color("black", "red")
>>> with turtle.fill():
... turtle.circle(60)
"""
self.begin_fill()
try:
yield
finally:
self.end_fill()
def begin_fill(self):
"""Called just before drawing a shape to be filled.
@ -3400,7 +3440,6 @@ class RawTurtle(TPen, TNavigator):
self.undobuffer.push(("beginfill", self._fillitem))
self._update()
def end_fill(self):
"""Fill the shape drawn after the call begin_fill().
@ -3504,6 +3543,27 @@ class RawTurtle(TPen, TNavigator):
if self.undobuffer:
self.undobuffer.cumulate = False
@contextmanager
def poly(self):
"""A context manager for recording the vertices of a polygon.
Implicitly ensures that the code block is wrapped with
begin_poly() and end_poly()
Example (for a Turtle instance named turtle) where we create a
triangle as the polygon and move the turtle 100 steps forward:
>>> with turtle.poly():
... for side in range(3)
... turtle.forward(50)
... turtle.right(60)
>>> turtle.forward(100)
"""
self.begin_poly()
try:
yield
finally:
self.end_poly()
def begin_poly(self):
"""Start recording the vertices of a polygon.

View file

@ -0,0 +1,2 @@
Add :func:`turtle.fill`, :func:`turtle.poly` and :func:`turtle.no_animation` context managers.
Patch by Marie Roald and Yngve Mardal Moe.