diff options
Diffstat (limited to '')
-rw-r--r-- | share/extensions/inkex/gui/asyncme.py | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/share/extensions/inkex/gui/asyncme.py b/share/extensions/inkex/gui/asyncme.py new file mode 100644 index 0000000..5011c17 --- /dev/null +++ b/share/extensions/inkex/gui/asyncme.py @@ -0,0 +1,330 @@ +# +# Copyright 2015 Ian Denhardt <ian@zenhack.net> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/> +# +"""Convienience library for concurrency + +GUI apps frequently need concurrency, for example to avoid blocking UI while +doing some long running computation. This module provides helpers for doing +this kind of thing. + +The functions/methods here which spawn callables asynchronously +don't supply a direct way to provide arguments. Instead, the user is +expected to use a lambda, e.g:: + + holding(lck, lambda: do_stuff(1,2,3, x='hello')) + +This is because the calling function may have additional arguments which +could obscure the user's ability to pass arguments expected by the called +function. For example, in the call:: + + holding(lck, lambda: run_task(blocking=True), blocking=False) + +the blocking argument to holding might otherwise conflict with the +blocking argument to run_task. +""" +import time +import threading +from datetime import datetime, timedelta + +from functools import wraps +from typing import Any, Tuple +from gi.repository import Gdk, GLib + + +class Future: + """A deferred result + + A `Future` is a result-to-be; it can be used to deliver a result + asynchronously. Typical usage: + + >>> def background_task(task): + ... ret = Future() + ... def _task(x): + ... return x - 4 + 2 + ... thread = threading.Thread(target=lambda: ret.run(lambda: _task(7))) + ... thread.start() + ... return ret + >>> # Do other stuff + >>> print(ret.wait()) + 5 + + :func:`run` will also propogate exceptions; see it's docstring for details. + """ + + def __init__(self): + self._lock = threading.Lock() + self._value = None + self._exception = None + self._lock.acquire() + + def is_ready(self): + """Return whether the result is ready""" + result = self._lock.acquire(False) + if result: + self._lock.release() + return result + + def wait(self): + """Wait for the result. + + `wait` blocks until the result is ready (either :func:`result` or + :func:`exception` has been called), and then returns it (in the case + of :func:`result`), or raises it (in the case of :func:`exception`). + """ + with self._lock: + if self._exception is None: + return self._value + else: + raise self._exception # pylint: disable=raising-bad-type + + def result(self, value): + """Supply the result as a return value. + + ``value`` is the result to supply; it will be returned when + :func:`wait` is called. + """ + self._value = value + self._lock.release() + + def exception(self, err): + """Supply an exception as the result. + + Args: + err (Exception): an exception, which will be raised when :func:`wait` + is called. + """ + self._exception = err + self._lock.release() + + def run(self, task): + """Calls task(), and supplies the result. + + If ``task`` raises an exception, pass it to :func:`exception`. + Otherwise, pass the return value to :func:`result`. + """ + try: + self.result(task()) + except Exception as err: # pylint: disable=broad-except + self.exception(err) + + +class DebouncedSyncVar: + """A synchronized variable, which debounces its value + + :class:`DebouncedSyncVar` supports three operations: put, replace, and get. + get will only retrieve a value once it has "settled," i.e. at least + a certain amount of time has passed since the last time the value + was modified. + """ + + def __init__(self, delay_seconds=0): + """Create a new dsv with the supplied delay, and no initial value.""" + self._cv = threading.Condition() + self._delay = timedelta(seconds=delay_seconds) + + self._deadline = None + self._value = None + + self._have_value = False + + def set_delay(self, delay_seconds): + """Set the delay in seconds of the debounce.""" + with self._cv: + self._delay = timedelta(seconds=delay_seconds) + + def get(self, blocking=True, remove=True) -> Tuple[Any, bool]: + """Retrieve a value. + + Args: + blocking (bool, optional): if True, block until (1) the dsv has a value + and (2) the value has been unchanged for an amount of time greater + than or equal to the dsv's delay. Otherwise, if these conditions + are not met, return ``(None, False)`` immediately. Defaults to True. + remove (bool, optional): if True, remove the value when returning it. + Otherwise, leave it where it is.. Defaults to True. + + Returns: + Tuple[Any, bool]: Tuple (value, ok). ``value`` is the value of the variable + (if successful, see above), and ok indicates whether or not a value was + successfully retrieved. + """ + while True: + with self._cv: + + # If there's no value, either wait for one or return + # failure. + while not self._have_value: + if blocking: + self._cv.wait() + else: + return None, False # pragma: no cover + + now = datetime.now() + deadline = self._deadline + value = self._value + if deadline <= now: + # Okay, we're good. Remove the value if necessary, and + # return it. + if remove: + self._have_value = False + self._value = None + self._cv.notify() + return value, True + + # Deadline hasn't passed yet. Either wait or return failure. + if blocking: + time.sleep((deadline - now).total_seconds()) + else: + return None, False # pragma: no cover + + def replace(self, value): + """Replace the current value of the dsv (if any) with ``value``. + + replace never blocks (except briefly to aquire the lock). It does not + wait for any unit of time to pass (though it does reset the timer on + completion), nor does it wait for the dsv's value to appear or + disappear. + """ + with self._cv: + self._replace(value) + + def put(self, value): + """Set the dsv's value to ``value``. + + If the dsv already has a value, this blocks until the value is removed. + Upon completion, this resets the timer. + """ + with self._cv: + while self._have_value: + self._cv.wait() + self._replace(value) + + def _replace(self, value): + self._have_value = True + self._value = value + self._deadline = datetime.now() + self._delay + self._cv.notify() + + +def spawn_thread(func): + """Call ``func()`` in a separate thread + + Returns the corresponding :class:`threading.Thread` object. + """ + thread = threading.Thread(target=func) + thread.start() + return thread + + +def in_mainloop(func): + """Run f() in the gtk main loop + + Returns a :class:`Future` object which can be used to retrieve the return + value of the function call. + + :func:`in_mainloop` exists because Gtk isn't threadsafe, and therefore cannot be + manipulated except in the thread running the Gtk main loop. :func:`in_mainloop` + can be used by other threads to manipulate Gtk safely. + """ + future = Future() + + def handler(*_args, **_kwargs): + """Function to be called in the future""" + future.run(func) + + Gdk.threads_add_idle(0, handler, None) + return future + + +def mainloop_only(f): + """A decorator which forces a function to only be run in Gtk's main loop. + + Invoking a decorated function as ``f(*args, **kwargs)`` is equivalent to + using the undecorated function (from a thread other than the one running + the Gtk main loop) as:: + + in_mainloop(lambda: f(*args, **kwargs)).wait() + + :func:`mainloop_only` should be used to decorate functions which are unsafe + to run outside of the Gtk main loop. + """ + + @wraps(f) + def wrapper(*args, **kwargs): + if GLib.main_depth(): + # Already in a mainloop, so just run it. + return f(*args, **kwargs) + return in_mainloop(lambda: f(*args, **kwargs)).wait() + + return wrapper + + +def holding(lock, task, blocking=True): + """Run task() while holding ``lock``. + + Args: + blocking (bool, optional): if True, wait for the lock before running. + Otherwise, if the lock is busy, return None immediately, and don't + spawn `task`. Defaults to True. + + Returns: + Union[Future, None]: The return value is a future which can be used to retrieve + the result of running task (or None if the task was not run). + """ + if not lock.acquire(False): + return None + ret = Future() + + def _target(): + ret.run(task) + if ret._exception: # pragma: no cover + ret.wait() + lock.release() + + threading.Thread(target=_target).start() + return ret + + +def run_or_wait(func): + """A decorator which runs the function using :func:`holding` + + This function creates a single lock for this function and + waits for the lock to release before returning. + + See :func:`holding` above, with ``blocking=True`` + """ + lock = threading.Lock() + + def _inner(*args, **kwargs): + return holding(lock, lambda: func(*args, **kwargs), blocking=True) + + return _inner + + +def run_or_none(func): + """A decorator which runs the function using :func:`holding` + + This function creates a single lock for this function and + returns None if the process is already running (locked) + + See :func:`holding` above with ``blocking=True`` + """ + lock = threading.Lock() + + def _inner(*args, **kwargs): + return holding(lock, lambda: func(*args, **kwargs), blocking=False) + + return _inner |