mirror of
https://github.com/python/cpython.git
synced 2025-07-07 11:25:30 +00:00
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:
parent
4dade055f4
commit
d3adf02c90
5 changed files with 277 additions and 16 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
-----------
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue