forked from serwy/tkthread
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_willdispatch.py
More file actions
342 lines (271 loc) · 9.23 KB
/
_willdispatch.py
File metadata and controls
342 lines (271 loc) · 9.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
#
# Copyright 2021, 2022 Roger D. Serwy
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
_willdispatch.py
This Tkinter multithreading support makes use of the undocumented
`.willdispatch()` function that then bypasses the `WaitForMainloop()`
C function in _tkinter.c.
The C code already checks the running thread and serializes the
request to the main thread with `Tkapp_ThreadSend()`. That
function acquires a mutex before adding the event to the Tcl event queue.
There is a threading race condition, where `dispatching` can be set to
zero before the thread reads it. This is why there is a retry loop on
the function call.
"""
from __future__ import print_function
import traceback
import sys
import threading
try:
import queue
except:
import Queue as queue # Py2.7
from . import tk
try:
# Python 3.4+
_main_thread_ = threading.main_thread()
except AttributeError:
# Python 3.3 support
# we will assume that import is done on the main thread
_main_thread_ = threading.current_thread()
class _tk_dispatcher(object):
""" Force `WaitForMainloop` to return 1"""
_RETRY_COUNT = 10
def __init__(self, tk):
self.__tk = tk
def __getattr__(self, name):
return getattr(self.__tk, name)
# ---------------------------
# create the needed functions
# ---------------------------
def _make_func(name):
def wrapped(self, *args, **kwargs):
_tk = self.__tk
func = getattr(_tk, name)
rcount = _tk_dispatcher._RETRY_COUNT
for n in range(rcount + 1):
_tk.willdispatch()
try:
result = func(*args, **kwargs)
break
except RuntimeError as exc:
# There is a race condition between this thread
# and the main thread event loop setting dispatch=0.
# As rare as the condition is, it needs to be handled.
if n < rcount:
try:
traceback.print_exc(file=sys.stderr)
print('retrying %r %i of %i' % (name, n+1, rcount),
file=sys.stderr)
except:
# pythonw.exe sys.stderr is None
pass
continue
else:
raise
return result
return wrapped
_locals = locals()
for _name in ['call', 'createcommand', 'deletecommand',
'setvar', 'unsetvar', 'getvar',
'globalsetvar', 'globalunsetvar', 'globalgetvar']:
_locals[_name] = _make_func(_name)
del _locals
del _name
del _make_func
class _Tk(tk.Tk):
def __init__(self, *args, **kw):
# GOAL: ensure init is performed on main thread
# for Tcl/Tk to be bound to it.
# WHY: matplotlib can create figure managers anywhere
def _actual_init():
tk._Tk_original_.__init__(self, *args, **kw)
# patch the existing Tkapp object
self.tk = _tk_dispatcher(self.tk)
if threading.current_thread() is _main_thread_:
# on main thread, proceed
_actual_init()
else:
# using Tk?
if len(args) >= 4:
# hack, assuming arg order
usetk = args[3]
else:
usetk = kw.get('useTk', True)
if usetk:
# we are using Tk, not on main thread
if tk._default_root is None:
raise Exception(
'Tcl/Tk default root instance not initialized. '
'Try using `tkthread.tkinstall(ensure_root=True)`'
)
main(sync=True)(_actual_init)
else:
# since we are not using Tk, let it initialize
# on this thread
_actual_init()
def tkinstall(ensure_root=False):
"""Replace tkinter's `Tk` class with a thread-enabled version"""
import platform
runtime = platform.python_implementation()
if 'cpython' not in runtime.lower():
raise RuntimeError('Requires CPython, not %s' % runtime)
tk.__dict__.setdefault('_Tk_original_', tk.Tk) # save the original version
if tk.Tk is tk._Tk_original_:
tk.Tk = _Tk
if ensure_root:
if tk._default_root:
tk._default_root.update() # in case of enqueued destruction
if tk._default_root is None:
w = tk.Tk()
w.withdraw()
return w
else:
return tk._default_root
def _tkuninstall():
"""Uninstall the thread-enabled version of Tk"""
orig = tk.__dict__.get('_Tk_original_', None)
if orig is None:
return
if tk.Tk is not orig:
tk.Tk = orig
def current(widget=None, sync=True):
"""Decorator to run callable on the current thread.
Useful for quickly changing with `tkthread.main`"""
# sync is a no-op
def wrapped(func):
func()
return func
return wrapped
_ENSURE_COUNT = 10
def _ensure_after_idle(widget, func, tries=None):
if tries is None:
tries = _ENSURE_COUNT
for n in range(tries):
widget.willdispatch()
try:
result = widget.after_idle(func)
break
except RuntimeError as exc:
# try again
pass
else:
# unable to call after_idle
raise RuntimeError('unable to call after_idle')
return result
class _NoSyncHandler:
'''
_tkinter.c will dispatch non-mainthread calls to the mainthread,
but will create a Tcl_Condition variable that blocks the calling thread.
'''
def __init__(self):
self.q = queue.Queue()
self.th = threading.Thread(
target=self._dispatcher,
name='tkthread.nosync',
)
self.th.daemon=True
self.th.start()
def _dispatcher(self):
while True:
func, args, kwargs = self.q.get()
try:
func(*args, **kwargs)
except:
# show the error
try:
traceback.print_exc(file=sys.stderr)
except:
# pythonw.exe sys.stderr is None
pass
def call(self, func, *args, **kw):
self.q.put((func, args, kw))
_nosync_handler = _NoSyncHandler()
def main(widget=None, sync=True):
"""Decorator to run callable (no arguments) on Tcl/Tk mainthread.
example:
@tkthread.main(sync=True)
def _():
... # this code runs on the main thread
"""
def wrapped(func):
w = widget
if w is None:
w = tk._default_root
if threading.current_thread() is _main_thread_:
if sync:
func()
else:
w.after_idle(func)
else:
if sync:
ev = threading.Event()
def sync_wrapped(_func=func, _ev=ev):
try:
_func()
finally:
_ev.set()
_ensure_after_idle(w, sync_wrapped)
ev.wait()
else:
_nosync_handler.call(_ensure_after_idle, w, func)
return func
return wrapped
def _callsync(sync, func, args, kwargs):
d = dict(
outerr=None, # result: output, error
func=func, # callable
args=args,
kwargs=kwargs,
)
def _handler(d=d):
func = d['func']
args = d['args']
kwargs = d['kwargs']
try:
error = None
result = func(*args, **kwargs)
except BaseException as exc:
result = None
error = exc
d['outerr'] = (result, error)
main(sync=sync)(_handler)
if sync:
result, error = d['outerr']
if error is not None:
raise error
else:
return result
def call(func, *args, **kwargs):
"""Call the function on the main thread, wait for result."""
return _callsync(True, func, args, kwargs)
def call_nosync(func, *args, **kwargs):
"""Enqueue the function for calling on the main thread, immediately return."""
return _callsync(False, func, args, kwargs)
class called_on_main:
"""Decorator to cause function call to execute on the main thread.
example:
@tkthread.called_on_main
def gui_code():
...
gui_code() # calling will automatically dispatch to main thread
"""
def __init__(self, func):
self.func = func
def __call__(self, *args, **kw):
return call(self.func, *args, **kw)
def __repr__(self):
return '<called_on_main %r>' % self.func