def debounce(interval_s, keyed_by=None):
def wrapper(func):<EOL><INDENT>timers = {}<EOL>lock = threading.Lock()<EOL>@functools.wraps(func)<EOL>def debounced(*args, **kwargs):<EOL><INDENT>call_args = inspect.getcallargs(func, *args, **kwargs)<EOL>key = call_args[keyed_by] if keyed_by else None<EOL>def run():<EOL><INDENT>with lock:<EOL><INDENT>del timers[key]<EOL><DEDENT>return func(*args, **kwargs)<EOL><DEDENT>with lock:<EOL><INDENT>old_timer = timers.get(key)<EOL>if old_timer:<EOL><INDENT>old_timer.cancel()<EOL><DEDENT>timer = threading.Timer(interval_s, run)<EOL>timers[key] = timer<EOL>timer.start()<EOL><DEDENT><DEDENT>return debounced<EOL><DEDENT>return wrapper<EOL>
Debounce calls to this function until interval_s seconds have passed.