Skip to content
Merged
Changes from all 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
61 changes: 61 additions & 0 deletions Lib/test/test_turtle.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pickle
import unittest
import unittest.mock
from test import support
from test.support import import_helper
from test.support import os_helper
Expand Down Expand Up @@ -50,6 +51,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):
Expand Down Expand Up @@ -461,6 +490,38 @@ def test_teleport(self):
self.assertTrue(tpen.isdown())


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_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")
Comment on lines +509 to +515
Copy link
Contributor

@adorilson adorilson Nov 4, 2025

Choose a reason for hiding this comment

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

These lines

self.turtle.dot(size="blue")

and

 with self.assertRaises(TypeError):
      self.turtle.dot(color="blue")

are surprising.

I've tested these and it worked as here, but it doesn't make sense.
And the docs say size is an integer, and color is a color string, as expected.
Is it a bug, alright?

(just checked before open a new issue)

Copy link
Contributor Author

@JanEricNitschke JanEricNitschke Nov 5, 2025

Choose a reason for hiding this comment

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

Yeah, it is a bit weird, but the actual logic is:

    def dot(self, size=None, *color):
        """Draw a dot with diameter size, using color.

        Optional arguments:
        size -- an integer >= 1 (if given)
        color -- a colorstring or a numeric color tuple

        Draw a circular dot with diameter size, using color.
        If size is not given, the maximum of pensize+4 and 2*pensize is used.

        Example (for a Turtle instance named turtle):
        >>> turtle.dot()
        >>> turtle.fd(50); turtle.dot(20, "blue"); turtle.fd(50)
        """
        if not color:
            if isinstance(size, (str, tuple)):
                color = self._colorstr(size)
                size = self._pensize + max(self._pensize, 4)
            else:
                color = self._pencolor
                if not size:
                    size = self._pensize + max(self._pensize, 4)
        else:
            if size is None:
                size = self._pensize + max(self._pensize, 4)
            color = self._colorstr(color)

And the correct matching typehint is

    @overload
    def dot(self, size: int | _Color | None = None) -> None: ...
    @overload
    def dot(self, size: int | None, color: _Color, /) -> None: ...
    @overload
    def dot(self, size: int | None, r: float, g: float, b: float, /) -> None: ...

The signature from the docs is

turtle.dot()
turtle.dot(size)
turtle.dot(color, /)
turtle.dot(size, color, /)
turtle.dot(size, r, g, b, /)

This makes it pretty clear the "color" is not a keyword arg, so (color="blue") shouldnt, work. But to make ("blue") and (20) work you have to have (size="blue") be valid, even though is probably isnt desired or recommended.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've opened an issue concerning size="blue": #141062

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
Expand Down
Loading