Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-126349 Add context managers to turtle for fill, poly and no_animation #126350

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions Doc/library/turtle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,32 @@ useful when working with learners for whom typing is not a skill.
use turtle graphics with a learner.


Automatically begin and end filling
-----------------------------------

If you have Python 3.14 or later, you don't need to call :func:`begin_fill` and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't usually include things like "If you have Python 3.14 or later" because each set of docs relates to one specific Python version.

But perhaps it's okay here because turtle is often used by beginners who might read these docs but have 3.12 or 3.13 installed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot of sense, but in this specific case I think it's useful. Many learners are at universities or schools with old versions of Python, and they might not be aware that there are several versions of Python. So I think it's useful to make it very clear that this is a new feature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to reopen, but IMO this reads a little bit unexpected. How about something in the lines of:

Starting with Python 3.14, you can use [...] instead of [...].

What do you think?

:func:`end_fill` for filling. Instead, you can use the :func:`fill`
:term:`context manager` to automatically begin and end fill. Here is an
example::

with fill():
for i in range(4):
forward(100)
right(90)

forward(200)
picnixz marked this conversation as resolved.
Show resolved Hide resolved

The code above is equivalent to::

begin_fill()
picnixz marked this conversation as resolved.
Show resolved Hide resolved
for i in range(4):
forward(100)
right(90)
end_fill()

forward(200)
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved


Use the ``turtle`` module namespace
-----------------------------------

Expand Down Expand Up @@ -381,6 +407,7 @@ Using events
| :func:`ondrag`

Special Turtle methods
| :func:`poly`
| :func:`begin_poly`
| :func:`end_poly`
| :func:`get_poly`
Expand All @@ -403,6 +430,7 @@ Window control
| :func:`setworldcoordinates`

Animation control
| :func:`no_animation`
| :func:`delay`
| :func:`tracer`
| :func:`update`
Expand Down Expand Up @@ -1275,6 +1303,29 @@ Filling
... else:
... turtle.pensize(3)

.. function:: fill()
hugovk marked this conversation as resolved.
Show resolved Hide resolved

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 ``fill`` is equivalent to adding the :func:`begin_fill` before the
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved
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:: 3.14
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved


.. function:: begin_fill()
Expand Down Expand Up @@ -1648,6 +1699,22 @@ 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.
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved

.. doctest::
:skipif: _tkinter is None

>>> with turtle.poly():
... turtle.forward(100)
... turtle.right(60)
... turtle.forward(100)

.. versionadded:: 3.14
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved

picnixz marked this conversation as resolved.
Show resolved Hide resolved
.. function:: begin_poly()

Start recording the vertices of a polygon. Current turtle position is first
Expand Down Expand Up @@ -1925,6 +1992,25 @@ Window control
Animation control
-----------------

.. function:: no_animation()

Temporarilly disable turtle animation. The code written inside the
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved
``no_animation`` block will not be animated, and once the code block is
exited, the drawing will appear.
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved

.. doctest::
:skipif: _tkinter is None

>>> with screen.no_animation():
... dist = 2
... for i in range(200):
... fd(dist)
... rt(90)
... dist += 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be written more compact. Though, I'm not sure it is more readable, so feel free to ignore this :)

Suggested change
... dist = 2
... for i in range(200):
... fd(dist)
... rt(90)
... dist += 2
... for dist in range(0, 400, 2):
... fd(dist+2)
... rt(90)



MarieRoald marked this conversation as resolved.
Show resolved Hide resolved
.. versionadded:: 3.14
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved

.. function:: delay(delay=None)

:param delay: positive integer
Expand Down
132 changes: 132 additions & 0 deletions Lib/test/test_turtle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import unittest
import unittest.mock
import tempfile
import sys
picnixz marked this conversation as resolved.
Show resolved Hide resolved
from contextlib import contextmanager
from test import support
from test.support import import_helper
from test.support import os_helper
Expand Down Expand Up @@ -54,6 +56,24 @@
"""


@contextmanager
def patch_screen():
"""Patch turtle._Screen for testing without a display.

We must patch the `_Screen` class itself instead of the `_Screen`
instance because instatiating it requires a display.
MarieRoald marked this conversation as resolved.
Show resolved Hide resolved
"""
m = unittest.mock.MagicMock()
m.__class__ = turtle._Screen
m.mode.return_value = "standard"

patch = unittest.mock.patch('turtle._Screen.__new__', return_value=m)
try:
yield patch.__enter__()
finally:
patch.__exit__(*sys.exc_info())


class TurtleConfigTest(unittest.TestCase):

def get_cfg_file(self, cfg_str):
Expand Down Expand Up @@ -526,6 +546,118 @@ def test_save(self) -> None:
with open(file_path) as f:
assert f.read() == "postscript"

def test_no_animation_sets_tracer_0(self):
s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())

with s.no_animation():
assert 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
assert 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 test_begin_end_fill(self):
"""begin_fill and end_fill counter each other."""
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
with patch_screen():
t = turtle.Turtle()
picnixz marked this conversation as resolved.
Show resolved Hide resolved

self.assertFalse(t.filling())
t.begin_fill()
self.assertTrue(t.filling())
t.end_fill()
self.assertFalse(t.filling())

def test_fill(self):
"""The context manager behaves like begin_ and end_ fill."""
with patch_screen():
t = turtle.Turtle()

self.assertFalse(t.filling())
with t.fill():
self.assertTrue(t.filling())
self.assertFalse(t.filling())

def test_fill_resets_after_exception(self):
"""The context manager cleans up correctly after exceptions."""
with patch_screen():
t = turtle.Turtle()
try:
with t.fill():
self.assertTrue(t.filling())
raise Exception
picnixz marked this conversation as resolved.
Show resolved Hide resolved
except Exception:
self.assertFalse(t.filling())

def test_fill_context_when_filling(self):
"""The context manager works even when the turtle is already filling."""
with patch_screen():
t = turtle.Turtle()

t.begin_fill()
self.assertTrue(t.filling())
with t.fill():
self.assertTrue(t.filling())
self.assertFalse(t.filling())

def test_begin_end_poly(self):
"""begin_fill and end_poly counter each other."""
with patch_screen():
t = turtle.Turtle()

self.assertFalse(t._creatingPoly)
t.begin_poly()
self.assertTrue(t._creatingPoly)
t.end_poly()
self.assertFalse(t._creatingPoly)

def test_poly(self):
"""The context manager behaves like begin_ and end_ poly."""
with patch_screen():
t = turtle.Turtle()

self.assertFalse(t._creatingPoly)
with t.poly():
self.assertTrue(t._creatingPoly)
self.assertFalse(t._creatingPoly)

def test_poly_resets_after_exception(self):
"""The context manager cleans up correctly after exceptions."""
with patch_screen():
t = turtle.Turtle()
try:
with t.poly():
self.assertTrue(t._creatingPoly)
raise Exception
picnixz marked this conversation as resolved.
Show resolved Hide resolved
except Exception:
self.assertFalse(t._creatingPoly)

def test_poly_context_when_creating_poly(self):
"""The context manager works when the turtle is already creating poly.
"""
with patch_screen():
t = turtle.Turtle()

t.begin_poly()
self.assertTrue(t._creatingPoly)
with t.poly():
self.assertTrue(t._creatingPoly)
self.assertFalse(t._creatingPoly)


class TestModuleLevel(unittest.TestCase):
def test_all_signatures(self):
Expand Down
Loading
Loading