Skip to content

Add Debounce, debounced, and throttled to fastcore.aio#856

Open
ncoop57 wants to merge 7 commits into
mainfrom
debounced
Open

Add Debounce, debounced, and throttled to fastcore.aio#856
ncoop57 wants to merge 7 commits into
mainfrom
debounced

Conversation

@ncoop57

@ncoop57 ncoop57 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR introduces async debouncing and throttling utilities to fastcore.aio:

  • Debounce — A class that coalesces bursts of calls to a function into a single invocation, firing wait seconds after calls stop (with optional max_wait cap). Supports leading and trailing edge modes, cross-thread safety via call_soon_threadsafe, and works as a descriptor on methods.
  • debounced(wait, max_wait=None, leading=False, trailing=True) — A decorator wrapping Debounce for easy use on functions and methods.
  • throttled(wait, leading=False) — A convenience decorator equivalent to debounced(wait, max_wait=wait), firing at most once per wait seconds.

Details

  • Debounce.__call__ schedules a background task that waits for the quiet period to elapse before firing the latest call.
  • flush() runs any pending call immediately; cancel() discards it.
  • When used as a method descriptor (__get__/__set_name__), each instance gets its own Debounce bound to partial(self.f, obj).
  • Safely handles being called from outside the running event loop via call_soon_threadsafe.

Notes

patch now works on callable objects too, not just functions (needed so it composes with Debounce). Objects have no __code__, so the co_qualname stamping from #821 is skipped for them. To fastaudit, a debounced method therefore shows up under its plain function name, not Class.name, so class-style allowlist entries won't match it.

Minimal repro:

from fastcore.utils import run, patch
from fastcore.aio import debounced
from safepyrun.core import RunPython, allow

class T: pass

@patch
def plain(self:T): return run('echo hi')

@patch
@debounced(0.1)
def deb(self:T): run('echo hi')

allow({T: ['plain', 'deb']})
t,python = T(),RunPython()

await python('t.plain()')   # 'hi' -- entry T.plain matches the stamped frame

await python('t.deb()')     # PermissionError -- the fire never presents as T.deb

@ncoop57 ncoop57 added the enhancement New feature or request label Jul 17, 2026
@ncoop57

ncoop57 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

CI failures are unrelated to these changes.

Comment thread nbs/03c_aio.ipynb
"outputs": [],
"source": [
"async def _outer(): return run_sync(_double(4))\n",
"test_eq(asyncio.run(_outer()), 8)"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This failed for me since jupyter/solveit is already running in an event loop

@ncoop57
ncoop57 marked this pull request as ready for review July 17, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant