From 3ab440ac98c62802d9714ad16e79f93d31ca6161 Mon Sep 17 00:00:00 2001 From: Matthias Kievernagel Date: Sun, 13 May 2018 21:46:17 +0200 Subject: [PATCH 1/6] bpo-3405: Add support for user data of Tk virtual events in tkinter (%d substitution). --- Lib/tkinter/__init__.py | 15 ++++++++++++--- .../2018-05-11-12-26-16.bpo-3405.CacMw9.rst | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index bf0b3b92155938..177d9195bc4822 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -251,6 +251,10 @@ class Event: type - type of the event as a number widget - widget in which the event occurred delta - delta of wheel movement (MouseWheel) + detail - certain fixed strings (see tcl/tk documentation) + (Enter, Leave, FocusIn, FocusOut, ConfigureRequest) + user_data - data string which was passed to event_generate or empty string + (VirtualEvent) """ def __repr__(self): @@ -1595,7 +1599,7 @@ def _root(self): w = self while w.master is not None: w = w.master return w - _subst_format = ('%#', '%b', '%f', '%h', '%k', + _subst_format = ('%#', '%b', '%d', '%f', '%h', '%k', '%s', '%t', '%w', '%x', '%y', '%A', '%E', '%K', '%N', '%W', '%T', '%X', '%Y', '%D') _subst_format_str = " ".join(_subst_format) @@ -1613,11 +1617,14 @@ def getint_event(s): except (ValueError, TclError): return s - nsign, b, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args - # Missing: (a, c, d, m, o, v, B, R) + nsign, b, d, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args + # Missing: (a, c, m, o, v, B, R) e = Event() # serial field: valid for all events # number of button: ButtonPress and ButtonRelease events only + # detail: for Enter, Leave, FocusIn, FocusOut and ConfigureRequest + # events certain fixed strings (see tcl/tk documentation) + # user_data: data string from a virtual event or an empty string # height field: Configure, ConfigureRequest, Create, # ResizeRequest, and Expose events only # keycode field: KeyPress and KeyRelease events only @@ -1631,6 +1638,8 @@ def getint_event(s): # KeyRelease, and Motion events e.serial = getint(nsign) e.num = getint_event(b) + e.user_data = d + e.detail = d try: e.focus = getboolean(f) except TclError: pass e.height = getint_event(h) diff --git a/Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst b/Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst new file mode 100644 index 00000000000000..8caa5bccddb3c3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst @@ -0,0 +1 @@ +Add support for user data of Tk virtual events to :mod:`tkinter`. From bebae490264ce9f628b83cc7ae1c1abb2db4cc26 Mon Sep 17 00:00:00 2001 From: Matthias Kievernagel Date: Sun, 20 May 2018 18:17:39 +0200 Subject: [PATCH 2/6] Tests added to test_tk for virtual events --- Lib/tkinter/test/test_tkinter/test_event.py | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Lib/tkinter/test/test_tkinter/test_event.py diff --git a/Lib/tkinter/test/test_tkinter/test_event.py b/Lib/tkinter/test/test_tkinter/test_event.py new file mode 100644 index 00000000000000..f09693af32f505 --- /dev/null +++ b/Lib/tkinter/test/test_tkinter/test_event.py @@ -0,0 +1,47 @@ +import unittest +import tkinter +import _tkinter +from test.support import requires, run_unittest +from tkinter.test.support import AbstractTkTest + +requires('gui') + +class EventTest(AbstractTkTest, unittest.TestCase): + + called = False + test_data = None + + # pump_events function 'borrowed' from ivan_pozdeev on stackoverflow + def pump_events(self): + while self.root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT): + pass + + def test_virtual_event(self): + def receive(e): + EventTest.called = True + self.assertIsInstance(e, tkinter.Event) + self.assertEqual(e.type, tkinter.EventType.VirtualEvent) + self.assertIsInstance(e.user_data, str) + if EventTest.test_data is not None: + self.assertEqual(e.user_data, EventTest.test_data) + self.pump_events() + EventTest.called = False + b = self.root.bind('<>', lambda e:receive(e)) + self.root.event_generate('<>') + self.pump_events() + self.assertTrue(EventTest.called) + EventTest.called = False + EventTest.test_data = 'test' + self.root.event_generate('<>', data='test') + self.pump_events() + self.assertTrue(EventTest.called) + self.root.unbind('<>', b) + EventTest.called = False + self.root.event_generate('<>') + self.pump_events() + self.assertFalse(EventTest.called) + +tests_gui = (EventTest, ) + +if __name__ == "__main__": + run_unittest(*tests_gui) From 36067fe3e4e5af65498d0e8fc062913028904cd7 Mon Sep 17 00:00:00 2001 From: Matthias Kievernagel Date: Sun, 27 May 2018 16:51:47 +0200 Subject: [PATCH 3/6] Also test for empty user_data --- Lib/tkinter/test/test_tkinter/test_event.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/tkinter/test/test_tkinter/test_event.py b/Lib/tkinter/test/test_tkinter/test_event.py index f09693af32f505..d21c32ba709fd8 100644 --- a/Lib/tkinter/test/test_tkinter/test_event.py +++ b/Lib/tkinter/test/test_tkinter/test_event.py @@ -24,6 +24,8 @@ def receive(e): self.assertIsInstance(e.user_data, str) if EventTest.test_data is not None: self.assertEqual(e.user_data, EventTest.test_data) + else: + self.assertEqual(e.user_data, '') self.pump_events() EventTest.called = False b = self.root.bind('<>', lambda e:receive(e)) From 0ec1de188432b41fd9b6688188ac089e73032e66 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 24 Feb 2026 18:40:26 +0200 Subject: [PATCH 4/6] Rewrite tests. --- Lib/test/test_tkinter/test_misc.py | 23 ++++++++++ Lib/tkinter/test/test_tkinter/test_event.py | 49 --------------------- 2 files changed, 23 insertions(+), 49 deletions(-) delete mode 100644 Lib/tkinter/test/test_tkinter/test_event.py diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index a6ba55b3fcadb3..06b8b8e9cf345e 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -636,6 +636,8 @@ def test_focus(self): self.assertEqual(e.x_root, '??') self.assertEqual(e.y_root, '??') self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, 'NotifyAncestor') + self.assertEqual(e.detail, 'NotifyAncestor') self.assertEqual(repr(e), '') def test_configure(self): @@ -669,6 +671,8 @@ def test_configure(self): self.assertEqual(e.x_root, '??') self.assertEqual(e.y_root, '??') self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') self.assertEqual(repr(e), '') def test_event_generate_key_press(self): @@ -705,6 +709,8 @@ def test_event_generate_key_press(self): self.assertEqual(e.x_root, -1) self.assertEqual(e.y_root, -1) self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') self.assertEqual(repr(e), f"") @@ -740,6 +746,8 @@ def test_event_generate_enter(self): self.assertEqual(e.x_root, 100 + f.winfo_rootx()) self.assertEqual(e.y_root, 50 + f.winfo_rooty()) self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, 'NotifyAncestor') + self.assertEqual(e.detail, 'NotifyAncestor') self.assertEqual(repr(e), '') def test_event_generate_button_press(self): @@ -774,6 +782,8 @@ def test_event_generate_button_press(self): self.assertEqual(e.x_root, f.winfo_rootx() + 100) self.assertEqual(e.y_root, f.winfo_rooty() + 50) self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') self.assertEqual(repr(e), '') def test_event_generate_motion(self): @@ -808,6 +818,8 @@ def test_event_generate_motion(self): self.assertEqual(e.x_root, f.winfo_rootx() + 100) self.assertEqual(e.y_root, f.winfo_rooty() + 50) self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') self.assertEqual(repr(e), '') def test_event_generate_mouse_wheel(self): @@ -842,6 +854,8 @@ def test_event_generate_mouse_wheel(self): self.assertEqual(e.x_root, f.winfo_rootx() + 100) self.assertEqual(e.y_root, f.winfo_rooty() + 50) self.assertEqual(e.delta, -5) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, '??') self.assertEqual(repr(e), '') def test_generate_event_virtual_event(self): @@ -876,9 +890,18 @@ def test_generate_event_virtual_event(self): self.assertEqual(e.x_root, f.winfo_rootx() + 50) self.assertEqual(e.y_root, -1) self.assertEqual(e.delta, 0) + self.assertEqual(e.user_data, '') + self.assertEqual(e.detail, '') self.assertEqual(repr(e), f"") + f.event_generate('<>', data='spam') + self.assertEqual(len(events), 2, events) + e = events[1] + self.assertIs(e.type, tkinter.EventType.VirtualEvent) + self.assertEqual(e.user_data, 'spam') + self.assertEqual(e.detail, 'spam') + class BindTest(AbstractTkTest, unittest.TestCase): diff --git a/Lib/tkinter/test/test_tkinter/test_event.py b/Lib/tkinter/test/test_tkinter/test_event.py deleted file mode 100644 index d21c32ba709fd8..00000000000000 --- a/Lib/tkinter/test/test_tkinter/test_event.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -import tkinter -import _tkinter -from test.support import requires, run_unittest -from tkinter.test.support import AbstractTkTest - -requires('gui') - -class EventTest(AbstractTkTest, unittest.TestCase): - - called = False - test_data = None - - # pump_events function 'borrowed' from ivan_pozdeev on stackoverflow - def pump_events(self): - while self.root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT): - pass - - def test_virtual_event(self): - def receive(e): - EventTest.called = True - self.assertIsInstance(e, tkinter.Event) - self.assertEqual(e.type, tkinter.EventType.VirtualEvent) - self.assertIsInstance(e.user_data, str) - if EventTest.test_data is not None: - self.assertEqual(e.user_data, EventTest.test_data) - else: - self.assertEqual(e.user_data, '') - self.pump_events() - EventTest.called = False - b = self.root.bind('<>', lambda e:receive(e)) - self.root.event_generate('<>') - self.pump_events() - self.assertTrue(EventTest.called) - EventTest.called = False - EventTest.test_data = 'test' - self.root.event_generate('<>', data='test') - self.pump_events() - self.assertTrue(EventTest.called) - self.root.unbind('<>', b) - EventTest.called = False - self.root.event_generate('<>') - self.pump_events() - self.assertFalse(EventTest.called) - -tests_gui = (EventTest, ) - -if __name__ == "__main__": - run_unittest(*tests_gui) From e3e3cd02b24c495db7037f95e9c3ba66f7c00f1c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 24 Feb 2026 19:24:56 +0200 Subject: [PATCH 5/6] Only set user_data for virtual events and detail for corresponding events. --- Lib/test/test_tkinter/test_misc.py | 17 ++++++++++++----- Lib/tkinter/__init__.py | 18 +++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 06b8b8e9cf345e..f017e94a8b3c22 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -636,7 +636,7 @@ def test_focus(self): self.assertEqual(e.x_root, '??') self.assertEqual(e.y_root, '??') self.assertEqual(e.delta, 0) - self.assertEqual(e.user_data, 'NotifyAncestor') + self.assertEqual(e.user_data, '??') self.assertEqual(e.detail, 'NotifyAncestor') self.assertEqual(repr(e), '') @@ -746,10 +746,17 @@ def test_event_generate_enter(self): self.assertEqual(e.x_root, 100 + f.winfo_rootx()) self.assertEqual(e.y_root, 50 + f.winfo_rooty()) self.assertEqual(e.delta, 0) - self.assertEqual(e.user_data, 'NotifyAncestor') + self.assertEqual(e.user_data, '??') self.assertEqual(e.detail, 'NotifyAncestor') self.assertEqual(repr(e), '') + f.event_generate('', x=100, y=50, detail='NotifyPointer') + self.assertEqual(len(events), 2, events) + e = events[1] + self.assertIs(e.type, tkinter.EventType.Enter) + self.assertEqual(e.user_data, '??') + self.assertEqual(e.detail, 'NotifyPointer') + def test_event_generate_button_press(self): f = tkinter.Frame(self.root, width=150, height=100) f.pack() @@ -858,7 +865,7 @@ def test_event_generate_mouse_wheel(self): self.assertEqual(e.detail, '??') self.assertEqual(repr(e), '') - def test_generate_event_virtual_event(self): + def test_event_generate_virtual_event(self): f = tkinter.Frame(self.root, width=150, height=100) f.pack() self.root.wait_visibility() # needed on Windows @@ -891,7 +898,7 @@ def test_generate_event_virtual_event(self): self.assertEqual(e.y_root, -1) self.assertEqual(e.delta, 0) self.assertEqual(e.user_data, '') - self.assertEqual(e.detail, '') + self.assertEqual(e.detail, '??') self.assertEqual(repr(e), f"") @@ -900,7 +907,7 @@ def test_generate_event_virtual_event(self): e = events[1] self.assertIs(e.type, tkinter.EventType.VirtualEvent) self.assertEqual(e.user_data, 'spam') - self.assertEqual(e.detail, 'spam') + self.assertEqual(e.detail, '??') class BindTest(AbstractTkTest, unittest.TestCase): diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 91dfc03070be12..ba8365f56c37a7 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -255,10 +255,10 @@ class Event: type - type of the event as a number widget - widget in which the event occurred delta - delta of wheel movement (MouseWheel) - detail - certain fixed strings (see tcl/tk documentation) + detail - certain fixed strings (see Tcl/Tk documentation) (Enter, Leave, FocusIn, FocusOut, ConfigureRequest) - user_data - data string which was passed to event_generate or empty string - (VirtualEvent) + user_data - data string which was passed to event_generate() or empty + string (VirtualEvent) """ def __repr__(self): @@ -1542,7 +1542,7 @@ def bind(self, sequence=None, func=None, add=None): for pressing A and the Alt key (KeyPress can be omitted). An event pattern can also be a virtual event of the form <> where AString can be arbitrary. This - event can be generated by event_generate. + event can be generated by event_generate(). If events are concatenated they must appear shortly after each other. @@ -1754,7 +1754,7 @@ def getint_event(s): # serial field: valid for all events # number of button: ButtonPress and ButtonRelease events only # detail: for Enter, Leave, FocusIn, FocusOut and ConfigureRequest - # events certain fixed strings (see tcl/tk documentation) + # events certain fixed strings (see Tcl/Tk documentation) # user_data: data string from a virtual event or an empty string # height field: Configure, ConfigureRequest, Create, # ResizeRequest, and Expose events only @@ -1769,8 +1769,12 @@ def getint_event(s): # KeyRelease, and Motion events e.serial = getint(nsign) e.num = getint_event(b) - e.user_data = d - e.detail = d + if T == EventType.VirtualEvent: + e.user_data = d + e.detail = '??' + else: + e.user_data = '??' + e.detail = d try: e.focus = getboolean(f) except TclError: pass e.height = getint_event(h) From 67ee37a15fbd2de1488b6b5569ffa72b320c7250 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 24 Feb 2026 19:36:07 +0200 Subject: [PATCH 6/6] Update docs. --- Doc/whatsnew/3.15.rst | 5 +++++ .../next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 816d45e0756824..37ebdfee7915fe 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1059,6 +1059,11 @@ tkinter with outdated names. (Contributed by Serhiy Storchaka in :gh:`143754`.) +* Added :class:`!Event` attributes :attr:`!user_data` for Tk virtual events + and :attr:`!detail` for ``Enter``, ``Leave``, ``FocusIn``, ``FocusOut``, + and ``ConfigureRequest`` events. + (Contributed by Matthias Kievernagel and Serhiy Storchaka in :gh:`47655`.) + .. _whatsnew315-tomllib-1-1-0: diff --git a/Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst b/Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst index 8caa5bccddb3c3..bf97bf0231fc0a 100644 --- a/Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst +++ b/Misc/NEWS.d/next/Library/2018-05-11-12-26-16.bpo-3405.CacMw9.rst @@ -1 +1,3 @@ -Add support for user data of Tk virtual events to :mod:`tkinter`. +Add support for user data of Tk virtual events and detail for +``Enter``, ``Leave``, ``FocusIn``, ``FocusOut``, and +``ConfigureRequest`` events to :mod:`tkinter`.