From b20464eeba7bbd9f44d2c390108d5eb416cc399d Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:46:12 +0200 Subject: [PATCH 1/6] [3.13] gh-138772: Add tests for Turtle.dot() signature (GH-138773) (cherry picked from commit 2462807b702d9a71f0bd73a6618d4948491852a0) Co-authored-by: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com> --- Lib/test/test_turtle.py | 209 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 14121a590a5026..e281cd8316a6e3 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -1,5 +1,9 @@ +import os import pickle +import re +import tempfile import unittest +import unittest.mock from test import support from test.support import import_helper from test.support import os_helper @@ -50,6 +54,34 @@ """ +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. + """ + # Create a mock screen that delegates color validation to the real TurtleScreen methods + mock_screen = unittest.mock.MagicMock() + mock_screen.__class__ = turtle._Screen + mock_screen.mode.return_value = "standard" + mock_screen._colormode = 1.0 + + def mock_iscolorstring(color): + valid_colors = {'red', 'green', 'blue', 'black', 'white', 'yellow', + 'orange', 'purple', 'pink', 'brown', 'gray', 'grey', + 'cyan', 'magenta'} + + return color in valid_colors or (isinstance(color, str) and color.startswith('#')) + + mock_screen._iscolorstring = mock_iscolorstring + mock_screen._colorstr = turtle._Screen._colorstr.__get__(mock_screen) + + return unittest.mock.patch( + "turtle._Screen.__new__", + return_value=mock_screen + ) + + class TurtleConfigTest(unittest.TestCase): def get_cfg_file(self, cfg_str): @@ -461,6 +493,183 @@ def test_teleport(self): self.assertTrue(tpen.isdown()) +class TestTurtleScreen(unittest.TestCase): + def test_save_raises_if_wrong_extension(self) -> None: + screen = unittest.mock.Mock() + + msg = "Unknown file extension: '.png', must be one of {'.ps', '.eps'}" + with ( + tempfile.TemporaryDirectory() as tmpdir, + self.assertRaisesRegex(ValueError, re.escape(msg)) + ): + turtle.TurtleScreen.save(screen, os.path.join(tmpdir, "file.png")) + + def test_save_raises_if_parent_not_found(self) -> None: + screen = unittest.mock.Mock() + + with tempfile.TemporaryDirectory() as tmpdir: + parent = os.path.join(tmpdir, "unknown_parent") + msg = f"The directory '{parent}' does not exist. Cannot save to it" + + with self.assertRaisesRegex(FileNotFoundError, re.escape(msg)): + turtle.TurtleScreen.save(screen, os.path.join(parent, "a.ps")) + + def test_save_raises_if_file_found(self) -> None: + screen = unittest.mock.Mock() + + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, "some_file.ps") + with open(file_path, "w") as f: + f.write("some text") + + msg = ( + f"The file '{file_path}' already exists. To overwrite it use" + " the 'overwrite=True' argument of the save function." + ) + with self.assertRaisesRegex(FileExistsError, re.escape(msg)): + turtle.TurtleScreen.save(screen, file_path) + + def test_save_overwrites_if_specified(self) -> None: + screen = unittest.mock.Mock() + screen.cv.postscript.return_value = "postscript" + + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, "some_file.ps") + with open(file_path, "w") as f: + f.write("some text") + + turtle.TurtleScreen.save(screen, file_path, overwrite=True) + with open(file_path) as f: + self.assertEqual(f.read(), "postscript") + + def test_save(self) -> None: + screen = unittest.mock.Mock() + screen.cv.postscript.return_value = "postscript" + + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, "some_file.ps") + + turtle.TurtleScreen.save(screen, file_path) + with open(file_path) as f: + 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() + + # Reset the Screen singleton to avoid reference leaks + self.addCleanup(setattr, turtle.Turtle, '_screen', None) + + 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) + + def test_dot_signature(self): + self.turtle.dot() + self.turtle.dot(10) + self.turtle.dot(size=10) + self.turtle.dot((0, 0, 0)) + self.turtle.dot(size=(0, 0, 0)) + self.turtle.dot("blue") + self.turtle.dot("") + self.turtle.dot(size="blue") + self.turtle.dot(20, "blue") + self.turtle.dot(20, "blue") + self.turtle.dot(20, (0, 0, 0)) + self.turtle.dot(20, 0, 0, 0) + with self.assertRaises(TypeError): + self.turtle.dot(color="blue") + self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, "_not_a_color_") + self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, (0, 0, 0, 0)) + self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, 0, 0, 0, 0) + self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, (-1, 0, 0)) + self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, -1, 0, 0) + self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, (0, 257, 0)) + self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, 0, 257, 0) + class TestModuleLevel(unittest.TestCase): def test_all_signatures(self): import inspect From 142507efacdc877b51c7e403e8c58473d1b0a71d Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Tue, 4 Nov 2025 13:57:53 +0100 Subject: [PATCH 2/6] Only backport minimal changes --- Lib/test/test_turtle.py | 150 ---------------------------------------- 1 file changed, 150 deletions(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index e281cd8316a6e3..efdb91d9b6e24e 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -1,9 +1,5 @@ -import os import pickle -import re -import tempfile import unittest -import unittest.mock from test import support from test.support import import_helper from test.support import os_helper @@ -493,90 +489,6 @@ def test_teleport(self): self.assertTrue(tpen.isdown()) -class TestTurtleScreen(unittest.TestCase): - def test_save_raises_if_wrong_extension(self) -> None: - screen = unittest.mock.Mock() - - msg = "Unknown file extension: '.png', must be one of {'.ps', '.eps'}" - with ( - tempfile.TemporaryDirectory() as tmpdir, - self.assertRaisesRegex(ValueError, re.escape(msg)) - ): - turtle.TurtleScreen.save(screen, os.path.join(tmpdir, "file.png")) - - def test_save_raises_if_parent_not_found(self) -> None: - screen = unittest.mock.Mock() - - with tempfile.TemporaryDirectory() as tmpdir: - parent = os.path.join(tmpdir, "unknown_parent") - msg = f"The directory '{parent}' does not exist. Cannot save to it" - - with self.assertRaisesRegex(FileNotFoundError, re.escape(msg)): - turtle.TurtleScreen.save(screen, os.path.join(parent, "a.ps")) - - def test_save_raises_if_file_found(self) -> None: - screen = unittest.mock.Mock() - - with tempfile.TemporaryDirectory() as tmpdir: - file_path = os.path.join(tmpdir, "some_file.ps") - with open(file_path, "w") as f: - f.write("some text") - - msg = ( - f"The file '{file_path}' already exists. To overwrite it use" - " the 'overwrite=True' argument of the save function." - ) - with self.assertRaisesRegex(FileExistsError, re.escape(msg)): - turtle.TurtleScreen.save(screen, file_path) - - def test_save_overwrites_if_specified(self) -> None: - screen = unittest.mock.Mock() - screen.cv.postscript.return_value = "postscript" - - with tempfile.TemporaryDirectory() as tmpdir: - file_path = os.path.join(tmpdir, "some_file.ps") - with open(file_path, "w") as f: - f.write("some text") - - turtle.TurtleScreen.save(screen, file_path, overwrite=True) - with open(file_path) as f: - self.assertEqual(f.read(), "postscript") - - def test_save(self) -> None: - screen = unittest.mock.Mock() - screen.cv.postscript.return_value = "postscript" - - with tempfile.TemporaryDirectory() as tmpdir: - file_path = os.path.join(tmpdir, "some_file.ps") - - turtle.TurtleScreen.save(screen, file_path) - with open(file_path) as f: - 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(): @@ -585,68 +497,6 @@ def setUp(self): # Reset the Screen singleton to avoid reference leaks self.addCleanup(setattr, turtle.Turtle, '_screen', None) - 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) - def test_dot_signature(self): self.turtle.dot() self.turtle.dot(10) From 05d6bb6fddd1213c669b6391586571fac269960d Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Tue, 4 Nov 2025 13:59:05 +0100 Subject: [PATCH 3/6] Fix whitespace --- Lib/test/test_turtle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index efdb91d9b6e24e..47135d4041e402 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -520,6 +520,7 @@ def test_dot_signature(self): self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, (0, 257, 0)) self.assertRaises(turtle.TurtleGraphicsError, self.turtle.dot, 0, 0, 257, 0) + class TestModuleLevel(unittest.TestCase): def test_all_signatures(self): import inspect From d19be3950067e7bce9bb9acacbe80c28ac4f4771 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Tue, 4 Nov 2025 14:17:33 +0100 Subject: [PATCH 4/6] Fix unittest.mock imports --- Lib/test/test_turtle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 47135d4041e402..2436002aa03f7a 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -1,5 +1,6 @@ import pickle import unittest +from unittest.mock import MagicMock, patch from test import support from test.support import import_helper from test.support import os_helper @@ -57,7 +58,7 @@ def patch_screen(): instance because instantiating it requires a display. """ # Create a mock screen that delegates color validation to the real TurtleScreen methods - mock_screen = unittest.mock.MagicMock() + mock_screen = MagicMock() mock_screen.__class__ = turtle._Screen mock_screen.mode.return_value = "standard" mock_screen._colormode = 1.0 @@ -72,7 +73,7 @@ def mock_iscolorstring(color): mock_screen._iscolorstring = mock_iscolorstring mock_screen._colorstr = turtle._Screen._colorstr.__get__(mock_screen) - return unittest.mock.patch( + return patch( "turtle._Screen.__new__", return_value=mock_screen ) From 7f7d4d3625dfc5c792e763ed0e02b6d74c6fca00 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:02:16 +0100 Subject: [PATCH 5/6] Use import unittest.mock Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/test/test_turtle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 2436002aa03f7a..f7098997e72c1f 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -1,6 +1,7 @@ import pickle import unittest -from unittest.mock import MagicMock, patch +import unittest +import unittest.mock from test import support from test.support import import_helper from test.support import os_helper @@ -58,7 +59,7 @@ def patch_screen(): instance because instantiating it requires a display. """ # Create a mock screen that delegates color validation to the real TurtleScreen methods - mock_screen = MagicMock() + mock_screen = unittest.mock.MagicMock() mock_screen.__class__ = turtle._Screen mock_screen.mode.return_value = "standard" mock_screen._colormode = 1.0 @@ -73,7 +74,7 @@ def mock_iscolorstring(color): mock_screen._iscolorstring = mock_iscolorstring mock_screen._colorstr = turtle._Screen._colorstr.__get__(mock_screen) - return patch( + return unittest.mock.patch( "turtle._Screen.__new__", return_value=mock_screen ) From 46300231c9f614fa59e64a963f0f3ce02f643faf Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Tue, 4 Nov 2025 17:10:08 +0100 Subject: [PATCH 6/6] Fix linting errors --- Lib/test/test_turtle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index f7098997e72c1f..a572ff4b9e91cc 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -1,6 +1,5 @@ import pickle import unittest -import unittest import unittest.mock from test import support from test.support import import_helper