From 40cddd99ff659b6a1303553e8ce12845de415450 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 21:41:20 +0100 Subject: [PATCH 01/16] Beginnings of type annotations --- pyproject.toml | 8 ++++++++ src/fontra_rcjk/backend_fs.py | 31 ++++++++++++++++++++----------- src/fontra_rcjk/base.py | 2 +- src/fontra_rcjk/py.typed | 0 4 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 src/fontra_rcjk/py.typed diff --git a/pyproject.toml b/pyproject.toml index 34e0ad6..ced3f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,3 +61,11 @@ testpaths = [ "tests", ] asyncio_mode = "auto" + +[[tool.mypy.overrides]] +module = "fontTools.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "glyphsLib.glyphdata" +ignore_missing_imports = true diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index 817e989..b9d0f59 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -4,12 +4,15 @@ import pathlib import shutil from functools import cached_property +from os import PathLike +from typing import Any import watchfiles from fontra.backends.designspace import cleanupWatchFilesChanges from fontra.backends.ufo_utils import extractGlyphNameAndUnicodes -from fontra.core.classes import unstructure +from fontra.core.classes import VariableGlyph, unstructure from fontra.core.instancer import mapLocationFromUserToSource +from fontra.core.protocols import WritableFontBackend from fontTools.ufoLib.filenames import userNameToFileName from .base import ( @@ -33,14 +36,14 @@ class RCJKBackend: @classmethod - def fromPath(cls, path): + def fromPath(cls, path: PathLike) -> WritableFontBackend: return cls(path) @classmethod - def createFromPath(cls, path): + def createFromPath(cls, path: PathLike) -> WritableFontBackend: return cls(path, create=True) - def __init__(self, path, *, create=False): + def __init__(self, path: PathLike, *, create: bool = False): self.path = pathlib.Path(path).resolve() if create: if self.path.is_dir(): @@ -49,9 +52,7 @@ def __init__(self, path, *, create=False): self.path.unlink() cgPath = self.path / "characterGlyph" cgPath.mkdir(exist_ok=True, parents=True) - self.characterGlyphGlyphSet = ( - RCJKGlyphSet(cgPath, self.registerWrittenPath), - ) + self.characterGlyphGlyphSet = RCJKGlyphSet(cgPath, self.registerWrittenPath) for name in glyphSetNames: setattr( @@ -69,7 +70,7 @@ def __init__(self, path, *, create=False): else: self.designspace = {} - self._glyphMap = {} + self._glyphMap: dict[str, list[int]] = {} for gs, hasEncoding in self._iterGlyphSets(): glyphMap = gs.getGlyphMap(not hasEncoding) for glyphName, unicodes in glyphMap.items(): @@ -78,7 +79,7 @@ def __init__(self, path, *, create=False): assert not unicodes self._glyphMap[glyphName] = unicodes - self._recentlyWrittenPaths = {} + self._recentlyWrittenPaths: dict[str, Any] = {} self._tempGlyphCache = TimedCache() def close(self): @@ -114,6 +115,9 @@ def _defaultLocation(self): async def getGlyphMap(self): return dict(self._glyphMap) + async def putGlyphMap(self, glyphMap: dict[str, list[int]]) -> None: + pass + async def getGlobalAxes(self): return unpackAxes(self.designspace.get("axes", ())) @@ -132,7 +136,10 @@ def _writeDesignSpaceFile(self): async def getUnitsPerEm(self): return 1000 - async def getGlyph(self, glyphName): + async def putUnitsPerEm(self, value): + pass + + async def getGlyph(self, glyphName: str) -> VariableGlyph | None: layerGlyphs = self._getLayerGlyphs(glyphName) return buildVariableGlyphFromLayerGlyphs(layerGlyphs) @@ -168,7 +175,9 @@ def _getLayerGLIFData(self, glyphName): return gs.getGlyphLayerData(glyphName) return None - async def putGlyph(self, glyphName, glyph, unicodes): + async def putGlyph( + self, glyphName: str, glyph: VariableGlyph, unicodes: list[int] + ) -> None: if glyphName not in self._glyphMap: existingLayerGlyphs = {} else: diff --git a/src/fontra_rcjk/base.py b/src/fontra_rcjk/base.py index 5226365..ea68f63 100644 --- a/src/fontra_rcjk/base.py +++ b/src/fontra_rcjk/base.py @@ -118,7 +118,7 @@ def cleanupAxis(axisDict): return LocalAxis(**axisDict) -def buildVariableGlyphFromLayerGlyphs(layerGlyphs): +def buildVariableGlyphFromLayerGlyphs(layerGlyphs) -> VariableGlyph: layers = { layerName: Layer(glyph=glyph.toStaticGlyph()) for layerName, glyph in layerGlyphs.items() diff --git a/src/fontra_rcjk/py.typed b/src/fontra_rcjk/py.typed new file mode 100644 index 0000000..e69de29 From 77a27676220ce76649e588a26b4300716709f90a Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 21:42:40 +0100 Subject: [PATCH 02/16] Run mypy as part of CI --- .github/workflows/pytest.yml | 8 ++++++++ requirements-dev.txt | 1 + 2 files changed, 9 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index fce5a70..af7d509 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -18,20 +18,28 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install . --no-deps python -m pip install -r requirements.txt python -m pip install -r requirements-dev.txt + - name: Run pre-commit uses: pre-commit/action@v3.0.0 with: extra_args: --all-files --verbose --show-diff-on-failure + - name: Test with pytest run: | pytest -vv + + - name: Run mypy + run: | + mypy src/fontra/ test-py/ diff --git a/requirements-dev.txt b/requirements-dev.txt index 84b7bfd..217c26b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +mypy==1.7.1 pre-commit==3.5.0 pytest==7.4.3 pytest-asyncio==0.23.2 From f21f1193564db7f777194de5a421293d53d54b39 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 21:43:04 +0100 Subject: [PATCH 03/16] Fix paths --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index af7d509..d66b9ed 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -42,4 +42,4 @@ jobs: - name: Run mypy run: | - mypy src/fontra/ test-py/ + mypy src/ tests/ From 08214fbf96b748f68f80093a99aee3412a2871c6 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 21:53:25 +0100 Subject: [PATCH 04/16] Use unstructure instead of asdict --- tests/test_font.py | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/tests/test_font.py b/tests/test_font.py index a41468d..61ecf56 100644 --- a/tests/test_font.py +++ b/tests/test_font.py @@ -14,6 +14,7 @@ StaticGlyph, VariableGlyph, structure, + unstructure, ) from fontra_rcjk.base import makeSafeLayerName @@ -871,60 +872,28 @@ async def test_delete_items(writableTestFont): expectedReadMixedComponentTestData = { "name": "b", - "axes": [], "sources": [ { "name": "", "layerName": "foreground", - "location": {}, - "inactive": False, "customData": {"fontra.development.status": 0}, } ], "layers": { "foreground": { "glyph": { - "path": {"coordinates": [], "pointTypes": [], "contourInfo": []}, "components": [ - { - "name": "a", - "transformation": { - "translateX": 0, - "translateY": 0, - "rotation": 0.0, - "scaleX": 1.0, - "scaleY": 1.0, - "skewX": 0.0, - "skewY": 0.0, - "tCenterX": 0, - "tCenterY": 0, - }, - "location": {}, - }, + {"name": "a"}, { "name": "DC_0033_00", - "transformation": { - "translateX": 30, - "translateY": 0, - "rotation": 0, - "scaleX": 1, - "scaleY": 1, - "skewX": 0, - "skewY": 0, - "tCenterX": 0, - "tCenterY": 0, - }, + "transformation": {"translateX": 30}, "location": {"X_X_bo": 0, "X_X_la": 0}, }, ], "xAdvance": 500, - "yAdvance": None, - "verticalOrigin": None, }, - "customData": {}, } }, - "customData": {}, } @@ -932,7 +901,7 @@ async def test_readMixClassicAndVariableComponents(): font = getTestFont("rcjk") with contextlib.closing(font): glyph = await font.getGlyph("b") - assert expectedReadMixedComponentTestData == asdict(glyph) + assert expectedReadMixedComponentTestData == unstructure(glyph) expectedWriteMixedComponentTestData = [ From 4ff1c3fe06d8144e75e7bff2de68b09efa51b7d0 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 21:53:37 +0100 Subject: [PATCH 05/16] Add type annotations --- src/fontra_rcjk/backend_fs.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index b9d0f59..ef59d94 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -10,7 +10,12 @@ import watchfiles from fontra.backends.designspace import cleanupWatchFilesChanges from fontra.backends.ufo_utils import extractGlyphNameAndUnicodes -from fontra.core.classes import VariableGlyph, unstructure +from fontra.core.classes import ( + GlobalAxis, + GlobalDiscreteAxis, + VariableGlyph, + unstructure, +) from fontra.core.instancer import mapLocationFromUserToSource from fontra.core.protocols import WritableFontBackend from fontTools.ufoLib.filenames import userNameToFileName @@ -112,16 +117,16 @@ def _defaultLocation(self): } return mapLocationFromUserToSource(userLoc, axes) - async def getGlyphMap(self): + async def getGlyphMap(self) -> dict[str, list[int]]: return dict(self._glyphMap) async def putGlyphMap(self, glyphMap: dict[str, list[int]]) -> None: pass - async def getGlobalAxes(self): + async def getGlobalAxes(self) -> list[GlobalAxis | GlobalDiscreteAxis]: return unpackAxes(self.designspace.get("axes", ())) - async def putGlobalAxes(self, axes): + async def putGlobalAxes(self, axes: list[GlobalAxis | GlobalDiscreteAxis]) -> None: self.designspace["axes"] = unstructure(axes) if hasattr(self, "_defaultLocation"): del self._defaultLocation @@ -133,10 +138,10 @@ def _writeDesignSpaceFile(self): json.dumps(self.designspace, indent=2), encoding="utf-8" ) - async def getUnitsPerEm(self): + async def getUnitsPerEm(self) -> int: return 1000 - async def putUnitsPerEm(self, value): + async def putUnitsPerEm(self, value: int) -> None: pass async def getGlyph(self, glyphName: str) -> VariableGlyph | None: @@ -203,14 +208,14 @@ async def deleteGlyph(self, glyphName): del self._glyphMap[glyphName] - async def getCustomData(self): + async def getCustomData(self) -> dict[str, Any]: customData = {} customDataPath = self.path / FONTLIB_FILENAME if customDataPath.is_file(): customData = json.loads(customDataPath.read_text(encoding="utf-8")) return customData | standardCustomDataItems - async def putCustomData(self, customData): + async def putCustomData(self, customData: dict[str, Any]) -> None: customDataPath = self.path / FONTLIB_FILENAME customData = { k: v From 04c58ec2f03b07760a39cbad067bdc99afa45846 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 21:54:23 +0100 Subject: [PATCH 06/16] Use unstructure instead of asdict --- tests/test_font.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_font.py b/tests/test_font.py index 61ecf56..8b9b3f6 100644 --- a/tests/test_font.py +++ b/tests/test_font.py @@ -1,7 +1,6 @@ import contextlib import pathlib import shutil -from dataclasses import asdict from importlib.metadata import entry_points import pytest @@ -401,7 +400,7 @@ async def test_getGlyph(backendName, expectedGlyph): font = getTestFont(backendName) with contextlib.closing(font): glyph = await font.getGlyph(expectedGlyph.name) - assert asdict(glyph) == asdict(expectedGlyph) + assert unstructure(glyph) == unstructure(expectedGlyph) assert glyph == expectedGlyph From 2247b6dcdf3bc1c991399095db882ec84a00e857 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 21:57:22 +0100 Subject: [PATCH 07/16] For now, use fontra's protocols PR branch --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 217c26b..fc2752a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ mypy==1.7.1 pre-commit==3.5.0 pytest==7.4.3 pytest-asyncio==0.23.2 -git+https://github.com/googlefonts/fontra.git +git+https://github.com/googlefonts/fontra.git@protocols From 714f11a778dba1d8e3374fbe56caf5543d32a5fd Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 22:01:38 +0100 Subject: [PATCH 08/16] Ignore urllib3 and requests, they aren't really used --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ced3f5e..91bf191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,3 +69,11 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = "glyphsLib.glyphdata" ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "urllib3" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "requests" +ignore_missing_imports = true From 3fa5b7fca4a07df481f1043584ec753b8ff73df6 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 22:07:02 +0100 Subject: [PATCH 09/16] Add minimal type annotations, add missing methods --- src/fontra_rcjk/backend_mysql.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index 59cb943..237d500 100644 --- a/src/fontra_rcjk/backend_mysql.py +++ b/src/fontra_rcjk/backend_mysql.py @@ -10,6 +10,7 @@ from fontra.backends.designspace import makeGlyphMapChange from fontra.core.classes import VariableGlyph, structure, unstructure from fontra.core.instancer import mapLocationFromUserToSource +from fontra.core.protocols import WritableFontBackend from .base import ( GLIFGlyph, @@ -47,8 +48,10 @@ class RCJKGlyphInfo: class RCJKMySQLBackend: @classmethod - def fromRCJKClient(cls, client, fontUID, cacheDir=None): - self = cls() + def fromRCJKClient(cls, client, fontUID, cacheDir=None) -> WritableFontBackend: + return cls(client, fontUID, cacheDir) + + def __init__(self, client, fontUID, cacheDir=None): self.client = client self.fontUID = fontUID if cacheDir is not None: @@ -65,7 +68,6 @@ def fromRCJKClient(cls, client, fontUID, cacheDir=None): self._glyphMap = None self._glyphMapTask = None self._defaultLocation = None - return self def close(self): self._tempFontItemsCache.cancel() @@ -74,6 +76,9 @@ async def getGlyphMap(self): await self._ensureGlyphMap() return dict(self._glyphMap) + async def putGlyphMap(self, glyphMap): + pass + def _ensureGlyphMap(self): # Prevent multiple concurrent queries by using a single task if self._glyphMapTask is None: @@ -140,6 +145,9 @@ async def getDefaultLocation(self): async def getUnitsPerEm(self): return 1000 + async def putUnitsPerEm(self, value): + pass + async def getCustomData(self): customData = self._tempFontItemsCache.get("customData") if customData is None: From b707d3dff3647944aedf0ad41ab039e9d4d259a9 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 12 Dec 2023 22:16:58 +0100 Subject: [PATCH 10/16] Add minimal type annotations so at least the project manager gets checked --- src/fontra_rcjk/projectmanager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fontra_rcjk/projectmanager.py b/src/fontra_rcjk/projectmanager.py index 6f6e2e8..43528f2 100644 --- a/src/fontra_rcjk/projectmanager.py +++ b/src/fontra_rcjk/projectmanager.py @@ -1,11 +1,14 @@ +import argparse import logging import pathlib import secrets from importlib import resources +from types import SimpleNamespace from urllib.parse import parse_qs, quote from aiohttp import web from fontra.core.fonthandler import FontHandler +from fontra.core.protocols import ProjectManager from .backend_mysql import RCJKMySQLBackend from .client import HTTPError @@ -16,13 +19,13 @@ class RCJKProjectManagerFactory: @staticmethod - def addArguments(parser): + def addArguments(parser: argparse.ArgumentParser) -> None: parser.add_argument("rcjk_host") parser.add_argument("--read-only", action="store_true") parser.add_argument("--cache-dir") @staticmethod - def getProjectManager(arguments): + def getProjectManager(arguments: SimpleNamespace) -> ProjectManager: return RCJKProjectManager( host=arguments.rcjk_host, readOnly=arguments.read_only, From 03910af1a9a3e9c67d2f224912ccee6037e7ab23 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Wed, 13 Dec 2023 15:25:30 +0100 Subject: [PATCH 11/16] Add more typing, fix typing error --- src/fontra_rcjk/projectmanager.py | 41 +++++++++++++++++-------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/fontra_rcjk/projectmanager.py b/src/fontra_rcjk/projectmanager.py index 43528f2..daa15c4 100644 --- a/src/fontra_rcjk/projectmanager.py +++ b/src/fontra_rcjk/projectmanager.py @@ -4,6 +4,7 @@ import secrets from importlib import resources from types import SimpleNamespace +from typing import Callable from urllib.parse import parse_qs, quote from aiohttp import web @@ -43,11 +44,11 @@ def __init__(self, host, *, readOnly=False, cacheDir=None): self.cacheDir = cacheDir self.authorizedClients = {} - async def close(self): + async def close(self) -> None: for client in self.authorizedClients.values(): await client.close() - def setupWebRoutes(self, fontraServer): + def setupWebRoutes(self, fontraServer) -> None: routes = [ web.post("/login", self.loginHandler), web.post("/logout", self.logoutHandler), @@ -56,7 +57,7 @@ def setupWebRoutes(self, fontraServer): self.cookieMaxAge = fontraServer.cookieMaxAge self.startupTime = fontraServer.startupTime - async def loginHandler(self, request): + async def loginHandler(self, request: web.Request) -> web.Response: formContent = parse_qs(await request.text()) username = formContent["username"][0] password = formContent["password"][0] @@ -77,29 +78,33 @@ async def loginHandler(self, request): else: response.set_cookie("fontra-authorization-failed", "true", max_age=5) response.del_cookie("fontra-authorization-token") - raise response + return response - async def logoutHandler(self, request): + async def logoutHandler(self, request: web.Request) -> web.Response: token = request.cookies.get("fontra-authorization-token") if token is not None and token in self.authorizedClients: client = self.authorizedClients.pop(token) logger.info(f"logging out '{client.username}'") await client.close() - raise web.HTTPFound("/") + return web.HTTPFound("/") - async def authorize(self, request): + async def authorize(self, request: web.Request) -> str | None: token = request.cookies.get("fontra-authorization-token") if token not in self.authorizedClients: return None return token - async def projectPageHandler(self, request, filterContent=None): + async def projectPageHandler( + self, + request: web.Request, + filterContent: Callable[[bytes, str], bytes] | None = None, + ) -> web.Response: token = await self.authorize(request) htmlPath = resources.files("fontra_rcjk") / "landing.html" - html = htmlPath.read_text() + html = htmlPath.read_bytes() if filterContent is not None: html = filterContent(html, "text/html") - response = web.Response(text=html, content_type="text/html") + response = web.Response(body=html, content_type="text/html") if token: response.set_cookie( @@ -110,7 +115,7 @@ async def projectPageHandler(self, request, filterContent=None): return response - async def login(self, username, password): + async def login(self, username: str, password: str) -> str | None: url = f"https://{self.host}/" rcjkClient = RCJKClientAsync( host=url, @@ -130,15 +135,15 @@ async def login(self, username, password): ) return token - async def projectAvailable(self, path, token): + async def projectAvailable(self, path: str, token: str) -> bool: client = self.authorizedClients[token] return await client.projectAvailable(path) - async def getProjectList(self, token): + async def getProjectList(self, token: str) -> list[str]: client = self.authorizedClients[token] return await client.getProjectList() - async def getRemoteSubject(self, path, token): + async def getRemoteSubject(self, path: str, token: str) -> FontHandler | None: client = self.authorizedClients.get(token) if client is None: logger.info("reject unrecognized token") @@ -169,22 +174,22 @@ async def close(self): for fontHandler in self.fontHandlers.values(): await fontHandler.close() - async def projectAvailable(self, path): + async def projectAvailable(self, path: str) -> bool: await self._setupProjectList() return path in self.projectMapping - async def getProjectList(self): + async def getProjectList(self) -> list[str]: await self._setupProjectList(True) return sorted(self.projectMapping) - async def _setupProjectList(self, forceRebuild=False): + async def _setupProjectList(self, forceRebuild: bool = False) -> None: if not forceRebuild and self.projectMapping is not None: return projectMapping = await self.rcjkClient.get_project_font_uid_mapping() projectMapping = {f"{p}/{f}": uids for (p, f), uids in projectMapping.items()} self.projectMapping = projectMapping - async def getFontHandler(self, path): + async def getFontHandler(self, path: str) -> FontHandler: fontHandler = self.fontHandlers.get(path) if fontHandler is None: _, fontUID = self.projectMapping[path] From 13cd2075ae295cc88bdfc7fb9d3859bd77b626cd Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Wed, 13 Dec 2023 15:26:37 +0100 Subject: [PATCH 12/16] Fix delete glyph when cache dir is not set --- src/fontra_rcjk/backend_mysql.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index 237d500..e8cf34a 100644 --- a/src/fontra_rcjk/backend_mysql.py +++ b/src/fontra_rcjk/backend_mysql.py @@ -194,6 +194,8 @@ def _writeGlyphToCacheDir(self, glyphName, glyph): logger.exception(f"error writing {glyphName!r} to local cache: {e!r}") def _deleteGlyphFromCacheDir(self, glyphName): + if self.cacheDir is None: + return glyphInfo = self._rcjkGlyphInfo.get(glyphName) if glyphInfo is None: return From 7be5ca7229f902ed886c6577f94347f793906dfa Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Wed, 13 Dec 2023 15:57:31 +0100 Subject: [PATCH 13/16] Add more typing --- src/fontra_rcjk/backend_mysql.py | 47 +++++++++++++++++++------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index e8cf34a..7b2bcdd 100644 --- a/src/fontra_rcjk/backend_mysql.py +++ b/src/fontra_rcjk/backend_mysql.py @@ -6,9 +6,16 @@ from dataclasses import dataclass from datetime import datetime, timedelta from random import random +from typing import Any from fontra.backends.designspace import makeGlyphMapChange -from fontra.core.classes import VariableGlyph, structure, unstructure +from fontra.core.classes import ( + GlobalAxis, + GlobalDiscreteAxis, + VariableGlyph, + structure, + unstructure, +) from fontra.core.instancer import mapLocationFromUserToSource from fontra.core.protocols import WritableFontBackend @@ -72,20 +79,20 @@ def __init__(self, client, fontUID, cacheDir=None): def close(self): self._tempFontItemsCache.cancel() - async def getGlyphMap(self): + async def getGlyphMap(self) -> dict[str, list[int]]: await self._ensureGlyphMap() return dict(self._glyphMap) - async def putGlyphMap(self, glyphMap): + async def putGlyphMap(self, value: dict[str, list[int]]) -> None: pass - def _ensureGlyphMap(self): + def _ensureGlyphMap(self) -> asyncio.Task: # Prevent multiple concurrent queries by using a single task if self._glyphMapTask is None: self._glyphMapTask = asyncio.create_task(self._ensureGlyphMapTask()) return self._glyphMapTask - async def _ensureGlyphMapTask(self): + async def _ensureGlyphMapTask(self) -> None: if self._glyphMap is not None: return rcjkGlyphInfo = {} @@ -102,7 +109,7 @@ async def _ensureGlyphMapTask(self): self._glyphMap = glyphMap self._rcjkGlyphInfo = rcjkGlyphInfo - async def _getMiscFontItems(self): + async def _getMiscFontItems(self) -> None: if not hasattr(self, "_getMiscFontItemsTask"): async def taskFunc(): @@ -119,7 +126,7 @@ async def taskFunc(): self._getMiscFontItemsTask = asyncio.create_task(taskFunc()) await self._getMiscFontItemsTask - async def getGlobalAxes(self): + async def getGlobalAxes(self) -> list[GlobalAxis | GlobalDiscreteAxis]: axes = self._tempFontItemsCache.get("axes") if axes is None: await self._getMiscFontItems() @@ -130,37 +137,37 @@ async def getGlobalAxes(self): self._defaultLocation = mapLocationFromUserToSource(userLoc, axes) return axes - async def putGlobalAxes(self, axes): + async def putGlobalAxes(self, axes: list[GlobalAxis | GlobalDiscreteAxis]) -> None: await self._getMiscFontItems() designspace = self._tempFontItemsCache["designspace"] designspace["axes"] = unstructure(axes) _ = await self.client.font_update(self.fontUID, designspace=designspace) - async def getDefaultLocation(self): + async def getDefaultLocation(self) -> dict[str, float]: if self._defaultLocation is None: _ = await self.getGlobalAxes() assert self._defaultLocation is not None return self._defaultLocation - async def getUnitsPerEm(self): + async def getUnitsPerEm(self) -> int: return 1000 - async def putUnitsPerEm(self, value): + async def putUnitsPerEm(self, value: int) -> None: pass - async def getCustomData(self): + async def getCustomData(self) -> dict[str, Any]: customData = self._tempFontItemsCache.get("customData") if customData is None: await self._getMiscFontItems() customData = self._tempFontItemsCache["customData"] return customData - async def putCustomData(self, customData): + async def putCustomData(self, customData: dict[str, Any]) -> None: await self._getMiscFontItems() self._tempFontItemsCache["customData"] = deepcopy(customData) _ = await self.client.font_update(self.fontUID, fontlib=customData) - def _readGlyphFromCacheDir(self, glyphName): + def _readGlyphFromCacheDir(self, glyphName: str) -> VariableGlyph | None: if self.cacheDir is None: return None glyphInfo = self._rcjkGlyphInfo[glyphName] @@ -176,7 +183,7 @@ def _readGlyphFromCacheDir(self, glyphName): logger.exception(f"error reading {glyphName!r} from local cache: {e!r}") return None - def _writeGlyphToCacheDir(self, glyphName, glyph): + def _writeGlyphToCacheDir(self, glyphName: str, glyph: VariableGlyph) -> None: if self.cacheDir is None: return glyphInfo = self._rcjkGlyphInfo[glyphName] @@ -193,7 +200,7 @@ def _writeGlyphToCacheDir(self, glyphName, glyph): except Exception as e: logger.exception(f"error writing {glyphName!r} to local cache: {e!r}") - def _deleteGlyphFromCacheDir(self, glyphName): + def _deleteGlyphFromCacheDir(self, glyphName: str) -> None: if self.cacheDir is None: return glyphInfo = self._rcjkGlyphInfo.get(glyphName) @@ -203,7 +210,7 @@ def _deleteGlyphFromCacheDir(self, glyphName): for stalePath in self.cacheDir.glob(globPattern): stalePath.unlink() - async def getGlyph(self, glyphName): + async def getGlyph(self, glyphName: str) -> VariableGlyph | None: await self._ensureGlyphMap() if glyphName not in self._glyphMap: return None @@ -249,12 +256,14 @@ def _populateGlyphCache(self, glyphName, glyphData): assert glyphInfo.glyphID == subGlyphData["id"] self._populateGlyphCache(subGlyphName, subGlyphData) - async def putGlyph(self, glyphName, glyph, unicodes): + async def putGlyph( + self, glyphName: str, glyph: VariableGlyph, codePoints: list[int] + ) -> None: await self._ensureGlyphMap() logger.info(f"Start writing {glyphName}") self._writingChanges += 1 try: - return await self._putGlyph(glyphName, glyph, unicodes) + return await self._putGlyph(glyphName, glyph, codePoints) finally: self._writingChanges -= 1 logger.info(f"Done writing {glyphName}") From a0520b7d27e9bb2e85a00390af593faefee3da86 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Wed, 13 Dec 2023 16:02:44 +0100 Subject: [PATCH 14/16] More typing --- src/fontra_rcjk/backend_mysql.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index 7b2bcdd..489c866 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 +from typing import Any, AsyncGenerator from fontra.backends.designspace import makeGlyphMapChange from fontra.core.classes import ( @@ -342,7 +342,7 @@ async def _putGlyph(self, glyphName, glyph, unicodes): return errorMessage - async def _newGlyph(self, glyphName, unicodes): + async def _newGlyph(self, glyphName: str, unicodes: list[int]) -> None: # In _newGlyph() we create a new character glyph in the database. # _putGlyph will immediately overwrite it with the real glyph data, # with a lock acquired. Our dummy glyph has to have a glyph name, but @@ -363,7 +363,7 @@ async def _newGlyph(self, glyphName, unicodes): self._rcjkGlyphInfo[glyphName] = RCJKGlyphInfo("CG", glyphID, timeStamp) self._glyphTimeStamps[glyphName] = timeStamp - async def deleteGlyph(self, glyphName): + async def deleteGlyph(self, glyphName: str) -> None: await self._ensureGlyphMap() if glyphName not in self._rcjkGlyphInfo: raise KeyError(f"Glyph '{glyphName}' does not exist") @@ -391,7 +391,7 @@ 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): + async def watchExternalChanges(self) -> AsyncGenerator[tuple[Any, Any], None]: await self._ensureGlyphMap() errorDelay = 30 while True: @@ -406,7 +406,7 @@ async def watchExternalChanges(self): if externalChange or reloadPattern: yield externalChange, reloadPattern - async def _pollOnceForChanges(self): + async def _pollOnceForChanges(self) -> tuple[Any, Any]: try: await asyncio.wait_for( self._pollNowEvent.wait(), @@ -428,7 +428,7 @@ async def _pollOnceForChanges(self): ) responseData = response["data"] glyphNames = set() - glyphMapUpdates = {} + glyphMapUpdates: dict[str, list[int] | None] = {} latestTimeStamp = "" # less than any timestamp string for glyphInfo in responseData.get("deleted_glifs", []): @@ -490,11 +490,11 @@ async def _pollOnceForChanges(self): return externalChange, reloadPattern -def _unicodesFromGlyphInfo(glyphInfo): +def _unicodesFromGlyphInfo(glyphInfo: dict) -> list[int]: return glyphInfo.get("unicodes", []) -def getUpdatedTimeStamp(info): +def getUpdatedTimeStamp(info: dict) -> str: timeStamp = info["updated_at"] layers_updated_at = info.get("layers_updated_at") if layers_updated_at: @@ -502,7 +502,7 @@ def getUpdatedTimeStamp(info): return timeStamp -def buildLayerGlyphsFromResponseData(glyphData): +def buildLayerGlyphsFromResponseData(glyphData: dict) -> dict[str, GLIFGlyph]: layerGLIFData = [("foreground", glyphData["data"])] layerGLIFData.extend( (layer["group_name"], layer["data"]) for layer in glyphData.get("layers", ()) @@ -551,7 +551,7 @@ def __setitem__(self, key, value): del self[next(iter(self))] -def fudgeTimeStamp(isoString): +def fudgeTimeStamp(isoString: str) -> str: """Add one millisecond to the timestamp, so we can account for differences in the microsecond range. """ From 0e7bfdf05f67b815e11dca281878d090edf95544 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Wed, 13 Dec 2023 19:34:55 +0100 Subject: [PATCH 15/16] no need for setup.py --- setup.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 8d0b2d2..0000000 --- a/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -# This is just a stub; see pyproject.toml for the metadata. - -from setuptools import setup - -setup() From 5116bd86067736faf3d74ff22b18a4222cd72d6f Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Wed, 13 Dec 2023 21:27:42 +0100 Subject: [PATCH 16/16] Use main branch again --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fc2752a..217c26b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ mypy==1.7.1 pre-commit==3.5.0 pytest==7.4.3 pytest-asyncio==0.23.2 -git+https://github.com/googlefonts/fontra.git@protocols +git+https://github.com/googlefonts/fontra.git