mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +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.
|
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
|
Use the ``turtle`` module namespace
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
@ -351,6 +376,7 @@ Pen control
|
||||||
|
|
||||||
Filling
|
Filling
|
||||||
| :func:`filling`
|
| :func:`filling`
|
||||||
|
| :func:`fill`
|
||||||
| :func:`begin_fill`
|
| :func:`begin_fill`
|
||||||
| :func:`end_fill`
|
| :func:`end_fill`
|
||||||
|
|
||||||
|
@ -381,6 +407,7 @@ Using events
|
||||||
| :func:`ondrag`
|
| :func:`ondrag`
|
||||||
|
|
||||||
Special Turtle methods
|
Special Turtle methods
|
||||||
|
| :func:`poly`
|
||||||
| :func:`begin_poly`
|
| :func:`begin_poly`
|
||||||
| :func:`end_poly`
|
| :func:`end_poly`
|
||||||
| :func:`get_poly`
|
| :func:`get_poly`
|
||||||
|
@ -403,6 +430,7 @@ Window control
|
||||||
| :func:`setworldcoordinates`
|
| :func:`setworldcoordinates`
|
||||||
|
|
||||||
Animation control
|
Animation control
|
||||||
|
| :func:`no_animation`
|
||||||
| :func:`delay`
|
| :func:`delay`
|
||||||
| :func:`tracer`
|
| :func:`tracer`
|
||||||
| :func:`update`
|
| :func:`update`
|
||||||
|
@ -1275,6 +1303,29 @@ Filling
|
||||||
... else:
|
... else:
|
||||||
... turtle.pensize(3)
|
... 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()
|
.. function:: begin_fill()
|
||||||
|
@ -1648,6 +1699,23 @@ Using events
|
||||||
Special Turtle methods
|
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()
|
.. function:: begin_poly()
|
||||||
|
|
||||||
Start recording the vertices of a polygon. Current turtle position is first
|
Start recording the vertices of a polygon. Current turtle position is first
|
||||||
|
@ -1926,6 +1994,23 @@ Window control
|
||||||
Animation 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)
|
.. function:: delay(delay=None)
|
||||||
|
|
||||||
:param delay: positive integer
|
:param delay: positive integer
|
||||||
|
|
|
@ -660,6 +660,14 @@ tkinter
|
||||||
(Contributed by Zhikang Yan in :gh:`126899`.)
|
(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
|
unicodedata
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
import tempfile
|
|
||||||
from test import support
|
from test import support
|
||||||
from test.support import import_helper
|
from test.support import import_helper
|
||||||
from test.support import os_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):
|
class TurtleConfigTest(unittest.TestCase):
|
||||||
|
|
||||||
def get_cfg_file(self, cfg_str):
|
def get_cfg_file(self, cfg_str):
|
||||||
|
@ -513,7 +528,7 @@ class TestTurtleScreen(unittest.TestCase):
|
||||||
|
|
||||||
turtle.TurtleScreen.save(screen, file_path, overwrite=True)
|
turtle.TurtleScreen.save(screen, file_path, overwrite=True)
|
||||||
with open(file_path) as f:
|
with open(file_path) as f:
|
||||||
assert f.read() == "postscript"
|
self.assertEqual(f.read(), "postscript")
|
||||||
|
|
||||||
def test_save(self) -> None:
|
def test_save(self) -> None:
|
||||||
screen = unittest.mock.Mock()
|
screen = unittest.mock.Mock()
|
||||||
|
@ -524,7 +539,98 @@ class TestTurtleScreen(unittest.TestCase):
|
||||||
|
|
||||||
turtle.TurtleScreen.save(screen, file_path)
|
turtle.TurtleScreen.save(screen, file_path)
|
||||||
with open(file_path) as f:
|
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):
|
class TestModuleLevel(unittest.TestCase):
|
||||||
|
|
|
@ -107,6 +107,7 @@ import sys
|
||||||
|
|
||||||
from os.path import isfile, split, join
|
from os.path import isfile, split, join
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from contextlib import contextmanager
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from tkinter import simpledialog
|
from tkinter import simpledialog
|
||||||
|
|
||||||
|
@ -114,23 +115,24 @@ _tg_classes = ['ScrolledCanvas', 'TurtleScreen', 'Screen',
|
||||||
'RawTurtle', 'Turtle', 'RawPen', 'Pen', 'Shape', 'Vec2D']
|
'RawTurtle', 'Turtle', 'RawPen', 'Pen', 'Shape', 'Vec2D']
|
||||||
_tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye',
|
_tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye',
|
||||||
'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas',
|
'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas',
|
||||||
'getshapes', 'listen', 'mainloop', 'mode', 'numinput',
|
'getshapes', 'listen', 'mainloop', 'mode', 'no_animation', 'numinput',
|
||||||
'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer',
|
'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer',
|
||||||
'register_shape', 'resetscreen', 'screensize', 'save', 'setup',
|
'register_shape', 'resetscreen', 'screensize', 'save', 'setup',
|
||||||
'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles', 'update',
|
'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles',
|
||||||
'window_height', 'window_width']
|
'update', 'window_height', 'window_width']
|
||||||
_tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk',
|
_tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk',
|
||||||
'circle', 'clear', 'clearstamp', 'clearstamps', 'clone', 'color',
|
'circle', 'clear', 'clearstamp', 'clearstamps', 'clone', 'color',
|
||||||
'degrees', 'distance', 'dot', 'down', 'end_fill', 'end_poly', 'fd',
|
'degrees', 'distance', 'dot', 'down', 'end_fill', 'end_poly', 'fd',
|
||||||
'fillcolor', 'filling', 'forward', 'get_poly', 'getpen', 'getscreen', 'get_shapepoly',
|
'fillcolor', 'fill', 'filling', 'forward', 'get_poly', 'getpen',
|
||||||
'getturtle', 'goto', 'heading', 'hideturtle', 'home', 'ht', 'isdown',
|
'getscreen', 'get_shapepoly', 'getturtle', 'goto', 'heading',
|
||||||
'isvisible', 'left', 'lt', 'onclick', 'ondrag', 'onrelease', 'pd',
|
'hideturtle', 'home', 'ht', 'isdown', 'isvisible', 'left', 'lt',
|
||||||
'pen', 'pencolor', 'pendown', 'pensize', 'penup', 'pos', 'position',
|
'onclick', 'ondrag', 'onrelease', 'pd', 'pen', 'pencolor', 'pendown',
|
||||||
'pu', 'radians', 'right', 'reset', 'resizemode', 'rt',
|
'pensize', 'penup', 'poly', 'pos', 'position', 'pu', 'radians', 'right',
|
||||||
'seth', 'setheading', 'setpos', 'setposition',
|
'reset', 'resizemode', 'rt', 'seth', 'setheading', 'setpos',
|
||||||
'setundobuffer', 'setx', 'sety', 'shape', 'shapesize', 'shapetransform', 'shearfactor', 'showturtle',
|
'setposition', 'setundobuffer', 'setx', 'sety', 'shape', 'shapesize',
|
||||||
'speed', 'st', 'stamp', 'teleport', 'tilt', 'tiltangle', 'towards',
|
'shapetransform', 'shearfactor', 'showturtle', 'speed', 'st', 'stamp',
|
||||||
'turtlesize', 'undo', 'undobufferentries', 'up', 'width',
|
'teleport', 'tilt', 'tiltangle', 'towards', 'turtlesize', 'undo',
|
||||||
|
'undobufferentries', 'up', 'width',
|
||||||
'write', 'xcor', 'ycor']
|
'write', 'xcor', 'ycor']
|
||||||
_tg_utilities = ['write_docstringdict', 'done']
|
_tg_utilities = ['write_docstringdict', 'done']
|
||||||
|
|
||||||
|
@ -1275,6 +1277,26 @@ class TurtleScreen(TurtleScreenBase):
|
||||||
return self._delayvalue
|
return self._delayvalue
|
||||||
self._delayvalue = int(delay)
|
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):
|
def _incrementudc(self):
|
||||||
"""Increment update counter."""
|
"""Increment update counter."""
|
||||||
if not TurtleScreen._RUNNING:
|
if not TurtleScreen._RUNNING:
|
||||||
|
@ -3380,6 +3402,24 @@ class RawTurtle(TPen, TNavigator):
|
||||||
"""
|
"""
|
||||||
return isinstance(self._fillpath, list)
|
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):
|
def begin_fill(self):
|
||||||
"""Called just before drawing a shape to be filled.
|
"""Called just before drawing a shape to be filled.
|
||||||
|
|
||||||
|
@ -3400,7 +3440,6 @@ class RawTurtle(TPen, TNavigator):
|
||||||
self.undobuffer.push(("beginfill", self._fillitem))
|
self.undobuffer.push(("beginfill", self._fillitem))
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
|
|
||||||
def end_fill(self):
|
def end_fill(self):
|
||||||
"""Fill the shape drawn after the call begin_fill().
|
"""Fill the shape drawn after the call begin_fill().
|
||||||
|
|
||||||
|
@ -3504,6 +3543,27 @@ class RawTurtle(TPen, TNavigator):
|
||||||
if self.undobuffer:
|
if self.undobuffer:
|
||||||
self.undobuffer.cumulate = False
|
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):
|
def begin_poly(self):
|
||||||
"""Start recording the vertices of a polygon.
|
"""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