Skip to content

Commit ec7e49f

Browse files
committed
gh-154801: expose Tcl notifier to external event loops
1 parent b0ba60e commit ec7e49f

3 files changed

Lines changed: 928 additions & 2 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import os
2+
import select
3+
import time
4+
import unittest
5+
6+
from tkinter import _tkinter
7+
8+
9+
class TkinterEventHandler:
10+
def __init__(self, proc, clientdata):
11+
self.proc = proc
12+
self.clientdata = clientdata
13+
self.ready_events = 0
14+
self.dispatched = False
15+
16+
def process_event(self, flags):
17+
if not (flags & _tkinter.FILE_EVENTS):
18+
return False
19+
20+
events = self.ready_events
21+
self.ready_events = 0
22+
23+
if events:
24+
self.dispatched = True
25+
26+
return True
27+
28+
def __call__(self, events):
29+
if self.ready_events == 0:
30+
_tkinter.queue_event(self.process_event)
31+
32+
self.ready_events = events
33+
34+
35+
class EventSource:
36+
def __init__(self):
37+
self.handlers = {}
38+
self.timer_abstime = 0
39+
40+
def set_timer(self, t):
41+
if t is None:
42+
self.timer_abstime = 0
43+
else:
44+
sec, usec = t
45+
self.timer_abstime = (
46+
time.perf_counter_ns() // 1000
47+
+ sec * 1_000_000
48+
+ usec
49+
)
50+
51+
_tkinter.set_service_mode(_tkinter.SERVICE_ALL)
52+
53+
def wait_for_event(self, timeout):
54+
return 0
55+
56+
def create_file_handler(self, fd, mask, proc, clientdata):
57+
if mask & _tkinter.READABLE:
58+
self.handlers[fd] = TkinterEventHandler(
59+
proc,
60+
clientdata,
61+
)
62+
63+
def delete_file_handler(self, fd):
64+
self.handlers.pop(fd, None)
65+
66+
def init_notifier(self):
67+
return None
68+
69+
def finalize_notifier(self, clientdata):
70+
return None
71+
72+
def alert_notifier(self, clientdata):
73+
return None
74+
75+
def service_mode_hook(self, mode):
76+
return None
77+
78+
def setup(self):
79+
if self.timer_abstime:
80+
now = time.perf_counter_ns() // 1000
81+
timeout_usec = self.timer_abstime - now
82+
timeout = max(
83+
0.0,
84+
timeout_usec / 1_000_000,
85+
)
86+
else:
87+
timeout = None
88+
89+
return timeout, list(self.handlers)
90+
91+
def check(self, ready_events):
92+
if self.timer_abstime:
93+
now = time.perf_counter_ns() // 1000
94+
if self.timer_abstime <= now:
95+
self.timer_abstime = 0
96+
97+
ready_rlist, _, _ = ready_events
98+
99+
for fd in ready_rlist:
100+
handler = self.handlers.get(fd)
101+
if handler is not None:
102+
handler(_tkinter.READABLE)
103+
104+
_tkinter.service_all()
105+
106+
107+
source = EventSource()
108+
109+
_tkinter.set_notifier(
110+
source.set_timer,
111+
source.wait_for_event,
112+
source.create_file_handler,
113+
source.delete_file_handler,
114+
source.init_notifier,
115+
source.finalize_notifier,
116+
source.alert_notifier,
117+
source.service_mode_hook,
118+
)
119+
120+
INTERP = _tkinter.create(
121+
None,
122+
"",
123+
"Tk",
124+
False,
125+
1,
126+
False,
127+
False,
128+
None,
129+
)
130+
131+
132+
class TclNotifierTests(unittest.TestCase):
133+
134+
def test_set_notifier_is_one_shot(self):
135+
with self.assertRaises(RuntimeError):
136+
_tkinter.set_notifier(
137+
None,
138+
None,
139+
None,
140+
None,
141+
None,
142+
None,
143+
None,
144+
None,
145+
)
146+
147+
def test_queue_event_is_serviced(self):
148+
_tkinter.set_service_mode(_tkinter.SERVICE_ALL)
149+
150+
served = []
151+
152+
def callback(flags):
153+
served.append(flags)
154+
return True
155+
156+
_tkinter.queue_event(callback)
157+
_tkinter.service_all()
158+
159+
self.assertEqual(len(served), 1)
160+
161+
def test_file_handler_registration_via_external_loop(self):
162+
rfd, wfd = os.pipe()
163+
164+
self.addCleanup(os.close, rfd)
165+
self.addCleanup(os.close, wfd)
166+
167+
def callback(fd, mask):
168+
os.read(fd, 64)
169+
170+
INTERP.createfilehandler(
171+
rfd,
172+
_tkinter.READABLE,
173+
callback,
174+
)
175+
176+
self.addCleanup(
177+
INTERP.deletefilehandler,
178+
rfd,
179+
)
180+
181+
self.assertIn(rfd, source.handlers)
182+
183+
def test_timer_via_external_loop(self):
184+
called = []
185+
186+
def callback():
187+
called.append(True)
188+
189+
INTERP.createtimerhandler(
190+
1,
191+
callback,
192+
)
193+
194+
deadline = time.monotonic() + 1.0
195+
196+
while time.monotonic() < deadline and not called:
197+
timeout, fds = source.setup()
198+
199+
remaining = max(
200+
0.0,
201+
deadline - time.monotonic(),
202+
)
203+
204+
if timeout is None:
205+
timeout = remaining
206+
else:
207+
timeout = min(timeout, remaining)
208+
209+
ready = select.select(
210+
fds,
211+
[],
212+
[],
213+
timeout,
214+
)
215+
216+
source.check(ready)
217+
218+
self.assertTrue(called)
219+
220+
def test_external_loop_dispatches_ready_event(self):
221+
rfd, wfd = os.pipe()
222+
223+
self.addCleanup(os.close, rfd)
224+
self.addCleanup(os.close, wfd)
225+
226+
INTERP.createfilehandler(
227+
rfd,
228+
_tkinter.READABLE,
229+
lambda fd, mask: None,
230+
)
231+
232+
self.addCleanup(
233+
INTERP.deletefilehandler,
234+
rfd,
235+
)
236+
237+
os.write(wfd, b"ready")
238+
239+
timeout, fds = source.setup()
240+
241+
self.assertIn(rfd, fds)
242+
243+
ready = select.select(
244+
fds,
245+
[],
246+
[],
247+
timeout,
248+
)
249+
250+
source.check(ready)
251+
252+
handler = source.handlers[rfd]
253+
254+
self.assertTrue(
255+
handler.dispatched,
256+
"file-handler event was not queued and serviced",
257+
)
258+
259+
260+
if __name__ == "__main__":
261+
unittest.main()

0 commit comments

Comments
 (0)