Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/fontra_glyphs/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import openstep_plist
from fontra.backends.base import WritableBaseBackend
from fontra.backends.filewatcher import Change
from fontra.backends.includedfeaturefiles import extractIncludedFeatureFiles
from fontra.backends.watchable import WatchableBackend
from fontra.core import kernutils
from fontra.core.classes import (
Expand Down Expand Up @@ -124,9 +125,10 @@ def fromPath(cls, path: PathLike) -> WritableFontBackend:
self._setupFromPath(path)
return self

def __init__(self):
def __init__(self) -> None:
super().__init__()
self._writeLock = asyncio.Lock()
self._includedFeaturePaths: list[pathlib.Path] = []

def _setupFromPath(self, path: PathLike) -> None:
self.path = pathlib.Path(path)
Expand Down Expand Up @@ -540,6 +542,12 @@ async def _getFeatures(self) -> OpenTypeFeatures:
return OpenTypeFeatures(text=invalidFeatures)

featureText = glyphsLib.builder.features._to_ufo_features(self.gsFont)

self._includedFeaturePaths = extractIncludedFeatureFiles(
featureText, self.path.parent
)
self._updatePathsToWatch()

if not canParseFeatures(featureText, self.glyphNameToIndex.keys()):
expandedFeatures = await runInSubProcess(expensiveGetFeatures, self.path)
if expandedFeatures:
Expand Down Expand Up @@ -1017,14 +1025,28 @@ async def findGlyphsThatUseGlyph(self, glyphName: str) -> list[str]:
return sorted(usedBy)

def fileWatcherWasInstalled(self):
self.fileWatcher.setPaths([self.path])
self._updatePathsToWatch()

def _updatePathsToWatch(self):
if self.fileWatcher is not None:
self.fileWatcher.setPaths([self.path, *self._includedFeaturePaths])

async def fileWatcherProcessChanges(
self, changes: set[tuple[Change, str]]
) -> dict[str, Any] | None:
reloadPattern: dict[str, Any] = {}
glyphChanges = set()

featuresChanged = False
for change, path in changes:
if path.endswith(".fea"):
featuresChanged = True

if featuresChanged and len(changes) == 1:
self._cachedFeatures = None
reloadPattern["features"] = None
return reloadPattern

rawFontData, rawGlyphsData = self._loadFiles()

if rawFontData != self.rawFontData:
Expand Down Expand Up @@ -1067,6 +1089,10 @@ async def fileWatcherProcessChanges(
if glyphChanges or glyphMapChanged:
self._updateRawGlyphsData(rawGlyphsData)

if featuresChanged:
self._cachedFeatures = None
reloadPattern["features"] = None

return reloadPattern


Expand Down
30 changes: 27 additions & 3 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,17 @@ def sourceNameMappingFromSources(fontSources):
}


def _getCopiedBackend(srcPath, tmpdir):
def _getCopiedBackend(srcPath, tmpdir, copyFeaFiles=False):
dstPath = tmpdir / os.path.basename(srcPath)
if os.path.isdir(srcPath):
shutil.copytree(srcPath, dstPath)
else:
shutil.copy(srcPath, dstPath)

if copyFeaFiles:
for feaPath in srcPath.parent.glob("*.fea"):
shutil.copy(feaPath, tmpdir / feaPath.name)

return getFileSystemBackend(dstPath)


Expand All @@ -68,8 +73,8 @@ def testFont(request):


@pytest.fixture
def externalFeaturesFileFont():
return getFileSystemBackend(externalFeaturesFilePath)
def externalFeaturesFileFont(tmpdir):
return _getCopiedBackend(externalFeaturesFilePath, tmpdir, True)


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -1092,6 +1097,25 @@ async def test_externalChanges_putFeatures(writableTestFont):
assert features == listenerFeatures


async def test_externalChanges_includedFeatureFile(externalFeaturesFileFont):
listenerFont = getFileSystemBackend(externalFeaturesFileFont.path)
listenerHandler = await setupFontHandler(listenerFont)

async with aclosing(listenerHandler):
listenerFeatures = await listenerHandler.getFeatures() # load in cache

featureFilePath = (
externalFeaturesFileFont.path.parent / "ExternalFeatureFile.fea"
)
assert featureFilePath.is_file()
featureFilePath.write_text("# dummy comment\n")

await asyncio.sleep(0.15) # give the file watcher a moment to catch up

listenerFeatures = await listenerHandler.getFeatures()
assert "dummy comment" in listenerFeatures.text


async def test_deleteUnknownGlyph(writableTestFont):
glyphName = "A.doesnotexist"
glyphMap = await writableTestFont.getGlyphMap()
Expand Down
Loading