/ client_code / non_blocking.py
non_blocking.py
1 # SPDX-License-Identifier: MIT 2 # 3 # Copyright (c) 2021 The Anvil Extras project team members listed at 4 # https://github.com/anvilistas/anvil-extras/graphs/contributors 5 # 6 # This software is published at https://github.com/anvilistas/anvil-extras 7 8 from functools import partial as _partial 9 10 from anvil.js import report_exceptions as _report 11 from anvil.js import window as _W 12 from anvil.server import call_s as _call_s 13 14 __version__ = "3.1.0" 15 16 try: 17 # just for a nice repr by default 18 _call_s.__name__ = "call_s" 19 _call_s.__qualname__ = "anvil.server.call_s" 20 except AttributeError: 21 pass 22 23 # python errors get wrapped when called from a js function in python 24 # so instead reject the error from a js function in js 25 _deferred = _W.Function( 26 "fn", 27 """ 28 const deferred = { status: "PENDING", error: null }; 29 30 deferred.promise = new Promise(async (resolve, reject) => { 31 try { 32 resolve(await fn()); 33 deferred.status = "FULFILLED"; 34 } catch (e) { 35 deferred.status = "REJECTED"; 36 deferred.error = e; 37 reject(e); 38 } 39 }); 40 41 let handledResult = deferred.promise; 42 let handledError = null; 43 44 return Object.assign(deferred, { 45 on_result(resultHandler, errorHandler) { 46 if (!errorHandler && handledError) { 47 // the on_error was already called so provide a dummy handler; 48 errorHandler = () => {}; 49 } 50 handledResult = deferred.promise.then(resultHandler, errorHandler); 51 handledError = null; 52 }, 53 on_error(errorHandler) { 54 handledError = handledResult.catch(errorHandler); 55 handledResult = deferred.promise; 56 }, 57 await_result: async () => await deferred.promise, 58 }); 59 """, 60 ) 61 62 63 class _Result: 64 # dicts may come back as javascript object literals 65 # so wrap the results in a more opaque python object 66 def __init__(self, value): 67 self.value = value 68 69 @staticmethod 70 def wrap(fn): 71 def wrapper(): 72 return _Result(fn()) 73 74 return wrapper 75 76 @staticmethod 77 def unwrap(fn): 78 def unwrapper(res): 79 return fn(res.value) 80 81 return unwrapper 82 83 84 class _AsyncCall: 85 def __init__(self, fn, *args, **kws): 86 self._fn = _partial(fn, *args, **kws) 87 self._deferred = _deferred(_Result.wrap(self._fn)) 88 89 def _check_pending(self): 90 if self._deferred.status == "PENDING": 91 raise RuntimeError("the async call is still pending") 92 93 @property 94 def result(self): 95 """If the function call is not complete, raises a RuntimeError 96 If the function call is complete: 97 Returns: the return value from the function call 98 Raises: the error raised by the function call 99 """ 100 self._check_pending() 101 return self.await_result() 102 103 @property 104 def error(self): 105 """Returns the error raised by the function call, else None""" 106 self._check_pending() 107 return self._deferred.error 108 109 @property 110 def status(self): 111 """Returns: 'PENDING', 'FULFILLED', 'REJECTED'""" 112 return self._deferred.status 113 114 @property 115 def promise(self): 116 """Returns: JavaScript Promise that resolves to the value from the function call""" 117 return _W.Promise( 118 lambda resolve, reject: resolve( 119 self._deferred.promise.then(lambda r: r.value, reject) 120 ) 121 ) 122 123 def on_result(self, result_handler, error_handler=None): 124 error_handler = error_handler and _report(error_handler) 125 result_handler = _Result.unwrap(_report(result_handler)) 126 self._deferred.on_result(result_handler, error_handler) 127 return self 128 129 def on_error(self, error_handler): 130 self._deferred.on_error(_report(error_handler)) 131 return self 132 133 def await_result(self): 134 return self._deferred.await_result().value 135 136 def __repr__(self): 137 fn_repr = repr(self._fn).replace("functools.partial", "") 138 return f"<non_blocking.AsyncCall{fn_repr}>" 139 140 141 def call_async(fn_or_name, *args, **kws): 142 """ 143 Call a function or a server function (if a string is provided) in a non-blocking way. 144 145 Parameters 146 ---------- 147 fn_or_name: A function or the name of a server function to call. 148 """ 149 if isinstance(fn_or_name, str): 150 return _AsyncCall(_call_s, fn_or_name, *args, **kws) 151 if callable(fn_or_name): 152 return _AsyncCall(fn_or_name, *args, **kws) 153 msg = "the first argument must be a callable or the name of a server function" 154 raise TypeError(msg) 155 156 157 def wait_for(async_call_object): 158 "Wait for a non-blocking function to complete its execution" 159 if not isinstance(async_call_object, _AsyncCall): 160 raise TypeError( 161 f"expected an AsyncCall object, got {type(async_call_object).__name__}" 162 ) 163 return async_call_object.await_result() 164 165 166 class _AbstractTimerRef: 167 def _clear(self, id): 168 raise NotImplementedError("implemented by subclasses") 169 170 def __init__(self, id): 171 self._id = id 172 173 def cancel(self): 174 self._clear(self._id) 175 176 177 class _DeferRef(_AbstractTimerRef): 178 _clear = _W.clearTimeout 179 180 181 class _RepeatRef(_AbstractTimerRef): 182 _clear = _W.clearInterval 183 184 185 def cancel(ref): 186 """Cancel an active call to delay or defer 187 Parameters 188 ---------- 189 ref: should be None, or the return value from calling delay/defer 190 191 e.g. 192 >>> ref = defer(fn, 1) 193 >>> cancel(ref) 194 """ 195 if ref is None: 196 return 197 if not isinstance(ref, _AbstractTimerRef): 198 msg = "Invalid argumnet to cancel(), expected None or the return value from calling delay/defer" 199 raise TypeError(msg) 200 return ref.cancel() 201 202 203 def defer(fn, delay): 204 """Defer a function call after a set period of time has elapsed (in seconds) 205 206 Parameters 207 ---------- 208 fn : a callable that takes no args 209 delay : int | float 210 the time delay in seconds to wait before calling fn 211 212 Returns 213 ------- 214 DeferRef 215 a reference to the deferred call that can be cancelled 216 either with ref.cancel() or non_blocking.cancel(ref) 217 """ 218 return _DeferRef(_W.setTimeout(fn, delay * 1000)) 219 220 221 def repeat(fn, interval): 222 """Repeatedly call a function with a set interval (in seconds) 223 224 Parameters 225 ---------- 226 fn : a callable that takes no args 227 interval : int | float 228 the time between calls to fn 229 230 Returns 231 ------- 232 RepeatRef 233 a reference to the repeated call that can be cancelled 234 either with ref.cancel() or non_blocking.cancel(ref) 235 """ 236 return _RepeatRef(_W.setInterval(fn, interval * 1000)) 237 238 239 if __name__ == "__main__": 240 # TESTS 241 from time import sleep as _sleep 242 243 _v = 0 244 245 def _f(): 246 global _x, _v 247 _v += 1 248 if _v >= 5: 249 cancel(_x) 250 251 print("Testing repeat") 252 _x = repeat(_f, 0.01) 253 _x.cancel() 254 assert _v == 0 255 _x = repeat(_f, 0.01) 256 _sleep(0.1) 257 assert _v == 5 258 _x = repeat(_f, 0.01) 259 assert _v == 5 260 _sleep(0.1) 261 assert _v == 6 262 263 print("Testing defer") 264 _v = 0 265 _x = defer(_f, delay=0.05) 266 _sleep(0.01) 267 cancel(_x) 268 _x = defer(_f, delay=0.05) 269 _sleep(0.1) 270 assert _v == 1 271 272 print("Testing Async Call") 273 _x = call_async(lambda v: v + 1, 42) 274 assert _x.status == "PENDING" 275 try: 276 _x.result 277 except RuntimeError: 278 pass 279 else: 280 assert False 281 _v = _x.await_result() 282 assert _x.status == "FULFILLED" 283 assert _x.result == 43 284 assert _x.error is None 285 _v = None 286 287 def _f(v): 288 global _v 289 _v = v 290 291 _x.on_result(_f) 292 assert _v is None 293 _sleep(0) 294 assert _v == 43 295 _v = None 296 _x = call_async(lambda v: v + 1, "foo") 297 _x.on_result(_f) 298 _x.on_error(_f) 299 _sleep(0) 300 assert _x.status == "REJECTED" 301 assert isinstance(_v, TypeError) 302 assert _v is _x.error 303 try: 304 _x.result 305 except TypeError: 306 pass 307 else: 308 assert False 309 _v = call_async(lambda: {}).await_result() 310 assert type(_v) is dict 311 print("PASSED")