diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 14f741e802535f..eae61765deb607 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -45,8 +45,27 @@ class Button2(tkinter.Button): self.assertNotEqual(str(f), str(f2)) b = tkinter.Button(f2) b2 = Button2(f2) - for name in str(b).split('.') + str(b2).split('.'): + for w in (t, f, f2, b, b2): + # The full path name starts with a dot, the name of the root. + self.assertTrue(str(w).startswith('.'), msg=repr(str(w))) + name = w.winfo_name() + # A generated name is not empty and contains no dot, which would + # be interpreted as a path name component separator. + self.assertTrue(name, msg=repr(name)) + self.assertNotIn('.', name, msg=repr(name)) + # A generated name can be used not only as a window name, but also + # as a canvas or text tag, an option database pattern or a Tcl list + # element, so it must avoid characters that are special there. + # It is marked so as not to look like a user-chosen name. self.assertFalse(name.isidentifier(), msg=repr(name)) + # A capital letter starts a class name in an option pattern. + self.assertFalse(name[0].isupper(), msg=repr(name)) + # "!&|^()" are operators in canvas tag expressions (gh-143070), + # "*" separates words in an option pattern, and whitespace and + # "{}[]\\"$;" are special in Tcl lists and scripts. + self.assertNotRegex(name, r'[][!&|^()*\s{}"\\$;]', msg=repr(name)) + # "-", "@" and "~" are special only as the first character. + self.assertNotIn(name[0], '-@~', msg=repr(name)) b3 = tkinter.Button(f2) b4 = Button2(f2) self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4) diff --git a/Lib/test/test_tkinter/test_text.py b/Lib/test/test_tkinter/test_text.py index 905c194af3ac47..32fa1fa95022d5 100644 --- a/Lib/test/test_tkinter/test_text.py +++ b/Lib/test/test_tkinter/test_text.py @@ -389,6 +389,9 @@ def test_image(self): # An embedded image occupies a single index position. self.assertEqual(text.index('end - 1 char'), '1.3') + # The image name can be used as an index; it is matched as a whole. + self.assertEqual(text.index(name), '1.1') + # Either a name or an image is required, and the index must be valid. self.assertRaises(TclError, text.image_create, '1.0') self.assertRaises(TclError, text.image_create, 'invalid', @@ -405,6 +408,11 @@ def test_window(self): (str(button),)) self.assertEqual(text.window_cget('1.1', 'window'), str(button)) + # The window can be addressed by its name where an index is expected; + # the name is matched as a whole rather than parsed (gh-143070). + self.assertEqual(text.index(str(button)), '1.1') + self.assertEqual(text.window_cget(str(button), 'window'), str(button)) + text.window_configure('1.1', padx=5) self.assertEqual(text.window_cget('1.1', 'padx'), 5) diff --git a/Lib/test/test_tkinter/test_widgets.py b/Lib/test/test_tkinter/test_widgets.py index 4b7a6acfad0b92..7bd1d84b0508a1 100644 --- a/Lib/test/test_tkinter/test_widgets.py +++ b/Lib/test/test_tkinter/test_widgets.py @@ -1483,6 +1483,12 @@ def test_find(self): for result in (c.find_all(), c.find_withtag(r1)): self.assertIsInstance(result, tuple) + # An automatically generated widget name can be used as a tag + # (gh-143070). + w = tkinter.Frame(c) + r4 = c.create_window(0, 0, window=w, tags=str(w)) + self.assertEqual(c.find_withtag(str(w)), (r4,)) + self.assertRaises(TclError, c.find_closest, 'spam', 0) self.assertRaises(TclError, c.find_enclosed, 0, 0, 'spam', 0) self.assertRaises(TclError, c.find_overlapping, 0, 0, 'spam', 0) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 122f4da25de5a2..09961efb7cb07f 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -2949,16 +2949,20 @@ def _setup(self, master, cnf): del cnf['name'] if not name: name = self.__class__.__name__.lower() + # Generated names are marked with a leading "+", which a user is + # unlikely to use, so they do not clash with explicit names. + # "+" is also one of the few symbols with no special meaning in + # canvas and text tag expressions, so the name can be used as a tag. if name[-1].isdigit(): - name += "!" # Avoid duplication when calculating names below + name += "+" # Avoid duplication when calculating names below if master._last_child_ids is None: master._last_child_ids = {} count = master._last_child_ids.get(name, 0) + 1 master._last_child_ids[name] = count if count == 1: - name = '!%s' % (name,) + name = '+%s' % (name,) else: - name = '!%s%d' % (name, count) + name = '+%s%d' % (name, count) self._name = name if master._w=='.': self._w = '.' + name diff --git a/Misc/NEWS.d/next/Library/2026-06-23-11-00-03.gh-issue-143070.fV5w7s.rst b/Misc/NEWS.d/next/Library/2026-06-23-11-00-03.gh-issue-143070.fV5w7s.rst new file mode 100644 index 00000000000000..51c2dc3d51bc86 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-23-11-00-03.gh-issue-143070.fV5w7s.rst @@ -0,0 +1,3 @@ +Automatically generated :mod:`tkinter` widget names now start with ``"+"`` +instead of ``"!"``, so that they can be used as tags in the +:class:`!tkinter.Canvas` and :class:`!tkinter.Text` widgets.