From 09b1f10ef7b1183d40fe08e56d42dc6152d31f9a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 30 Oct 2025 13:11:56 +0200 Subject: [PATCH] gh-140481: Improve error message when trying to iterate a Tk widget, image or font (GH-140501) --- Lib/test/test_tkinter/test_font.py | 11 ++++ Lib/test/test_tkinter/test_images.py | 52 +++++++++++-------- Lib/test/test_tkinter/test_misc.py | 12 +++++ Lib/tkinter/__init__.py | 3 ++ Lib/tkinter/font.py | 2 + ...-10-23-13-42-15.gh-issue-140481.XKxWpq.rst | 1 + 6 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-23-13-42-15.gh-issue-140481.XKxWpq.rst diff --git a/Lib/test/test_tkinter/test_font.py b/Lib/test/test_tkinter/test_font.py index 3616da54cf70..fc50f9fdbb58 100644 --- a/Lib/test/test_tkinter/test_font.py +++ b/Lib/test/test_tkinter/test_font.py @@ -1,3 +1,4 @@ +import collections.abc import unittest import tkinter from tkinter import font @@ -118,6 +119,16 @@ class FontTest(AbstractTkTest, unittest.TestCase): repr(self.font), f'' ) + def test_iterable_protocol(self): + self.assertNotIsSubclass(font.Font, collections.abc.Iterable) + self.assertNotIsSubclass(font.Font, collections.abc.Container) + self.assertNotIsInstance(self.font, collections.abc.Iterable) + self.assertNotIsInstance(self.font, collections.abc.Container) + with self.assertRaisesRegex(TypeError, 'is not iterable'): + iter(self.font) + with self.assertRaisesRegex(TypeError, 'is not a container or iterable'): + self.font in self.font + class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): diff --git a/Lib/test/test_tkinter/test_images.py b/Lib/test/test_tkinter/test_images.py index 38371fe00d6e..358a18beee25 100644 --- a/Lib/test/test_tkinter/test_images.py +++ b/Lib/test/test_tkinter/test_images.py @@ -1,3 +1,4 @@ +import collections.abc import unittest import tkinter from test import support @@ -61,7 +62,33 @@ class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): self.assertRaises(RuntimeError, tkinter.PhotoImage) -class BitmapImageTest(AbstractTkTest, unittest.TestCase): +class BaseImageTest: + def create(self): + return self.image_class('::img::test', master=self.root, + file=self.testfile) + + def test_bug_100814(self): + # gh-100814: Passing a callable option value causes AttributeError. + with self.assertRaises(tkinter.TclError): + self.image_class('::img::test', master=self.root, spam=print) + image = self.image_class('::img::test', master=self.root) + with self.assertRaises(tkinter.TclError): + image.configure(spam=print) + + def test_iterable_protocol(self): + image = self.create() + self.assertNotIsSubclass(self.image_class, collections.abc.Iterable) + self.assertNotIsSubclass(self.image_class, collections.abc.Container) + self.assertNotIsInstance(image, collections.abc.Iterable) + self.assertNotIsInstance(image, collections.abc.Container) + with self.assertRaisesRegex(TypeError, 'is not iterable'): + iter(image) + with self.assertRaisesRegex(TypeError, 'is not a container or iterable'): + image in image + + +class BitmapImageTest(BaseImageTest, AbstractTkTest, unittest.TestCase): + image_class = tkinter.BitmapImage @classmethod def setUpClass(cls): @@ -144,26 +171,15 @@ class BitmapImageTest(AbstractTkTest, unittest.TestCase): self.assertEqual(image['foreground'], '-foreground {} {} #000000 yellow') - def test_bug_100814(self): - # gh-100814: Passing a callable option value causes AttributeError. - with self.assertRaises(tkinter.TclError): - tkinter.BitmapImage('::img::test', master=self.root, spam=print) - image = tkinter.BitmapImage('::img::test', master=self.root) - with self.assertRaises(tkinter.TclError): - image.configure(spam=print) - -class PhotoImageTest(AbstractTkTest, unittest.TestCase): +class PhotoImageTest(BaseImageTest, AbstractTkTest, unittest.TestCase): + image_class = tkinter.PhotoImage @classmethod def setUpClass(cls): AbstractTkTest.setUpClass.__func__(cls) cls.testfile = support.findfile('python.gif', subdir='tkinterdata') - def create(self): - return tkinter.PhotoImage('::img::test', master=self.root, - file=self.testfile) - def colorlist(self, *args): if tkinter.TkVersion >= 8.6 and self.wantobjects: return args @@ -282,14 +298,6 @@ class PhotoImageTest(AbstractTkTest, unittest.TestCase): image.configure(palette='3/4/2') self.assertEqual(image['palette'], '3/4/2') - def test_bug_100814(self): - # gh-100814: Passing a callable option value causes AttributeError. - with self.assertRaises(tkinter.TclError): - tkinter.PhotoImage('::img::test', master=self.root, spam=print) - image = tkinter.PhotoImage('::img::test', master=self.root) - with self.assertRaises(tkinter.TclError): - image.configure(spam=print) - def test_blank(self): image = self.create() image.blank() diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 0c76e07066f8..32e2329506e7 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -1,3 +1,4 @@ +import collections.abc import functools import unittest import tkinter @@ -508,6 +509,17 @@ class MiscTest(AbstractTkTest, unittest.TestCase): widget.selection_range(0, 'end') self.assertEqual(widget.selection_get(), '\u20ac\0abc\x00def') + def test_iterable_protocol(self): + widget = tkinter.Entry(self.root) + self.assertNotIsSubclass(tkinter.Entry, collections.abc.Iterable) + self.assertNotIsSubclass(tkinter.Entry, collections.abc.Container) + self.assertNotIsInstance(widget, collections.abc.Iterable) + self.assertNotIsInstance(widget, collections.abc.Container) + with self.assertRaisesRegex(TypeError, 'is not iterable'): + iter(widget) + with self.assertRaisesRegex(TypeError, 'is not a container or iterable'): + widget in widget + class WmTest(AbstractTkTest, unittest.TestCase): diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 9526d8b949fa..c54530740395 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1848,6 +1848,7 @@ class Misc: return self.tk.call(self._w, 'cget', '-' + key) __getitem__ = cget + __iter__ = None # prevent using __getitem__ for iteration def __setitem__(self, key, value): self.configure({key: value}) @@ -4280,6 +4281,8 @@ class Image: def __getitem__(self, key): return self.tk.call(self.name, 'configure', '-'+key) + __iter__ = None # prevent using __getitem__ for iteration + def configure(self, **kw): """Configure the image.""" res = () diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index 7aed523cce37..896e910d69f6 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -114,6 +114,8 @@ class Font: def __setitem__(self, key, value): self.configure(**{key: value}) + __iter__ = None # prevent using __getitem__ for iteration + def __del__(self): try: if self.delete_font: diff --git a/Misc/NEWS.d/next/Library/2025-10-23-13-42-15.gh-issue-140481.XKxWpq.rst b/Misc/NEWS.d/next/Library/2025-10-23-13-42-15.gh-issue-140481.XKxWpq.rst new file mode 100644 index 000000000000..1f511c3b9d05 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-23-13-42-15.gh-issue-140481.XKxWpq.rst @@ -0,0 +1 @@ +Improve error message when trying to iterate a Tk widget, image or font. -- 2.47.3