Skip to content

Commit

Permalink
chore: try CI fixing
Browse files Browse the repository at this point in the history
  • Loading branch information
phil65 committed Dec 17, 2024
1 parent e9f2c19 commit 2d73719
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 39 deletions.
84 changes: 54 additions & 30 deletions src/llmling/monitors/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(
step_ms: int = 50,
polling: bool | None = None,
poll_delay_ms: int = 300,
max_retries: int = 3,
) -> None:
"""Initialize watcher.
Expand All @@ -47,6 +48,7 @@ def __init__(
step_ms: Time between checks (milliseconds)
polling: Whether to force polling mode (None = auto)
poll_delay_ms: Delay between polls if polling is used
max_retries: Maximum number of retries for watch failures
"""
self._running = False
self._watches: dict[str, set[str]] = {} # path -> patterns
Expand All @@ -56,6 +58,7 @@ def __init__(
self._polling = polling
self._poll_delay_ms = poll_delay_ms
self.signals = FileWatcherSignals()
self._max_retries = max_retries

async def start(self) -> None:
self._running = True
Expand All @@ -82,9 +85,14 @@ def add_watch(
raise RuntimeError(msg)

path_str = str(path)
logger.debug("Setting up watch for %s with patterns %s", path_str, patterns)
# Validate path before creating watch task
if not Path(path_str).exists():
msg = f"Path does not exist: {path_str}"
exc = FileNotFoundError(msg)
self.signals.watch_error.emit(path_str, exc)
return

# Create watch task
logger.debug("Setting up watch for %s with patterns %s", path_str, patterns)
coro = self._watch_path(path_str, patterns or ["*"])
task = asyncio.create_task(coro, name=f"watch-{path_str}")
self._tasks.add(task)
Expand All @@ -97,32 +105,48 @@ def remove_watch(self, path: str | os.PathLike[str]) -> None:

async def _watch_path(self, path: str, patterns: list[str]) -> None:
"""Watch a path and emit signals for changes."""
try:
logger.debug("Starting watch on %s with patterns %s", path, patterns)

async for changes in awatch(
path,
watch_filter=lambda _, p: any(
fnmatch.fnmatch(Path(p).name, pattern) for pattern in patterns
),
debounce=self._debounce_ms,
step=self._step_ms,
recursive=True,
):
if not self._running:
retries = self._max_retries
while retries and self._running:
try:
logger.debug("Starting watch on %s with patterns %s", path, patterns)

async for changes in awatch(
path,
watch_filter=lambda _, p: any(
fnmatch.fnmatch(Path(p).name, pattern) for pattern in patterns
),
debounce=self._debounce_ms,
step=self._step_ms,
recursive=True,
force_polling=self._polling,
poll_delay_ms=self._poll_delay_ms,
):
if not self._running:
break

for change_type, changed_path in changes:
logger.debug(
"Detected change: %s -> %s", change_type, changed_path
)
match change_type:
case Change.added:
self.signals.file_added.emit(changed_path)
case Change.modified:
self.signals.file_modified.emit(changed_path)
case Change.deleted:
self.signals.file_deleted.emit(changed_path)

except asyncio.CancelledError:
logger.debug("Watch cancelled for: %s", path)
break
except Exception as exc:
logger.warning(
"Watch error for %s: %s. Retries left: %d", path, exc, retries
)
retries -= 1
if retries:
await asyncio.sleep(1)
else:
logger.exception("Watch failed for: %s", path)
self.signals.watch_error.emit(path, exc)
break

for change_type, changed_path in changes:
logger.debug("Detected change: %s -> %s", change_type, changed_path)
match change_type:
case Change.added:
self.signals.file_added.emit(changed_path)
case Change.modified:
self.signals.file_modified.emit(changed_path)
case Change.deleted:
self.signals.file_deleted.emit(changed_path)
except asyncio.CancelledError:
logger.debug("Watch cancelled for: %s", path)
except Exception as exc:
logger.exception("Watch error for: %s", path)
self.signals.watch_error.emit(path, exc)
24 changes: 15 additions & 9 deletions tests/test_watchfiles_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def on_change(path: str) -> None:
event.set()

watcher.add_watch(test_file)
await asyncio.sleep(0.1)
await asyncio.sleep(0.3)

test_file.write_text("modified")

Expand Down Expand Up @@ -80,10 +80,10 @@ def on_change(path: str) -> None:
event.set()

watcher.add_watch(temp_dir, patterns=["*.py"])
await asyncio.sleep(0.1)
await asyncio.sleep(0.3)

py_file.write_text("python modified")
await asyncio.sleep(0.1)
await asyncio.sleep(0.3)
txt_file.write_text("text modified")

try:
Expand Down Expand Up @@ -112,7 +112,7 @@ def on_change(path: str) -> None:
event.set()

watcher.add_watch(str(test_file))
await asyncio.sleep(0.1)
await asyncio.sleep(0.3)

test_file.write_text("modified")

Expand All @@ -137,7 +137,7 @@ def on_change(path: str) -> None:
event.set()

watcher.add_watch(test_file.absolute())
await asyncio.sleep(0.1)
await asyncio.sleep(0.3)

test_file.write_text("modified")

Expand Down Expand Up @@ -176,7 +176,7 @@ def on_deleted(path: str) -> None:
event.set()

watcher.add_watch(temp_dir)
await asyncio.sleep(0.1)
await asyncio.sleep(0.3)

# Test creation
test_file.write_text("initial")
Expand All @@ -199,15 +199,21 @@ def on_deleted(path: str) -> None:
async def test_watch_error_handling(watcher: FileWatcher, temp_dir: Path) -> None:
"""Test error handling in watcher."""
errors: list[tuple[str, Exception]] = []
event = asyncio.Event()

@watcher.signals.watch_error.connect
def on_error(path: str, exc: Exception) -> None:
errors.append((path, exc))
event.set()

# Try to watch a non-existent directory
nonexistent = temp_dir / "nonexistent"
watcher.add_watch(nonexistent)

await asyncio.sleep(0.1)
assert errors, "No error reported for invalid watch"
assert str(nonexistent) in errors[0][0]
try:
await asyncio.wait_for(event.wait(), timeout=0.5)
assert errors, "No error reported for invalid watch"
assert str(nonexistent) in errors[0][0]
assert isinstance(errors[0][1], FileNotFoundError)
except TimeoutError:
pytest.fail("No error reported within timeout")

0 comments on commit 2d73719

Please sign in to comment.