Skip to content

Instantly share code, notes, and snippets.

@duangsuse
Created August 31, 2020 08:04
Show Gist options
  • Save duangsuse/6b9160c9a5ac7706e6165885fc07238d to your computer and use it in GitHub Desktop.
Save duangsuse/6b9160c9a5ac7706e6165885fc07238d to your computer and use it in GitHub Desktop.
Teek command.py / structure.py rewrite (partially)
import _tkinter
import threading
import traceback
from functools import wraps
MSG_CALL_FROM_THR_MAIN = "call from main thread"
MSG_CALLED_TWICE = "called twice"
NOT_THREADSAFE = RuntimeError("call init_threads() first")
class FutureResult:
'''pending operation result, use [getValue] / [getValueOr] to wait'''
def __init__(self):
self._cond = threading.Event()
self._value = None
self._error = None
def setValue(self, value):
self._value = value
self._cond.set()
def setError(self, exc):
self._error = exc
self._cond.set()
def getValueOr(self, on_error):
self._cond.wait()
if self._error != None: on_error(self._error)
return self._value
def getValue(self): return self.getValueOr(FutureResult.rethrow)
def fold(self, done, fail):
self._cond.wait()
return done(self._value) if self._error == None else fail(self._error)
@staticmethod
def rethrow(ex): raise ex
class EventCallback:
"""An object that calls functions. Use [bind] / [__add__] or [run]"""
def __init__(self):
self._callbacks = []
@staticmethod
def stopChain(): raise callbackBreak
class CallbackBreak: pass
callbackBreak = CallbackBreak()
def isIgnoredFrame(self, frame):
'''Is a stack trace frame ignored by [bind]'''
return False
def bind(self, op, args=(), kwargs={}):
"""Schedule `callback(*args, **kwargs) to [run]."""
stack = traceback.extract_stack()
while stack and isIgnoredFrame(stack[-1]): del stack[-1]
stack_info = "".join(traceback.format_list(stack))
self._callbacks.append((op, args, kwargs, stack_info))
def __add__(self, op):
self.bind(op); return self
def remove(self, op):
"""Undo a [bind] call. only [op] is used as its identity, args are ignored"""
idx_callbacks = len(self._callbacks) -1 # start from 0
for (i, cb) in enumerate(self._callbacks):
if cb[0] == op:
del self._callbacks[idx_callbacks-i]
return
raise ValueError("not bound: %r" %op)
def run(self) -> bool:
"""Run the connected callbacks(ignore result) and print errors. If one callback requested [stopChain], return False"""
for (op, args, kwargs, stack_info) in self._callbacks:
try: op(*args, **kwargs)
except EventCallback.CallbackBreak: return False
except Exception:
# it's important that this does NOT call sys.stderr.write directly
# because sys.stderr is None when running in windows, None.write is error
(trace, rest) = traceback.format_exc().split("\n", 1)
print(trace, file=sys.stderr)
print(stack_info+rest, end="", file=sys.stderr)
break
return True
class _TclInterpreter:
def __init__(self):
assert threading.current_thread() is threading.main_thread()
self._main_thread_ident = threading.get_ident() #< faster than threading.current_thread()
self._init_threads_done = False
# tkinter does this :D i have no idea what each argument means
self._app = _tkinter.create(None, sys.argv[0], 'Tk', 1, 1, 1, 0, None)
self._app.call('wm', 'withdraw', '.')
self._app.call('package', 'require', 'Ttk')
# when a main-thread-needing function is called from another thread, a
# tuple like this is added to this queue:
#
# (func, args, kwargs, future)
#
# func is a function that MUST be called from Tk main-loop
# args and kwargs are arguments for func
# future will be set when the function has been called
self._call_queue = queue.Queue()
def isThreadMain(self): return threading.get_ident() == self._main_thread_ident
def init_threads(self, poll_interval_ms=(1_000//20) ):
assert self.isThreadMain(), MSG_CALL_FROM_THR_MAIN
assert not self._init_threads_called, MSG_CALLED_TWICE #< there is a race condition, but just ignore this
# hard-coded name is ok because there is only one of these in each Tcl interpreter
TCL_CMD_POLLER = "teek_init_threads_queue_poller"
after_id = None
def poller():
nonlocal after_id
while True:
try: item = self._call_queue.get(block=False)
except queue.Empty: break
(func, args, kwargs, future) = item
try: value = func(*args, **kwargs)
except Exception as ex: future.setError(ex)
else: future.setValue(value)
after_id = self._app.call('after', poll_interval_ms, TCL_CMD_POLLER)
self._app.createcommand(TCL_CMD_POLLER, poller)
def quit_cancel_poller():
if after_id != None: self._app.call('after', 'cancel', after_id)
teek.on_quit.connect(quit_disconnecter)
poller()
self._init_threads_done = True
# Don't make kwargs=None and then check for ==None, that's about 5% slower
def call_thread_safe(self, non_threadsafe_func, args=(), kwargs={}, *,convert_errors=True):
if threading.get_ident() == self._main_thread_ident:
return non_threadsafe_func(*args, **kwargs)
if not self._init_threads_done: raise NOT_THREADSAFE
future = _Future()
self._call_queue.put((non_threadsafe_func, args, kwargs, future))
return future.get_value()
def run(self):
assert self.isThreadMain(), MSG_CALL_FROM_THR_MAIN
self._app.mainloop(0)
def getboolean(self, arg):
return self.call_thread_safe(self._app.getboolean, [arg])
# _tkinter returns tuples when tcl represents something as a
# list internally, but this forces it to string
def get_string(self, from_underscore_tkinter):
if isinstance(from_underscore_tkinter, str):
return from_underscore_tkinter
if isinstance(from_underscore_tkinter, _tkinter.Tcl_Obj):
return from_underscore_tkinter.string
# it's probably a tuple, i think because _tkinter returns tuples when
# tcl represents something as a list internally, this forces tcl to
# represent it as a string instead
result = self.call_thread_safe(self._app.call, ['format', '%s', from_underscore_tkinter])
assert isinstance(result, str)
return result
def splitlist(self, value):
return self.call_thread_safe(self._app.splitlist, [value])
def call(self, *args):
return self.call_thread_safe(self._app.call, args)
def eval(self, code):
return self.call_thread_safe(self._app.eval, [code])
def createcommand(self, name, func):
return self.call_thread_safe(self._app.createcommand, [name, func])
def deletecommand(self, name):
return self.call_thread_safe(self._app.deletecommand, [name])
# a global _TclInterpreter instance
_interp:_TclInterpreter = None
def _onThreadMain(): return threading.current_thread() != threading.main_thread()
# these are the only functions that access _interp directly
def _get_interp():
global _interp
if _interp == None:
if not _onThreadMain(): raise NOT_THREADSAFE
_interp = _TclInterpreter()
return _interp
def run():
"""Runs the event loop until :func:`~teek.quit` is called."""
_get_interp().run()
def quit():
global _interp
if not _onThreadMain(): raise RuntimeError(MSG_CALL_FROM_THR_MAIN)
if _interp == None: return
teek.on_quit.run()
_interp.call('destroy', '.')
# to avoid a weird errors, see test_weird_error in test_tcl_calls.py
isTeekCmd = lambda it: it.startswith('teek_command_')
for cmd in filter(isTeekCmd, teek.tcl_call([str], 'info', 'commands')): delete_command(cmd)
_interp = None
def init_threads(poll_interval_ms=50):
"""Allow using teek from other threads than the main thread.
This is implemented with a queue "poller". This function starts an
after-callback(msec) that checks for new messages in the queue,
and when another thread calls a teek function that does a Tcl call,
the information required for making the Tcl call is putted into the queue and
the Tcl call is done by the after callback.
.. note::
After callbacks don't work without the event loop, so make sure to run
the event loop with :func:`.run` after calling :func:`.init_threads`.
``poll_interval_ms`` can be given to specify a different interval than 50
milliseconds.
When a Tcl call is done from another thread, that thread blocks until the
after callback has handled it, which is slow. If this is a problem, there
are two things you can do:
* Use a smaller ``poll_interval_ms``. Watch your CPU usage though; if you
make ``poll_interval_ms`` too small, you might get 100% CPU usage when
your program is doing nothing.
* Try to rewrite the program so that it does less teek stuff in threads.
"""
_get_interp().init_threads(poll_interval_ms)
def make_thread_safe(op):
'''
A decorator that makes a function safe to be called from any thread, (and it runs in the main thread).
If you have a function runs a lot of Tk update and will be called asynchronous, better decorate with this (also it will be faster)
[op] should not block the main event loop.
'''
@wraps(op)
def safe(*args, **kwargs):
return _get_interp().call_thread_safe(op, args, kwargs)
return safe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment