Skip to content

Commit c7d5e6a

Browse files
gh-143070: Use "+" instead of "!" in automatically generated tkinter widget names
The "!" prefix has a special meaning in the tag expressions of the canvas and text widgets ("!", "&&", "||", "^" and parentheses), so an automatically generated widget name could not be used as a tag. "+" has no special meaning there, nor in option database patterns or Tcl lists, and a user is very unlikely to start an explicit name with it.
1 parent 868d9a8 commit c7d5e6a

5 files changed

Lines changed: 44 additions & 4 deletions

File tree

Lib/test/test_tkinter/test_misc.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,27 @@ class Button2(tkinter.Button):
4545
self.assertNotEqual(str(f), str(f2))
4646
b = tkinter.Button(f2)
4747
b2 = Button2(f2)
48-
for name in str(b).split('.') + str(b2).split('.'):
48+
for w in (t, f, f2, b, b2):
49+
# The full path name starts with a dot, the name of the root.
50+
self.assertTrue(str(w).startswith('.'), msg=repr(str(w)))
51+
name = w.winfo_name()
52+
# A generated name is not empty and contains no dot, which would
53+
# be interpreted as a path name component separator.
54+
self.assertTrue(name, msg=repr(name))
55+
self.assertNotIn('.', name, msg=repr(name))
56+
# A generated name can be used not only as a window name, but also
57+
# as a canvas or text tag, an option database pattern or a Tcl list
58+
# element, so it must avoid characters that are special there.
59+
# It is marked so as not to look like a user-chosen name.
4960
self.assertFalse(name.isidentifier(), msg=repr(name))
61+
# A capital letter starts a class name in an option pattern.
62+
self.assertFalse(name[0].isupper(), msg=repr(name))
63+
# "!&|^()" are operators in canvas tag expressions (gh-143070),
64+
# "*" separates words in an option pattern, and whitespace and
65+
# "{}[]\\"$;" are special in Tcl lists and scripts.
66+
self.assertNotRegex(name, r'[][!&|^()*\s{}"\\$;]', msg=repr(name))
67+
# "-", "@" and "~" are special only as the first character.
68+
self.assertNotIn(name[0], '-@~', msg=repr(name))
5069
b3 = tkinter.Button(f2)
5170
b4 = Button2(f2)
5271
self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4)

Lib/test/test_tkinter/test_text.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,9 @@ def test_image(self):
389389
# An embedded image occupies a single index position.
390390
self.assertEqual(text.index('end - 1 char'), '1.3')
391391

392+
# The image name can be used as an index; it is matched as a whole.
393+
self.assertEqual(text.index(name), '1.1')
394+
392395
# Either a name or an image is required, and the index must be valid.
393396
self.assertRaises(TclError, text.image_create, '1.0')
394397
self.assertRaises(TclError, text.image_create, 'invalid',
@@ -405,6 +408,11 @@ def test_window(self):
405408
(str(button),))
406409
self.assertEqual(text.window_cget('1.1', 'window'), str(button))
407410

411+
# The window can be addressed by its name where an index is expected;
412+
# the name is matched as a whole rather than parsed (gh-143070).
413+
self.assertEqual(text.index(str(button)), '1.1')
414+
self.assertEqual(text.window_cget(str(button), 'window'), str(button))
415+
408416
text.window_configure('1.1', padx=5)
409417
self.assertEqual(text.window_cget('1.1', 'padx'), 5)
410418

Lib/test/test_tkinter/test_widgets.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,6 +1483,12 @@ def test_find(self):
14831483
for result in (c.find_all(), c.find_withtag(r1)):
14841484
self.assertIsInstance(result, tuple)
14851485

1486+
# An automatically generated widget name can be used as a tag
1487+
# (gh-143070).
1488+
w = tkinter.Frame(c)
1489+
r4 = c.create_window(0, 0, window=w, tags=str(w))
1490+
self.assertEqual(c.find_withtag(str(w)), (r4,))
1491+
14861492
self.assertRaises(TclError, c.find_closest, 'spam', 0)
14871493
self.assertRaises(TclError, c.find_enclosed, 0, 0, 'spam', 0)
14881494
self.assertRaises(TclError, c.find_overlapping, 0, 0, 'spam', 0)

Lib/tkinter/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2949,16 +2949,20 @@ def _setup(self, master, cnf):
29492949
del cnf['name']
29502950
if not name:
29512951
name = self.__class__.__name__.lower()
2952+
# Generated names are marked with a leading "+", which a user is
2953+
# unlikely to use, so they do not clash with explicit names.
2954+
# "+" is also one of the few symbols with no special meaning in
2955+
# canvas and text tag expressions, so the name can be used as a tag.
29522956
if name[-1].isdigit():
2953-
name += "!" # Avoid duplication when calculating names below
2957+
name += "+" # Avoid duplication when calculating names below
29542958
if master._last_child_ids is None:
29552959
master._last_child_ids = {}
29562960
count = master._last_child_ids.get(name, 0) + 1
29572961
master._last_child_ids[name] = count
29582962
if count == 1:
2959-
name = '!%s' % (name,)
2963+
name = '+%s' % (name,)
29602964
else:
2961-
name = '!%s%d' % (name, count)
2965+
name = '+%s%d' % (name, count)
29622966
self._name = name
29632967
if master._w=='.':
29642968
self._w = '.' + name
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Automatically generated :mod:`tkinter` widget names now start with ``"+"``
2+
instead of ``"!"``, so that they can be used as tags in the
3+
:class:`!tkinter.Canvas` and :class:`!tkinter.Text` widgets.

0 commit comments

Comments
 (0)