1""" 2Async Module 3============ 4 5Helper utils for Orange GUI programming. 6 7Provides :func:`asynchronous` decorator for making methods calls in async mode. 8Once method is decorated it will have :func:`task.on_start`, :func:`task.on_result` and :func:`task.callback` decorators for callbacks wrapping. 9 10 - `on_start` must take no arguments 11 - `on_result` must accept one argument (the result) 12 - `callback` can accept any arguments 13 14For instance:: 15 16 class Widget(QObject): 17 def __init__(self, name): 18 super().__init__() 19 self.name = name 20 21 @asynchronous 22 def task(self): 23 for i in range(3): 24 time.sleep(0.5) 25 self.report_progress(i) 26 return 'Done' 27 28 @task.on_start 29 def report_start(self): 30 print('`{}` started'.format(self.name)) 31 32 @task.on_result 33 def report_result(self, result): 34 print('`{}` result: {}'.format(self.name, result)) 35 36 @task.callback 37 def report_progress(self, i): 38 print('`{}` progress: {}'.format(self.name, i)) 39 40 41Calling an asynchronous method will launch a daemon thread:: 42 43 first = Widget(name='First') 44 first.task() 45 second = Widget(name='Second') 46 second.task() 47 48 first.task.join() 49 second.task.join() 50 51 52A possible output:: 53 54 `First` started 55 `Second` started 56 `Second` progress: 0 57 `First` progress: 0 58 `First` progress: 1 59 `Second` progress: 1 60 `First` progress: 2 61 `First` result: Done 62 `Second` progress: 2 63 `Second` result: Done 64 65 66In order to terminate a thread either call :meth:`stop` method or raise :exc:`StopExecution` exception within :meth:`task`:: 67 68 first.task.stop() 69 70""" 71 72import threading 73from functools import wraps, partial 74from AnyQt.QtCore import pyqtSlot as Slot, QMetaObject, Qt, Q_ARG, QObject 75 76 77def safe_invoke(obj, method_name, *args): 78 try: 79 QMetaObject.invokeMethod(obj, method_name, Qt.QueuedConnection, *args) 80 except RuntimeError: 81 # C++ object wrapped by `obj` may be already destroyed 82 pass 83 84 85class CallbackMethod: 86 def __init__(self, master, instance): 87 self.instance = instance 88 self.master = master 89 90 def __call__(self, *args, **kwargs): 91 safe_invoke(self.master, 'call', Q_ARG(object, (self.instance, args, kwargs))) 92 93 94class CallbackFunction(QObject): 95 """ PyQt replacement for ordinary function. Will be always called in the main GUI thread (invoked). """ 96 97 def __init__(self, func): 98 super().__init__() 99 self.func = func 100 101 @Slot(object) 102 def call(self, scope): 103 instance, args, kwargs = scope 104 try: 105 self.func.__get__(instance, type(instance))(*args, **kwargs) 106 except RuntimeError: 107 # C++ object wrapped by `obj` may be already destroyed 108 pass 109 110 def __get__(self, instance, owner): 111 return CallbackMethod(self, instance) 112 113 114def callback(func): 115 """ Wraps QObject's method and makes its calls always invoked. """ 116 return wraps(func)(CallbackFunction(func)) 117 118 119class StopExecution(Exception): 120 """ An exception to stop execution of thread's inner cycle. """ 121 pass 122 123 124class BoundAsyncMethod(QObject): 125 def __init__(self, func, instance): 126 super().__init__() 127 if isinstance(instance, QObject): 128 instance.destroyed.connect(self.on_destroy) 129 130 self.im_func = func 131 self.im_self = instance 132 133 self.running = False 134 self._thread = None 135 136 def __call__(self, *args, **kwargs): 137 self.stop() 138 self.running = True 139 self._thread = threading.Thread(target=self.run, args=args, kwargs=kwargs, 140 daemon=True) 141 self._thread.start() 142 143 def run(self, *args, **kwargs): 144 if self.im_func.start_callback and self.im_self: 145 safe_invoke(self.im_self, self.im_func.start_callback) 146 147 if self.im_self: 148 args = (self.im_self,) + args 149 150 try: 151 result = self.im_func.method(*args, **kwargs) 152 except StopExecution: 153 result = None 154 155 if self.im_func.finish_callback and self.im_self: 156 safe_invoke(self.im_self, self.im_func.finish_callback, Q_ARG(object, result)) 157 self.running = False 158 159 def on_destroy(self): 160 self.running = False 161 self.im_self = None 162 163 def stop(self): 164 """ Terminates thread execution. """ 165 self.running = False 166 self.join() 167 168 def join(self): 169 """ Waits till task is completed. """ 170 if self._thread is not None and self._thread.is_alive(): 171 self._thread.join() 172 173 def should_break(self): 174 return not self.running 175 176 177class AsyncMethod(QObject): 178 def __init__(self, method): 179 super().__init__() 180 self.method = method 181 self.method_name = method.__name__ 182 self.finish_callback = None 183 self.start_callback = None 184 185 def __get__(self, instance, owner): 186 """ Bounds methods with instance. """ 187 bounded = BoundAsyncMethod(self, instance) 188 setattr(instance, self.method.__name__, bounded) 189 return bounded 190 191 def on_start(self, callback): 192 """ On start callback decorator. """ 193 self.start_callback = callback.__name__ 194 return Slot()(callback) 195 196 def callback(self, method=None, should_raise=True): 197 """ Callback decorator. Add checks for thread state. 198 199 Raises: 200 StopExecution: If thread was stopped (`running = False`). 201 """ 202 if method is None: 203 return partial(self.callback, should_raise=should_raise) 204 205 async_method = callback(method) 206 207 @wraps(method) 208 def wrapper(instance, *args, **kwargs): 209 # This check must take place in the background thread. 210 if should_raise and not getattr(instance, self.method_name).running: 211 raise StopExecution 212 # This call must be sent to the main thread. 213 return async_method.__get__(instance, method)(*args, **kwargs) 214 215 return wrapper 216 217 def on_result(self, callback): 218 """ On result callback decorator. """ 219 self.finish_callback = callback.__name__ 220 return Slot(object)(callback) 221 222 223def asynchronous(task): 224 """ Wraps method of a QObject and replaces it with :class:`AsyncMethod` instance 225 in order to run this method in a separate thread. 226 """ 227 return wraps(task)(AsyncMethod(task)) 228