From 3379b3d6620a55a95712d8707f7ffc7b66fbea82 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sun, 6 Oct 2024 04:13:45 +0000 Subject: [PATCH 01/19] Add context managers for turtle.fill and turtle.poly Co-authored-by: Marie Roald --- Lib/test/test_turtle.py | 108 ++++++++++++++++++++++++++++++++++++++++ Lib/turtle.py | 19 ++++++- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index c75a002a89b4c4..d44ec555148d5e 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -4,6 +4,8 @@ import unittest import unittest.mock import tempfile +import sys +from contextlib import contextmanager from test import support from test.support import import_helper from test.support import os_helper @@ -527,6 +529,112 @@ def test_save(self) -> None: assert f.read() == "postscript" +class TestTurtle(unittest.TestCase): + @contextmanager + def patch_screen(self): + """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. + """ + 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()) + + def test_begin_end_fill(self): + """begin_fill and end_fill counter each other.""" + with self.patch_screen(): + t = turtle.Turtle() + + 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 self.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 self.patch_screen(): + t = turtle.Turtle() + try: + with t.fill(): + self.assertTrue(t.filling()) + raise Exception + 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 self.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 self.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 self.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 self.patch_screen(): + t = turtle.Turtle() + try: + with t.poly(): + self.assertTrue(t._creatingPoly) + raise Exception + 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 self.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): import inspect diff --git a/Lib/turtle.py b/Lib/turtle.py index 8a5801f2efe625..e75effa5630149 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -110,6 +110,8 @@ from copy import deepcopy from tkinter import simpledialog +from contextlib import contextmanager + _tg_classes = ['ScrolledCanvas', 'TurtleScreen', 'Screen', 'RawTurtle', 'Turtle', 'RawPen', 'Pen', 'Shape', 'Vec2D'] _tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye', @@ -3382,6 +3384,14 @@ def filling(self): """ return isinstance(self._fillpath, list) + @contextmanager + def fill(self): + self.begin_fill() + try: + yield + finally: + self.end_fill() + def begin_fill(self): """Called just before drawing a shape to be filled. @@ -3402,7 +3412,6 @@ def begin_fill(self): self.undobuffer.push(("beginfill", self._fillitem)) self._update() - def end_fill(self): """Fill the shape drawn after the call begin_fill(). @@ -3506,6 +3515,14 @@ def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")): if self.undobuffer: self.undobuffer.cumulate = False + @contextmanager + def poly(self): + self.begin_poly() + try: + yield + finally: + self.end_poly() + def begin_poly(self): """Start recording the vertices of a polygon. From 879d8864b36ecb073fb0bbca145aabf487368833 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Sun, 20 Oct 2024 04:02:29 +0000 Subject: [PATCH 02/19] Add documentation Co-authored-by: Yngve Mardal Moe --- Lib/turtle.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Lib/turtle.py b/Lib/turtle.py index e75effa5630149..5de53481cac786 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -3386,6 +3386,18 @@ def filling(self): @contextmanager def fill(self): + """Create a filled shape + + No argument. + + This function sets up a context manager that will automatically end + filling once exited. + + Example (for a Turtle instance named turtle): + >>> turtle.color("black", "red") + >>> with turtle.poly(): + ... turtle.circle(60) + """ self.begin_fill() try: yield @@ -3517,6 +3529,22 @@ def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")): @contextmanager def poly(self): + """Record the vertices of a polygon. + + No argument. + + Record the vertices of a polygon. Current turtle position is first point + of polygon. This function sets up a context manager that will + automatically end recording once exited. + + 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 From 54f709c1e92efd0b49e7f60c716ecf0098dc20cc Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Sun, 27 Oct 2024 05:28:55 +0000 Subject: [PATCH 03/19] Add context manager to turtle.TurtleScreen for disabling auto-update Co-authored-by: Yngve Mardal Moe --- Lib/test/test_turtle.py | 70 +++++++++++++++++++++++++++-------------- Lib/turtle.py | 19 +++++++++++ 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index d44ec555148d5e..3aa43b5f79c13a 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -56,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. + """ + 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): @@ -528,28 +546,34 @@ 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()) -class TestTurtle(unittest.TestCase): - @contextmanager - def patch_screen(self): - """Patch turtle._Screen for testing without a display. + with s.no_animation(): + assert s.tracer() == 0 - We must patch the `_Screen` class itself instead of the `_Screen` - instance because instatiating it requires a display. - """ - m = unittest.mock.MagicMock() - m.__class__ = turtle._Screen - m.mode.return_value = "standard" + def test_no_animation_resets_tracer_to_old_value(self): + s = turtle.TurtleScreen(cv=unittest.mock.MagicMock()) - patch = unittest.mock.patch('turtle._Screen.__new__', return_value=m) - try: - yield patch.__enter__() - finally: - patch.__exit__(*sys.exc_info()) + 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.""" - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() self.assertFalse(t.filling()) @@ -560,7 +584,7 @@ def test_begin_end_fill(self): def test_fill(self): """The context manager behaves like begin_ and end_ fill.""" - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() self.assertFalse(t.filling()) @@ -570,7 +594,7 @@ def test_fill(self): def test_fill_resets_after_exception(self): """The context manager cleans up correctly after exceptions.""" - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() try: with t.fill(): @@ -581,7 +605,7 @@ def test_fill_resets_after_exception(self): def test_fill_context_when_filling(self): """The context manager works even when the turtle is already filling.""" - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() t.begin_fill() @@ -592,7 +616,7 @@ def test_fill_context_when_filling(self): def test_begin_end_poly(self): """begin_fill and end_poly counter each other.""" - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() self.assertFalse(t._creatingPoly) @@ -603,7 +627,7 @@ def test_begin_end_poly(self): def test_poly(self): """The context manager behaves like begin_ and end_ poly.""" - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() self.assertFalse(t._creatingPoly) @@ -613,7 +637,7 @@ def test_poly(self): def test_poly_resets_after_exception(self): """The context manager cleans up correctly after exceptions.""" - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() try: with t.poly(): @@ -625,7 +649,7 @@ def test_poly_resets_after_exception(self): def test_poly_context_when_creating_poly(self): """The context manager works when the turtle is already creating poly. """ - with self.patch_screen(): + with patch_screen(): t = turtle.Turtle() t.begin_poly() diff --git a/Lib/turtle.py b/Lib/turtle.py index 5de53481cac786..c0e28c9fb0be4c 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -1279,6 +1279,25 @@ def delay(self, delay=None): 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): + >>> with turtle.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: From 437c8ceedf769ba1766af652df23176d51e0f6e0 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sun, 27 Oct 2024 05:45:07 +0000 Subject: [PATCH 04/19] Forward functions correctly Co-authored-by: Marie Roald --- Lib/turtle.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Lib/turtle.py b/Lib/turtle.py index c0e28c9fb0be4c..eb010f5df15b75 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -116,23 +116,24 @@ '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'] From d11d8f99c8d5ff0f4418631bd52ecc0f066d3a53 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sun, 27 Oct 2024 05:51:59 +0000 Subject: [PATCH 05/19] Add documentation Co-authored-by: Marie Roald --- Doc/library/turtle.rst | 85 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index 8eb4f8271fcfae..3c186070074a30 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -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 +: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) + +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 ----------------------------------- @@ -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 ``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:: 3.14 .. function:: begin_fill() @@ -1648,6 +1699,22 @@ Using events Special Turtle methods ---------------------- + +.. function:: poly() + + Record the vertices of a polygon. 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:: 3.14 + .. function:: begin_poly() Start recording the vertices of a polygon. Current turtle position is first @@ -1925,6 +1992,24 @@ Window control Animation control ----------------- +.. function:: no_animation() + + Temporarilly disable turtle animation. The code written inside the + ``no_animation`` block will not be animated, and once the code block is + exitted, the drawing will appear. + + .. doctest:: + :skipif: _tkinter is None + + >>> with screen.no_animation(): + ... for i in range(200): + ... fd(dist) + ... rt(90) + ... dist += 2 + + + .. versionadded:: 3.14 + .. function:: delay(delay=None) :param delay: positive integer From 1f04ee4cc9f958b22de8b757026b0b58b940443c Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 06:05:16 +0000 Subject: [PATCH 06/19] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2024-11-03-06-05-16.gh-issue-126349.7YwWsI.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-11-03-06-05-16.gh-issue-126349.7YwWsI.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-11-03-06-05-16.gh-issue-126349.7YwWsI.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-11-03-06-05-16.gh-issue-126349.7YwWsI.rst new file mode 100644 index 00000000000000..3523ab624a840b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-11-03-06-05-16.gh-issue-126349.7YwWsI.rst @@ -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. From 3947de18673af79b6188b7a059bb14a2de943402 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Sun, 3 Nov 2024 06:59:49 +0000 Subject: [PATCH 07/19] Fix typo in docs Co-authored-by: Yngve Mardal Moe --- Doc/library/turtle.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index 3c186070074a30..64f6f074426cee 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -2002,6 +2002,7 @@ Animation control :skipif: _tkinter is None >>> with screen.no_animation(): + ... dist = 2 ... for i in range(200): ... fd(dist) ... rt(90) From 5ee489b75efed152f5c20689c69e2de0e6d3eb3a Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Fri, 8 Nov 2024 17:56:08 +0100 Subject: [PATCH 08/19] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Daniel Hollas --- Doc/library/turtle.rst | 6 +++--- Lib/turtle.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index 64f6f074426cee..640f28f944b2b3 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -1315,7 +1315,7 @@ Filling ... turtle.circle(80) Using ``fill`` is equivalent to adding the :func:`begin_fill` before the - fill-block and :func:`end_fill` after the fill-block + fill-block and :func:`end_fill` after the fill-block: .. doctest:: :skipif: _tkinter is None @@ -1702,7 +1702,7 @@ Special Turtle methods .. function:: poly() - Record the vertices of a polygon. The first and last vertices will be + Record the vertices of a polygon drawn in the ``with turtle.poly():`` block. The first and last vertices will be connected. .. doctest:: @@ -1996,7 +1996,7 @@ Animation control Temporarilly disable turtle animation. The code written inside the ``no_animation`` block will not be animated, and once the code block is - exitted, the drawing will appear. + exited, the drawing will appear. .. doctest:: :skipif: _tkinter is None diff --git a/Lib/turtle.py b/Lib/turtle.py index eb010f5df15b75..0a1846153fb37c 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -1289,7 +1289,7 @@ def no_animation(self): be displayed. Example (for a TurtleScreen instance named screen): - >>> with turtle.no_animation() + >>> with screen.no_animation() ... turtle.circle(50) """ tracer = self.tracer() From 28a5ac6d87b6575ff2ef0350aa343d5ca35e7229 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Fri, 8 Nov 2024 18:06:55 +0000 Subject: [PATCH 09/19] Address review comments --- Doc/library/turtle.rst | 11 ++++++----- Doc/whatsnew/3.14.rst | 9 +++++++++ Lib/test/test_turtle.py | 17 +++++++---------- Lib/turtle.py | 3 +-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index 640f28f944b2b3..e780093feace53 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -222,9 +222,9 @@ If you have Python 3.14 or later, you don't need to call :func:`begin_fill` and example:: with fill(): - for i in range(4): - forward(100) - right(90) + for i in range(4): + forward(100) + right(90) forward(200) @@ -232,8 +232,8 @@ The code above is equivalent to:: begin_fill() for i in range(4): - forward(100) - right(90) + forward(100) + right(90) end_fill() forward(200) @@ -377,6 +377,7 @@ Pen control Filling | :func:`filling` + | :func:`fill` | :func:`begin_fill` | :func:`end_fill` diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 21bc289c2be5d8..93f9268566c218 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -438,6 +438,15 @@ sys which only exists in specialized builds of Python, may now return objects from other interpreters than the one it's called in. +turtle +------ + +* Add context managers for :func:`turtle.fill`, :func:`turtle.poly` and + :func:`turtle.no_animation` for + :func:`turtle.begin_fill`/:func:`turtle.end_fill`, + :func:`turtle.begin_poly`/:func:`turtle.end_poly`, and + :func:`turtle.tracer(0)`/:func:`turtle.tracer(1)`, respectively. + (Contributed by Marie Roald and Yngve Mardal Moe in :gh:`126350`.) unicodedata ----------- diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 3aa43b5f79c13a..b937252af2c94f 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -3,8 +3,8 @@ import re import unittest import unittest.mock -import tempfile import sys +import tempfile from contextlib import contextmanager from test import support from test.support import import_helper @@ -572,7 +572,6 @@ def test_no_animation_calls_update_at_exit(self): class TestTurtle(unittest.TestCase): def test_begin_end_fill(self): - """begin_fill and end_fill counter each other.""" with patch_screen(): t = turtle.Turtle() @@ -583,7 +582,7 @@ def test_begin_end_fill(self): self.assertFalse(t.filling()) def test_fill(self): - """The context manager behaves like begin_ and end_ fill.""" + # The context manager behaves like begin_fill and end_fill. with patch_screen(): t = turtle.Turtle() @@ -593,7 +592,7 @@ def test_fill(self): self.assertFalse(t.filling()) def test_fill_resets_after_exception(self): - """The context manager cleans up correctly after exceptions.""" + # The context manager cleans up correctly after exceptions. with patch_screen(): t = turtle.Turtle() try: @@ -604,7 +603,7 @@ def test_fill_resets_after_exception(self): self.assertFalse(t.filling()) def test_fill_context_when_filling(self): - """The context manager works even when the turtle is already filling.""" + # The context manager works even when the turtle is already filling. with patch_screen(): t = turtle.Turtle() @@ -615,7 +614,6 @@ def test_fill_context_when_filling(self): self.assertFalse(t.filling()) def test_begin_end_poly(self): - """begin_fill and end_poly counter each other.""" with patch_screen(): t = turtle.Turtle() @@ -626,7 +624,7 @@ def test_begin_end_poly(self): self.assertFalse(t._creatingPoly) def test_poly(self): - """The context manager behaves like begin_ and end_ poly.""" + # The context manager behaves like begin_poly and end_poly. with patch_screen(): t = turtle.Turtle() @@ -636,7 +634,7 @@ def test_poly(self): self.assertFalse(t._creatingPoly) def test_poly_resets_after_exception(self): - """The context manager cleans up correctly after exceptions.""" + # The context manager cleans up correctly after exceptions. with patch_screen(): t = turtle.Turtle() try: @@ -647,8 +645,7 @@ def test_poly_resets_after_exception(self): self.assertFalse(t._creatingPoly) def test_poly_context_when_creating_poly(self): - """The context manager works when the turtle is already creating poly. - """ + # The context manager works when the turtle is already creating poly. with patch_screen(): t = turtle.Turtle() diff --git a/Lib/turtle.py b/Lib/turtle.py index 0a1846153fb37c..6a4b6e2cdf13e2 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -107,11 +107,10 @@ from os.path import isfile, split, join from pathlib import Path +from contextlib import contextmanager from copy import deepcopy from tkinter import simpledialog -from contextlib import contextmanager - _tg_classes = ['ScrolledCanvas', 'TurtleScreen', 'Screen', 'RawTurtle', 'Turtle', 'RawPen', 'Pen', 'Shape', 'Vec2D'] _tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye', From 6621bd3cd4350d720d509561a83bfcaba1be0c51 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Fri, 8 Nov 2024 19:11:06 +0100 Subject: [PATCH 10/19] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Daniel Hollas --- Doc/library/turtle.rst | 11 ++++++----- Lib/turtle.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index e780093feace53..7127172b770e79 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -1326,7 +1326,7 @@ Filling >>> turtle.circle(80) >>> turtle.end_fill() - .. versionadded:: 3.14 + .. versionadded:: next .. function:: begin_fill() @@ -1703,8 +1703,8 @@ 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. + 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 @@ -1714,7 +1714,7 @@ Special Turtle methods ... turtle.right(60) ... turtle.forward(100) - .. versionadded:: 3.14 + .. versionadded:: next .. function:: begin_poly() @@ -2010,7 +2010,8 @@ Animation control ... dist += 2 - .. versionadded:: 3.14 + .. versionadded:: next + .. function:: delay(delay=None) diff --git a/Lib/turtle.py b/Lib/turtle.py index 6a4b6e2cdf13e2..bfe1a9ba04c608 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -3414,7 +3414,7 @@ def fill(self): Example (for a Turtle instance named turtle): >>> turtle.color("black", "red") - >>> with turtle.poly(): + >>> with turtle.fill(): ... turtle.circle(60) """ self.begin_fill() From 2f5c4884adffb3061205c0118b8f8870b2d34ca4 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Fri, 8 Nov 2024 18:38:53 +0000 Subject: [PATCH 11/19] Address review comments --- Doc/library/turtle.rst | 1 + Doc/whatsnew/3.14.rst | 7 ++----- Lib/test/test_turtle.py | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index 7127172b770e79..ae4976e421fc38 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -1716,6 +1716,7 @@ Special Turtle methods .. versionadded:: next + .. function:: begin_poly() Start recording the vertices of a polygon. Current turtle position is first diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 93f9268566c218..29f58af98f26dd 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -442,11 +442,8 @@ turtle ------ * Add context managers for :func:`turtle.fill`, :func:`turtle.poly` and - :func:`turtle.no_animation` for - :func:`turtle.begin_fill`/:func:`turtle.end_fill`, - :func:`turtle.begin_poly`/:func:`turtle.end_poly`, and - :func:`turtle.tracer(0)`/:func:`turtle.tracer(1)`, respectively. - (Contributed by Marie Roald and Yngve Mardal Moe in :gh:`126350`.) + :func:`turtle.no_animation` (Contributed by Marie Roald and Yngve Mardal Moe + in :gh:`126350`.) unicodedata ----------- diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index b937252af2c94f..405c4628575a0b 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -1,10 +1,10 @@ import os import pickle import re -import unittest -import unittest.mock import sys import tempfile +import unittest +import unittest.mock from contextlib import contextmanager from test import support from test.support import import_helper From 302a1ed8493a6d151b1d1ed6d10242df99b5baa4 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Sat, 9 Nov 2024 05:39:16 +0100 Subject: [PATCH 12/19] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/turtle.rst | 2 -- Doc/whatsnew/3.14.rst | 6 +++--- Lib/test/test_turtle.py | 2 +- Lib/turtle.py | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index ae4976e421fc38..a76e92486c236c 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -2009,8 +2009,6 @@ Animation control ... fd(dist) ... rt(90) ... dist += 2 - - .. versionadded:: next diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 29f58af98f26dd..267cab5a0ebd79 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -441,9 +441,9 @@ sys 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`.) +* 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 ----------- diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 405c4628575a0b..61462669909eea 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -60,7 +60,7 @@ def patch_screen(): """Patch turtle._Screen for testing without a display. - We must patch the `_Screen` class itself instead of the `_Screen` + We must patch the _Screen class itself instead of the _Screen instance because instatiating it requires a display. """ m = unittest.mock.MagicMock() diff --git a/Lib/turtle.py b/Lib/turtle.py index bfe1a9ba04c608..fa66474a035521 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -1288,7 +1288,7 @@ def no_animation(self): be displayed. Example (for a TurtleScreen instance named screen): - >>> with screen.no_animation() + >>> with screen.no_animation(): ... turtle.circle(50) """ tracer = self.tracer() From ad779d587ce1996913547c6795bbaceab293ca1c Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sat, 9 Nov 2024 04:51:44 +0000 Subject: [PATCH 13/19] Use setUp for unit tests Co-authored-by: Marie Roald --- Lib/test/test_turtle.py | 92 +++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 55 deletions(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 61462669909eea..232e8c8f4a6fd4 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -571,89 +571,71 @@ def test_no_animation_calls_update_at_exit(self): class TestTurtle(unittest.TestCase): - def test_begin_end_fill(self): + def setUp(self): with patch_screen(): - t = turtle.Turtle() + self.turtle = turtle.Turtle() - self.assertFalse(t.filling()) - t.begin_fill() - self.assertTrue(t.filling()) - t.end_fill() - self.assertFalse(t.filling()) + 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. - with patch_screen(): - t = turtle.Turtle() - - self.assertFalse(t.filling()) - with t.fill(): - self.assertTrue(t.filling()) - self.assertFalse(t.filling()) + 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. - with patch_screen(): - t = turtle.Turtle() try: - with t.fill(): - self.assertTrue(t.filling()) + with self.turtle.fill(): + self.assertTrue(self.turtle.filling()) raise Exception except Exception: - self.assertFalse(t.filling()) + self.assertFalse(self.turtle.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()) + 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): - with patch_screen(): - t = turtle.Turtle() - - self.assertFalse(t._creatingPoly) - t.begin_poly() - self.assertTrue(t._creatingPoly) - t.end_poly() - self.assertFalse(t._creatingPoly) + 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. - with patch_screen(): - t = turtle.Turtle() - - self.assertFalse(t._creatingPoly) - with t.poly(): - self.assertTrue(t._creatingPoly) - self.assertFalse(t._creatingPoly) + 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. - with patch_screen(): - t = turtle.Turtle() try: - with t.poly(): - self.assertTrue(t._creatingPoly) + with self.turtle.poly(): + self.assertTrue(self.turtle._creatingPoly) raise Exception except Exception: - self.assertFalse(t._creatingPoly) + self.assertFalse(self.turtle._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) + 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): From fd89c957b3526d2d7a06615bfd0f5474804adbb8 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sat, 9 Nov 2024 04:59:10 +0000 Subject: [PATCH 14/19] Address review comments Co-authored-by: Marie Roald --- Lib/test/test_turtle.py | 8 ++++---- Lib/turtle.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 232e8c8f4a6fd4..5773b0c50bc9fd 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -594,8 +594,8 @@ def test_fill_resets_after_exception(self): try: with self.turtle.fill(): self.assertTrue(self.turtle.filling()) - raise Exception - except Exception: + raise ValueError + except ValueError: self.assertFalse(self.turtle.filling()) def test_fill_context_when_filling(self): @@ -625,8 +625,8 @@ def test_poly_resets_after_exception(self): try: with self.turtle.poly(): self.assertTrue(self.turtle._creatingPoly) - raise Exception - except Exception: + raise ValueError + except ValueError: self.assertFalse(self.turtle._creatingPoly) def test_poly_context_when_creating_poly(self): diff --git a/Lib/turtle.py b/Lib/turtle.py index fa66474a035521..e0b21beac4c1ab 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -1287,7 +1287,8 @@ def no_animation(self): is too slow. Once this context manager is exited, the drawing will be displayed. - Example (for a TurtleScreen instance named screen): + Example (for a TurtleScreen instance named screen + and a Turtle instance named turtle): >>> with screen.no_animation(): ... turtle.circle(50) """ From fe3975106f7fcb4db20aeb7ef1a4c54bea68425c Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Sat, 9 Nov 2024 18:35:39 +0000 Subject: [PATCH 15/19] Add missing blank line Co-authored-by: Yngve Mardal Moe --- Doc/library/turtle.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index a76e92486c236c..4526a4e2a7ce21 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -2009,6 +2009,7 @@ Animation control ... fd(dist) ... rt(90) ... dist += 2 + .. versionadded:: next From 0a5e250a735d1adc5f2c2f7ee6ba3230af850432 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Sat, 9 Nov 2024 21:40:42 +0100 Subject: [PATCH 16/19] Update Lib/turtle.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/turtle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/turtle.py b/Lib/turtle.py index e0b21beac4c1ab..3794251de9c69c 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -3406,7 +3406,7 @@ def filling(self): @contextmanager def fill(self): - """Create a filled shape + """Create a filled shape. No argument. From 6d381ecd5cd9a3d31bd53e6b812ec5c7e477d111 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Thu, 14 Nov 2024 05:44:39 +0100 Subject: [PATCH 17/19] Apply suggestions from code review Co-authored-by: Erlend E. Aasland --- Doc/library/turtle.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index 4526a4e2a7ce21..e96d55e84d14ea 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -1315,7 +1315,7 @@ Filling >>> with turtle.fill(): ... turtle.circle(80) - Using ``fill`` is equivalent to adding the :func:`begin_fill` before the + Using :func:`!fill` is equivalent to adding the :func:`begin_fill` before the fill-block and :func:`end_fill` after the fill-block: .. doctest:: @@ -1996,9 +1996,9 @@ Animation control .. function:: no_animation() - Temporarilly disable turtle animation. The code written inside the - ``no_animation`` block will not be animated, and once the code block is - exited, the drawing will appear. + 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 From 42d678d16df15227b3b3d9b57757f2e8cd6ce74c Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Thu, 14 Nov 2024 06:04:25 +0100 Subject: [PATCH 18/19] Update Lib/turtle.py Co-authored-by: Erlend E. Aasland --- Lib/turtle.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/turtle.py b/Lib/turtle.py index 3794251de9c69c..7ed2753ece6ea5 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -3406,12 +3406,10 @@ def filling(self): @contextmanager def fill(self): - """Create a filled shape. + """A context manager for filling a shape. - No argument. - - This function sets up a context manager that will automatically end - filling once exited. + Implicitly ensures the code block is wrapped with + begin_fill() and end_fill(). Example (for a Turtle instance named turtle): >>> turtle.color("black", "red") From 792830658b5229b09d074f344468483340b94216 Mon Sep 17 00:00:00 2001 From: Marie Roald Date: Thu, 14 Nov 2024 05:07:50 +0000 Subject: [PATCH 19/19] Address reviewer comments Co-authored-by: Yngve Mardal Moe --- Doc/library/turtle.rst | 11 ++++------- Lib/turtle.py | 9 +++------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index e96d55e84d14ea..b0cd7fe8750636 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -216,10 +216,9 @@ useful when working with learners for whom typing is not a skill. Automatically begin and end filling ----------------------------------- -If you have Python 3.14 or later, you don't need to call :func:`begin_fill` and -: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:: +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): @@ -2004,11 +2003,9 @@ Animation control :skipif: _tkinter is None >>> with screen.no_animation(): - ... dist = 2 - ... for i in range(200): + ... for dist in range(2, 400, 2): ... fd(dist) ... rt(90) - ... dist += 2 .. versionadded:: next diff --git a/Lib/turtle.py b/Lib/turtle.py index 7ed2753ece6ea5..8638a2dd2a80d3 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -3547,13 +3547,10 @@ def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")): @contextmanager def poly(self): - """Record the vertices of a polygon. + """A context manager for recording the vertices of a polygon. - No argument. - - Record the vertices of a polygon. Current turtle position is first point - of polygon. This function sets up a context manager that will - automatically end recording once exited. + 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: