watchdog Observer filesystem events in tmp_path: working versions (tested 2026-10-15)
Watch a directory with watchdog Observer and assert on created, modified, moved and deleted events delivered on a background thread, using pytest tmp_path.
passed6 passed · 0 failed2026-10-15 16:06 UTC- Status
- passed
- Last passed
- 2026-10-15 16:06 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
watchdog@6.0.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38764699834
The code
Exactly the files that passed. Your agent can pull them with get_recipe("watchdog-events-tmpdir") (install), or see them on GitHub.
requirements.txt
watchdog==6.0.0 pytest==9.1.1
watcher.py
import queue
import time
from pathlib import Path
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
class EventCollector(FileSystemEventHandler):
def __init__(self):
self.queue = queue.Queue()
def on_any_event(self, event):
self.queue.put(event)
def clear(self):
while True:
try:
self.queue.get_nowait()
except queue.Empty:
return
def wait_for(self, predicate, timeout=5.0):
deadline = time.monotonic() + timeout
seen = []
while time.monotonic() < deadline:
try:
event = self.queue.get(timeout=0.05)
except queue.Empty:
continue
seen.append(event)
if predicate(event):
return event
raise TimeoutError(f"no event matched, saw {[e.event_type for e in seen]}")
class DirectoryWatcher:
def __init__(self, path, recursive=True):
self.path = Path(path)
self.collector = EventCollector()
self.observer = Observer()
self.observer.schedule(self.collector, str(self.path), recursive=recursive)
def __enter__(self):
self.observer.start()
self._wait_ready()
return self.collector
def __exit__(self, *exc):
self.observer.stop()
self.observer.join(timeout=5)
def _wait_ready(self, timeout=5.0):
# Observer.start() returns before the inotify watch exists, and the
# probe's own create/delete events are delivered asynchronously, so the
# queue is drained explicitly to keep the next assertions clean.
deadline = time.monotonic() + timeout
attempt = 0
while time.monotonic() < deadline:
attempt += 1
name = f".ready-{attempt}"
probe = self.path / name
probe.write_text("")
try:
self.collector.wait_for(
lambda e, name=name: Path(e.src_path).name == name, timeout=0.3
)
except TimeoutError:
probe.unlink()
continue
probe.unlink()
try:
self.collector.wait_for(
lambda e, name=name: Path(e.src_path).name == name
and e.event_type == "deleted",
timeout=0.5,
)
except TimeoutError:
pass
self.collector.clear()
return
raise TimeoutError("observer did not become ready")
tests/test_watcher.py
from pathlib import Path
from watcher import DirectoryWatcher
def is_file_event(event_type, path):
return lambda e: (
e.event_type == event_type
and not e.is_directory
and Path(e.src_path) == path
)
def test_created_file(tmp_path):
target = tmp_path / "hello.txt"
with DirectoryWatcher(tmp_path) as events:
target.write_text("hi")
event = events.wait_for(is_file_event("created", target))
assert Path(event.src_path) == target
def test_modified_file(tmp_path):
target = tmp_path / "data.txt"
target.write_text("one")
with DirectoryWatcher(tmp_path) as events:
target.write_text("two")
event = events.wait_for(is_file_event("modified", target))
assert Path(event.src_path) == target
def test_moved_file_reports_dest(tmp_path):
src = tmp_path / "a.txt"
src.write_text("x")
dst = tmp_path / "b.txt"
with DirectoryWatcher(tmp_path) as events:
src.rename(dst)
event = events.wait_for(
lambda e: e.event_type == "moved" and Path(e.src_path) == src
)
assert Path(event.dest_path) == dst
def test_deleted_file(tmp_path):
target = tmp_path / "gone.txt"
target.write_text("x")
with DirectoryWatcher(tmp_path) as events:
target.unlink()
event = events.wait_for(is_file_event("deleted", target))
assert Path(event.src_path) == target
def test_directory_events_are_flagged(tmp_path):
sub = tmp_path / "folder"
with DirectoryWatcher(tmp_path) as events:
sub.mkdir()
event = events.wait_for(
lambda e: e.is_directory
and e.event_type == "created"
and Path(e.src_path) == sub
)
assert event.is_directory
def test_recursive_watches_existing_subdir(tmp_path):
sub = tmp_path / "nested"
sub.mkdir()
with DirectoryWatcher(tmp_path, recursive=True) as events:
target = sub / "leaf.txt"
target.write_text("x")
event = events.wait_for(is_file_event("created", target))
assert Path(event.src_path) == target
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.