diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index a154371..047987b 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -5,10 +5,9 @@ import shutil from functools import cached_property from os import PathLike -from typing import Any +from typing import Any, Awaitable, Callable -import watchfiles -from fontra.backends.designspace import cleanupWatchFilesChanges +from fontra.backends.filewatcher import Change, FileWatcher from fontra.backends.ufo_utils import extractGlyphNameAndCodePoints from fontra.core.classes import ( GlobalAxis, @@ -86,9 +85,13 @@ def __init__(self, path: PathLike, *, create: bool = False): self._recentlyWrittenPaths: dict[str, Any] = {} self._tempGlyphCache = TimedCache() + self.fileWatcher: FileWatcher | None = None + self.fileWatcherCallbacks: list[Callable[[Any, Any], Awaitable[None]]] = [] - async def aclose(self): + async def aclose(self) -> None: self._tempGlyphCache.cancel() + if self.fileWatcher is not None: + await self.fileWatcher.aclose() def registerWrittenPath(self, path, *, deleted=False): mTime = FILE_DELETED_TOKEN if deleted else os.path.getmtime(path) @@ -227,29 +230,45 @@ async def putCustomData(self, customData: dict[str, Any]) -> None: } customDataPath.write_text(json.dumps(customData, indent=2), encoding="utf-8") - async def watchExternalChanges(self): - async for changes in watchfiles.awatch(self.path): - changes = cleanupWatchFilesChanges(changes) - glyphNames = set() - for change, path in changes: - mTime = ( - FILE_DELETED_TOKEN - if not os.path.exists(path) - else os.path.getmtime(path) - ) - if self._recentlyWrittenPaths.pop(path, None) == mTime: - # We made this change ourselves, so it is not an external change - continue - fileName = os.path.basename(path) - for gs, _ in self._iterGlyphSets(): - glyphName = gs.glifFileNames.get(fileName) - if glyphName is not None: - break + async def watchExternalChanges( + self, callback: Callable[[Any, Any], Awaitable[None]] + ) -> None: + if self.fileWatcher is None: + self.fileWatcher = FileWatcher(self._fileWatcherCallback) + self.fileWatcher.setPaths(self._getFilesToWatch()) + self.fileWatcherCallbacks.append(callback) + + def _getFilesToWatch(self) -> list[os.PathLike | str]: + return [self.path] + + async def _fileWatcherCallback(self, changes: set[tuple[Change, str]]) -> None: + reloadPattern = await self.processExternalChanges(changes) + if reloadPattern: + for callback in self.fileWatcherCallbacks: + await callback(None, reloadPattern) + + async def processExternalChanges(self, changes) -> dict | None: + glyphNames = set() + for change, path in changes: + mTime = ( + FILE_DELETED_TOKEN + if not os.path.exists(path) + else os.path.getmtime(path) + ) + if self._recentlyWrittenPaths.pop(path, None) == mTime: + # We made this change ourselves, so it is not an external change + continue + fileName = os.path.basename(path) + for gs, _ in self._iterGlyphSets(): + glyphName = gs.glifFileNames.get(fileName) if glyphName is not None: - glyphNames.add(glyphName) - if glyphNames: - self._tempGlyphCache.clear() - yield None, {"glyphs": dict.fromkeys(glyphNames)} + break + if glyphName is not None: + glyphNames.add(glyphName) + if glyphNames: + self._tempGlyphCache.clear() + return {"glyphs": dict.fromkeys(glyphNames)} + return None class RCJKGlyphSet: diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index bd0df67..f26c45e 100644 --- a/src/fontra_rcjk/backend_mysql.py +++ b/src/fontra_rcjk/backend_mysql.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from random import random -from typing import Any, AsyncGenerator +from typing import Any, Awaitable, Callable from fontra.backends.designspace import makeGlyphMapChange from fontra.core.classes import ( @@ -78,9 +78,13 @@ def __init__(self, client, fontUID, cacheDir=None): self._glyphMap = None self._glyphMapTask = None self._defaultLocation = None + self.watcherCallbacks = [] + self.watcherTask = None async def aclose(self): self._tempFontItemsCache.cancel() + if self.watcherTask is not None: + self.watcherTask.cancel() async def getGlyphMap(self) -> dict[str, list[int]]: await self._ensureGlyphMap() @@ -410,7 +414,14 @@ async def _callGlyphMethod(self, glyphName, methodName, *args, **kwargs): method = getattr(self.client, apiMethodName) return await method(self.fontUID, glyphInfo.glyphID, *args, **kwargs) - async def watchExternalChanges(self) -> AsyncGenerator[tuple[Any, Any], None]: + async def watchExternalChanges( + self, callback: Callable[[Any, Any], Awaitable[None]] + ) -> None: + self.watcherCallbacks.append(callback) + if self.watcherTask is None: + self.watcherTask = asyncio.create_task(self._watchExternalChangesLoop()) + + async def _watchExternalChangesLoop(self): await self._ensureGlyphMap() errorDelay = 30 while True: @@ -423,7 +434,8 @@ async def watchExternalChanges(self) -> AsyncGenerator[tuple[Any, Any], None]: await asyncio.sleep(errorDelay) else: if externalChange or reloadPattern: - yield externalChange, reloadPattern + for callback in self.watcherCallbacks: + await callback(externalChange, reloadPattern) async def _pollOnceForChanges(self) -> tuple[Any, Any]: try: