Created
August 31, 2020 08:04
-
-
Save duangsuse/6b9160c9a5ac7706e6165885fc07238d to your computer and use it in GitHub Desktop.
Teek command.py / structure.py rewrite (partially)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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