/ 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")