diff --git a/src/fontra/backends/fontra.py b/src/fontra/backends/fontra.py index cedd21291..76e361ef3 100644 --- a/src/fontra/backends/fontra.py +++ b/src/fontra/backends/fontra.py @@ -8,9 +8,7 @@ from dataclasses import dataclass, field from typing import Callable -import cattrs - -from fontra.core.classes import Font, VariableGlyph +from fontra.core.classes import Font, VariableGlyph, structure, unstructure from .filenames import stringToFileName @@ -122,12 +120,12 @@ def _writeGlyphInfo(self): writer.writerow([glyphName, codePoints]) def _readFontData(self): - self.fontData = cattrs.structure( + self.fontData = structure( json.loads(self.fontDataPath.read_text(encoding="utf-8")), Font ) def _writeFontData(self): - fontData = cattrs.unstructure(self.fontData) + fontData = unstructure(self.fontData) fontData.pop("glyphs", None) fontData.pop("glyphMap", None) self.fontDataPath.write_text(serialize(fontData) + "\n", encoding="utf-8") @@ -144,7 +142,7 @@ def getGlyphFilePath(self, glyphName): def serializeGlyph(glyph, glyphName=None): glyph = glyph.convertToPaths() - jsonGlyph = cattrs.unstructure(glyph) + jsonGlyph = unstructure(glyph) if glyphName is not None: jsonGlyph["name"] = glyphName return serialize(jsonGlyph) + "\n" @@ -154,7 +152,7 @@ def deserializeGlyph(jsonSource, glyphName=None): jsonGlyph = json.loads(jsonSource) if glyphName is not None: jsonGlyph["name"] = glyphName - glyph = cattrs.structure(jsonGlyph, VariableGlyph) + glyph = structure(jsonGlyph, VariableGlyph) return glyph.convertToPackedPaths() diff --git a/src/fontra/core/classes.py b/src/fontra/core/classes.py index 2515b82a5..95edeec92 100644 --- a/src/fontra/core/classes.py +++ b/src/fontra/core/classes.py @@ -185,21 +185,21 @@ def makeSchema(*classes, schema=None): return schema -# cattrs hooks +# cattrs hooks + structure/unstructure support def _structurePath(d, tp): if "pointTypes" not in d: - return cattrs.structure(d, Path) + return structure(d, Path) else: - return cattrs.structure(d, PackedPath) + return structure(d, PackedPath) def _structureGlobalAxis(d, tp): if "values" not in d: - return cattrs.structure(d, GlobalAxis) + return structure(d, GlobalAxis) else: - return cattrs.structure(d, GlobalDiscreteAxis) + return structure(d, GlobalDiscreteAxis) def _structureNumber(d, tp): @@ -219,24 +219,26 @@ def _structurePointType(v, tp): return PointType(v) -cattrs.register_structure_hook(Union[PackedPath, Path], _structurePath) -cattrs.register_structure_hook( +_cattrsConverter = cattrs.Converter() + +_cattrsConverter.register_structure_hook(Union[PackedPath, Path], _structurePath) +_cattrsConverter.register_structure_hook( Union[GlobalAxis, GlobalDiscreteAxis], _structureGlobalAxis ) -cattrs.register_structure_hook(float, _structureNumber) -cattrs.register_structure_hook(Point, _structurePoint) -cattrs.register_unstructure_hook(Point, _unstructurePoint) -cattrs.register_structure_hook(bool, lambda x, y: x) -cattrs.register_structure_hook(PointType, _structurePointType) +_cattrsConverter.register_structure_hook(float, _structureNumber) +_cattrsConverter.register_structure_hook(Point, _structurePoint) +_cattrsConverter.register_unstructure_hook(Point, _unstructurePoint) +_cattrsConverter.register_structure_hook(bool, lambda x, y: x) +_cattrsConverter.register_structure_hook(PointType, _structurePointType) def registerOmitDefaultHook(cls): _hook = cattrs.gen.make_dict_unstructure_fn( cls, - cattrs.global_converter, + _cattrsConverter, _cattrs_omit_if_default=True, ) - cattrs.register_unstructure_hook(cls, _hook) + _cattrsConverter.register_unstructure_hook(cls, _hook) # The order in which the hooks are applied is significant, for unclear reasons @@ -250,13 +252,21 @@ def registerOmitDefaultHook(cls): registerOmitDefaultHook(PackedPath) +def structure(obj, cls): + return _cattrsConverter.structure(obj, cls) + + +def unstructure(obj): + return _cattrsConverter.unstructure(obj) + + atomicTypes = [str, int, float, bool, Any] def makeCastFuncs(schema): castFuncs = {} for cls, fields in schema.items(): - castFuncs[cls] = partial(cattrs.structure, cl=cls) + castFuncs[cls] = partial(structure, cls=cls) for fieldName, fieldInfo in fields.items(): fieldType = fieldInfo["type"] if fieldType in atomicTypes or fieldType in schema: @@ -264,7 +274,7 @@ def makeCastFuncs(schema): itemType = get_args(fieldType)[-1] if itemType in atomicTypes: continue - castFuncs[fieldType] = partial(cattrs.structure, cl=fieldType) + castFuncs[fieldType] = partial(structure, cls=fieldType) return castFuncs diff --git a/src/fontra/core/path.py b/src/fontra/core/path.py index a16d0e6b1..92bf6d66e 100644 --- a/src/fontra/core/path.py +++ b/src/fontra/core/path.py @@ -4,7 +4,6 @@ from enum import IntEnum from typing import TypedDict -import cattrs from fontTools.misc.transform import DecomposedTransform logger = logging.getLogger(__name__) @@ -38,7 +37,9 @@ def asPath(self): return self def asPackedPath(self): - return PackedPath.fromUnpackedContours(cattrs.unstructure(self.contours)) + from .classes import unstructure + + return PackedPath.fromUnpackedContours(unstructure(self.contours)) def isEmpty(self): return not self.contours @@ -91,7 +92,9 @@ def fromUnpackedContours(cls, unpackedContours): ) def asPath(self): - return Path(contours=cattrs.structure(self.unpackedContours(), list[Contour])) + from .classes import structure + + return Path(contours=structure(self.unpackedContours(), list[Contour])) def asPackedPath(self): return self diff --git a/src/fontra/core/remote.py b/src/fontra/core/remote.py index 84d819ace..65982eb7e 100644 --- a/src/fontra/core/remote.py +++ b/src/fontra/core/remote.py @@ -2,9 +2,10 @@ import logging import traceback -import cattrs from aiohttp import WSMsgType +from .classes import unstructure + logger = logging.getLogger(__name__) @@ -91,7 +92,7 @@ async def _performCall(self, message, subject): methodHandler = getattr(subject, methodName, None) if getattr(methodHandler, "fontraRemoteMethod", False): returnValue = await methodHandler(*arguments, connection=self) - returnValue = cattrs.unstructure(returnValue) + returnValue = unstructure(returnValue) response = {"client-call-id": clientCallID, "return-value": returnValue} else: response = { diff --git a/src/fontra/core/serverutils.py b/src/fontra/core/serverutils.py index fef0d3139..4fadd842c 100644 --- a/src/fontra/core/serverutils.py +++ b/src/fontra/core/serverutils.py @@ -1,6 +1,5 @@ -import cattrs - from . import clipboard, glyphnames +from .classes import unstructure apiFunctions = {} @@ -22,4 +21,4 @@ def getUnicodeFromGlyphName(glyphName): @api def parseClipboard(data): - return cattrs.unstructure(clipboard.parseClipboard(data)) + return unstructure(clipboard.parseClipboard(data)) diff --git a/test-py/test_classes.py b/test-py/test_classes.py index 37d45d7b6..471ece94f 100644 --- a/test-py/test_classes.py +++ b/test-py/test_classes.py @@ -1,8 +1,6 @@ import json import pathlib -import cattrs - from fontra.backends.fontra import deserializeGlyph from fontra.core.classes import ( Layer, @@ -10,6 +8,7 @@ VariableGlyph, classCastFuncs, serializableClassSchema, + unstructure, ) repoRoot = pathlib.Path(__file__).resolve().parent.parent @@ -35,17 +34,17 @@ def test_cast(): / "B^1.json" ) originalGlyph = deserializeGlyph(glyphPath.read_text(encoding="utf-8")) - unstructuredGlyph = cattrs.unstructure(originalGlyph) + unstructuredGlyph = unstructure(originalGlyph) # Ensure that the PointType enums get converted to ints unstructuredGlyph = json.loads(json.dumps(unstructuredGlyph)) glyph = classCastFuncs[VariableGlyph](unstructuredGlyph) assert glyph == originalGlyph assert str(glyph) == str(originalGlyph) - sourcesList = cattrs.unstructure(glyph.sources) + sourcesList = unstructure(glyph.sources) sources = classCastFuncs[list[Source]](sourcesList) assert glyph.sources == sources - layersDict = cattrs.unstructure(glyph.layers) + layersDict = unstructure(glyph.layers) layers = classCastFuncs[dict[str, Layer]](layersDict) assert glyph.layers == layers diff --git a/test-py/test_font.py b/test-py/test_font.py index 6739942de..52fe04537 100644 --- a/test-py/test_font.py +++ b/test-py/test_font.py @@ -2,11 +2,10 @@ import pathlib from dataclasses import asdict -import cattrs import pytest from fontra.backends import getFileSystemBackend -from fontra.core.classes import GlobalAxis, GlobalDiscreteAxis, VariableGlyph +from fontra.core.classes import GlobalAxis, GlobalDiscreteAxis, VariableGlyph, structure dataDir = pathlib.Path(__file__).resolve().parent / "data" @@ -963,7 +962,7 @@ async def test_getGlyphMap(testFontName, numGlyphs, testMapping): @pytest.mark.asyncio @pytest.mark.parametrize("testFontName, expectedGlyph", getGlyphTestData) async def test_getGlyph(testFontName, expectedGlyph): - expectedGlyph = cattrs.structure(expectedGlyph, VariableGlyph) + expectedGlyph = structure(expectedGlyph, VariableGlyph) font = getTestFont(testFontName) with contextlib.closing(font): glyph = await font.getGlyph(expectedGlyph.name) diff --git a/test-py/test_path.py b/test-py/test_path.py index c92cf28f3..3f0c9efa7 100644 --- a/test-py/test_path.py +++ b/test-py/test_path.py @@ -1,8 +1,8 @@ import operator -import cattrs import pytest +from fontra.core.classes import structure, unstructure from fontra.core.path import ( Contour, InterpolationError, @@ -127,26 +127,26 @@ @pytest.mark.parametrize("path", pathTestData) def test_packedPathPointPenRoundTrip(path): - path = cattrs.structure(path, PackedPath) + path = structure(path, PackedPath) pen = PackedPathPointPen() path.drawPoints(pen) repackedPath = pen.getPath() assert path == repackedPath - assert cattrs.unstructure(path) == cattrs.unstructure(repackedPath) + assert unstructure(path) == unstructure(repackedPath) @pytest.mark.parametrize("path", pathTestData) def test_unpackPathRoundTrip(path): - path = cattrs.structure(path, PackedPath) + path = structure(path, PackedPath) unpackedPath = path.unpackedContours() repackedPath = PackedPath.fromUnpackedContours(unpackedPath) assert path == repackedPath - assert cattrs.unstructure(path) == cattrs.unstructure(repackedPath) + assert unstructure(path) == unstructure(repackedPath) @pytest.mark.parametrize("path", pathTestData) def test_pathConversion(path): - packedPath = cattrs.structure(path, PackedPath) + packedPath = structure(path, PackedPath) path = packedPath.asPath() packedPath2 = path.asPackedPath() assert packedPath == packedPath2 @@ -167,7 +167,7 @@ def test_pathConversion(path): def test_packedPathRepr(): path = pathTestData[1] - packedPath = cattrs.structure(path, PackedPath) + packedPath = structure(path, PackedPath) assert expectedPackedPathRepr == str(packedPath) @@ -183,7 +183,7 @@ def test_packedPathRepr(): def test_pathRepr(): path = pathTestData[1] - packedPath = cattrs.structure(path, PackedPath) + packedPath = structure(path, PackedPath) path = packedPath.asPath() assert expectedPathRepr == str(path)