Merge pull request #403 from mcdonc/fix/watch-change-stat-race

fix: watch loop crash when file deleted before stat() in _handle_watch_change
This commit is contained in:
Yiorgis Gozadinos 2026-06-01 15:59:50 +03:00 committed by GitHub
commit 8bf1b6be1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 22 additions and 1 deletions

View file

@ -131,7 +131,12 @@ class FSPoller(BasePoller):
return
if change in (Change.added, Change.modified):
revision = str(path.stat().st_mtime_ns) if path.exists() else None
try:
revision = str(path.stat().st_mtime_ns)
except FileNotFoundError:
# File was deleted between the watchfiles event and our
# stat() call. Skip — the deletion event will handle it.
return
await self._jobs.enqueue(
self.source_id,
uri,

View file

@ -484,6 +484,22 @@ async def test_watch_deleted_then_added_enqueues_upsert(tmp_path, jobs, sync):
assert queued[0].op is JobOp.UPSERT
@pytest.mark.asyncio
async def test_watch_added_file_deleted_before_stat_does_not_crash(tmp_path, jobs, sync):
"""If a file is deleted between the watchfiles event and the stat()
call, the handler should return silently instead of raising
FileNotFoundError and killing the watch loop."""
from watchfiles import Change
poller = _fs_poller(tmp_path, jobs, sync)
missing = tmp_path / "vanished.md"
# File doesn't exist — simulate Change.added arriving after deletion.
await poller._handle_watch_change(Change.added, missing)
# No job should be enqueued, and no exception should have propagated.
assert await jobs.list_jobs(source_id="local") == []
# --- FSPoller end-to-end smoke ---