From 9f0dacb4ef0378c1bff89173eab856daa7a2f19c Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Thu, 14 Mar 2024 10:47:47 +0100 Subject: [PATCH 1/6] Adjust to watch files protocol change --- src/fontra_rcjk/backend_fs.py | 65 +++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index a154371..235e85a 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 FileWatcher from fontra.backends.ufo_utils import extractGlyphNameAndCodePoints from fontra.core.classes import ( GlobalAxis, @@ -227,29 +226,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): + return [self.path] + + async def _fileWatcherCallback(self, changes): + 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: From f8ae329673085e43afa720ca1c9785ceb1042a2b Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Thu, 14 Mar 2024 10:51:17 +0100 Subject: [PATCH 2/6] Set up watcher attrs --- src/fontra_rcjk/backend_fs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index 235e85a..478f14a 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -85,6 +85,8 @@ 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): self._tempGlyphCache.cancel() From 3a5d79d08b343e1a41207ee51f0a0ef195e147c1 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Thu, 14 Mar 2024 10:58:59 +0100 Subject: [PATCH 3/6] Adjust to watch protocol change --- src/fontra_rcjk/backend_mysql.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index bd0df67..f9dbba3 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,6 +78,8 @@ 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() @@ -410,7 +412,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 +432,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: From 8d78da2b32fec3455fe727520a73f1b20a68a7c3 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Thu, 14 Mar 2024 12:47:34 +0100 Subject: [PATCH 4/6] Close / cancel watcher tasks --- src/fontra_rcjk/backend_fs.py | 2 ++ src/fontra_rcjk/backend_mysql.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index 478f14a..ab663be 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -90,6 +90,8 @@ def __init__(self, path: PathLike, *, create: bool = False): async def aclose(self): 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) diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index f9dbba3..f26c45e 100644 --- a/src/fontra_rcjk/backend_mysql.py +++ b/src/fontra_rcjk/backend_mysql.py @@ -83,6 +83,8 @@ def __init__(self, client, fontUID, cacheDir=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() From 283a664a51ad2ab4fe34a3468e5365f262a7b34a Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Thu, 14 Mar 2024 14:51:58 +0100 Subject: [PATCH 5/6] More typing --- src/fontra_rcjk/backend_fs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index ab663be..03c72d2 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -7,7 +7,7 @@ from os import PathLike from typing import Any, Awaitable, Callable -from fontra.backends.filewatcher import FileWatcher +from fontra.backends.filewatcher import Change, FileWatcher from fontra.backends.ufo_utils import extractGlyphNameAndCodePoints from fontra.core.classes import ( GlobalAxis, @@ -241,7 +241,7 @@ async def watchExternalChanges( def _getFilesToWatch(self): return [self.path] - async def _fileWatcherCallback(self, changes): + async def _fileWatcherCallback(self, changes: set[tuple[Change, str]]) -> None: reloadPattern = await self.processExternalChanges(changes) if reloadPattern: for callback in self.fileWatcherCallbacks: From b276072e9a39a1111a5872d83c761bbad858362e Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Thu, 14 Mar 2024 14:53:37 +0100 Subject: [PATCH 6/6] More typing --- src/fontra_rcjk/backend_fs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index 03c72d2..047987b 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -88,7 +88,7 @@ def __init__(self, path: PathLike, *, create: bool = False): 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() @@ -238,7 +238,7 @@ async def watchExternalChanges( self.fileWatcher.setPaths(self._getFilesToWatch()) self.fileWatcherCallbacks.append(callback) - def _getFilesToWatch(self): + def _getFilesToWatch(self) -> list[os.PathLike | str]: return [self.path] async def _fileWatcherCallback(self, changes: set[tuple[Change, str]]) -> None: