Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for de/serializing objects with json and msgpack #230

Merged
merged 22 commits into from Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5a53459
WIP: add support for de/serializing objects with json, msgpack or pickle
anthrotype Jul 26, 2022
bc82a2a
LayerSet/Layer: record self._lazy and make unlazify idempotent
anthrotype Jul 27, 2022
c1322fe
remove unused Attribute.metadata[copyable]
anthrotype Jul 27, 2022
e0f3d6a
make lazy Font/LayerSet/Layer/DataStore pickleable
anthrotype Jul 27, 2022
829e35a
recompile requirements.txt with new [json,msgpack] extras
anthrotype Jul 27, 2022
d3a2b6a
add tests/ufoLib2/serde
anthrotype Jul 27, 2022
3442969
skip test if extras not installed; force mypy to not typecheck orjson
anthrotype Jul 27, 2022
0dfc108
info: forgot to add @serde decorator on Info class!
anthrotype Jul 27, 2022
fb32994
test de/serializing ALL ufoLib2.objects
anthrotype Jul 27, 2022
30f639f
skip format-specific tests when cattrs can't be imported
anthrotype Jul 27, 2022
0ac647b
remove ufoLib2.serde.pickle, doesn't add anything
anthrotype Jul 28, 2022
6378e56
also test {json,msgpack}_load/dump with paths, in addition to files
anthrotype Jul 28, 2022
5a8d9bc
change custom {get,set}state to use dict, like upstream attrs does
anthrotype Nov 3, 2022
a8223ed
require latest attrs/cattrs; rename GenConverter => Converter
anthrotype Nov 3, 2022
b421da3
import attrs/cattrs from the new namespaces with the 's' at the end
anthrotype Nov 3, 2022
7c7f7f7
converters: pass on new detailed_validation option, fix test
anthrotype Nov 3, 2022
1206873
recompile requirements-dev.txt as well
anthrotype Nov 3, 2022
ebc3131
make mypy happy again
anthrotype Nov 4, 2022
b88ff09
do use orjson typing stubs after all
anthrotype Nov 4, 2022
c6e188c
add comment to test_pickle_lazy_font [skip ci]
anthrotype Nov 4, 2022
36d4632
add standalone load/dump functions in each ufoLib2.serde submodule
anthrotype Nov 4, 2022
7a9d1de
fix typos in comments [skip ci]
anthrotype Nov 4, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -96,6 +96,8 @@ venv/
ENV/
env.bak/
venv.bak/
venv-*
.venv-*

# Spyder project settings
.spyderproject
Expand Down
4 changes: 2 additions & 2 deletions requirements-dev.txt
@@ -1,10 +1,10 @@
#
# This file is autogenerated by pip-compile with python 3.10
# This file is autogenerated by pip-compile with python 3.11
# To update, run:
#
# pip-compile --output-file=requirements-dev.txt requirements-dev.in
#
attrs==21.4.0
attrs==22.1.0
# via
# -c requirements.txt
# pytest
Expand Down
12 changes: 8 additions & 4 deletions requirements.txt
@@ -1,23 +1,27 @@
#
# This file is autogenerated by pip-compile with python 3.10
# This file is autogenerated by pip-compile with python 3.11
# To update, run:
#
# pip-compile --extra=converters --extra=lxml setup.cfg
# pip-compile --extra=converters --extra=json --extra=lxml --extra=msgpack setup.cfg
#
appdirs==1.4.4
# via fs
attrs==21.4.0
attrs==22.1.0
# via
# cattrs
# ufoLib2 (setup.cfg)
cattrs==1.10.0
cattrs==22.2.0
# via ufoLib2 (setup.cfg)
fonttools[ufo]==4.29.1
# via ufoLib2 (setup.cfg)
fs==2.4.15
# via fonttools
lxml==4.9.1
# via ufoLib2 (setup.cfg)
msgpack==1.0.4
# via ufoLib2 (setup.cfg)
orjson==3.7.8 ; platform_python_implementation != "PyPy"
# via ufoLib2 (setup.cfg)
pytz==2021.3
# via fs
six==1.16.0
Expand Down
8 changes: 7 additions & 1 deletion setup.cfg
Expand Up @@ -22,7 +22,7 @@ package_dir = =src
packages = find:
python_requires = >=3.7
install_requires =
attrs >= 20.1.0
attrs >= 22.1.0
fonttools[ufo] >= 4.0.0
typing_extensions ; python_version < "3.8"

Expand All @@ -33,6 +33,12 @@ install_requires =
[options.extras_require]
lxml = lxml
converters = cattrs >= 1.10.0
json =
cattrs >= 22.2.0
orjson ; platform_python_implementation != 'PyPy'
msgpack =
cattrs >= 22.2.0
msgpack

[options.packages.find]
where = src
Expand Down
30 changes: 20 additions & 10 deletions src/ufoLib2/converters.py
Expand Up @@ -4,9 +4,9 @@
from functools import partial
from typing import Any, Callable, Tuple, Type, cast

from attr import fields, has, resolve_types
from cattr import GenConverter
from cattr.gen import (
from attrs import fields, has, resolve_types
from cattrs import Converter
from cattrs.gen import (
AttributeOverride,
make_dict_structure_fn,
make_dict_unstructure_fn,
Expand Down Expand Up @@ -49,17 +49,19 @@ def is_ufoLib2_class_with_custom_structure(cls: Type[Any]) -> bool:
return is_ufoLib2_class(cls) and hasattr(cls, "_structure")


def register_hooks(conv: GenConverter, allow_bytes: bool = True) -> None:
def register_hooks(conv: Converter, allow_bytes: bool = True) -> None:
def attrs_hook_factory(
cls: Type[Any], gen_fn: Callable[..., Callable[[Any], Any]], structuring: bool
) -> Callable[[Any], Any]:
cls: Type[Any], gen_fn: Callable[..., Callable[..., Any]], structuring: bool
) -> Callable[..., Any]:
base = get_origin(cls)
if base is None:
base = cls
attribs = fields(base)
# PEP563 postponed annotations need resolving as we check Attribute.type below
resolve_types(base)
kwargs: dict[str, bool | AttributeOverride] = {}
kwargs: dict[str, bool | AttributeOverride] = {
"_cattrs_detailed_validation": conv.detailed_validation
madig marked this conversation as resolved.
Show resolved Hide resolved
}
if structuring:
kwargs["_cattrs_forbid_extra_keys"] = conv.forbid_extra_keys
kwargs["_cattrs_prefer_attrib_converters"] = conv._prefer_attrib_converters
Expand All @@ -71,7 +73,7 @@ def attrs_hook_factory(
# classes that don't have a custom hook registered) check for any
# type_overrides (Dict[Type, AttributeOverride]); they allow a custom
# converter to omit specific attributes of given type e.g.:
# >>> conv = GenConverter(type_overrides={Image: override(omit=True)})
# >>> conv = Converter(type_overrides={Image: override(omit=True)})
attrib_override = conv.type_overrides[a.type]
else:
# by default, we omit all Optional attributes (i.e. with None default),
Expand All @@ -94,7 +96,7 @@ def attrs_hook_factory(
def custom_unstructure_hook_factory(cls: Type[Any]) -> Callable[[Any], Any]:
return partial(cls._unstructure, converter=conv)

def custom_structure_hook_factory(cls: Type[Any]) -> Callable[[Any], Any]:
def custom_structure_hook_factory(cls: Type[Any]) -> Callable[[Any, Any], Any]:
return partial(cls._structure, converter=conv)

def unstructure_transform(t: Transform) -> Tuple[float]:
Expand Down Expand Up @@ -134,7 +136,7 @@ def structure_bytes(v: str, _: Any) -> bytes:
conv.register_structure_hook(bytes, structure_bytes)


default_converter = GenConverter(
default_converter = Converter(
omit_if_default=True,
forbid_extra_keys=True,
prefer_attrib_converters=False,
Expand All @@ -143,3 +145,11 @@ def structure_bytes(v: str, _: Any) -> bytes:

structure = default_converter.structure
unstructure = default_converter.unstructure

# same as default_converter but allows bytes
binary_converter = Converter(
omit_if_default=True,
forbid_extra_keys=True,
prefer_attrib_converters=False,
)
register_hooks(binary_converter, allow_bytes=True)
4 changes: 3 additions & 1 deletion src/ufoLib2/objects/anchor.py
Expand Up @@ -2,11 +2,13 @@

from typing import Optional

from attr import define
from attrs import define

from ufoLib2.objects.misc import AttrDictMixin
from ufoLib2.serde import serde


@serde
@define
class Anchor(AttrDictMixin):
"""Represents a single anchor.
Expand Down
4 changes: 3 additions & 1 deletion src/ufoLib2/objects/component.py
Expand Up @@ -3,17 +3,19 @@
import warnings
from typing import Optional

from attr import define, field
from attrs import define, field
from fontTools.misc.transform import Identity, Transform
from fontTools.pens.basePen import AbstractPen
from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen

from ufoLib2.objects.misc import BoundingBox
from ufoLib2.serde import serde
from ufoLib2.typing import GlyphSet

from .misc import _convert_transform, getBounds, getControlBounds


@serde
@define
class Component:
"""Represents a reference to another glyph in the same layer.
Expand Down
4 changes: 3 additions & 1 deletion src/ufoLib2/objects/contour.py
Expand Up @@ -4,12 +4,13 @@
from collections.abc import MutableSequence
from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, overload

from attr import define, field
from attrs import define, field
from fontTools.pens.basePen import AbstractPen
from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen

from ufoLib2.objects.misc import BoundingBox, getBounds, getControlBounds
from ufoLib2.objects.point import Point
from ufoLib2.serde import serde
from ufoLib2.typing import GlyphSet

# For Python 3.7 compatibility.
Expand All @@ -19,6 +20,7 @@
ContourMapping = MutableSequence


@serde
@define
class Contour(ContourMapping):
"""Represents a contour as a list of points.
Expand Down
2 changes: 2 additions & 0 deletions src/ufoLib2/objects/dataSet.py
Expand Up @@ -3,8 +3,10 @@
from fontTools.ufoLib import UFOReader, UFOWriter

from ufoLib2.objects.misc import DataStore
from ufoLib2.serde import serde


@serde
class DataSet(DataStore):
"""Represents a mapping of POSIX filename strings to arbitrary data bytes.

Expand Down
11 changes: 7 additions & 4 deletions src/ufoLib2/objects/features.py
Expand Up @@ -3,15 +3,18 @@
import re
from typing import TYPE_CHECKING, Type

from attr import define
from attrs import define

from ufoLib2.serde import serde

if TYPE_CHECKING:
from cattr import GenConverter
from cattrs import Converter


RE_NEWLINES = re.compile(r"\r\n|\r")


@serde
@define
class Features:
"""A data class representing UFO features.
Expand All @@ -33,11 +36,11 @@ def normalize_newlines(self) -> Features:
self.text = RE_NEWLINES.sub("\n", self.text)
return self

def _unstructure(self, converter: GenConverter) -> str:
def _unstructure(self, converter: Converter) -> str:
del converter # unused
return self.text

@staticmethod
def _structure(data: str, cls: Type[Features], converter: GenConverter) -> Features:
def _structure(data: str, cls: Type[Features], converter: Converter) -> Features:
del converter # unused
return cls(data)
13 changes: 9 additions & 4 deletions src/ufoLib2/objects/font.py
Expand Up @@ -18,7 +18,7 @@

import fs.base
import fs.tempfs
from attr import define, field
from attrs import define, field
from fontTools.ufoLib import UFOFileStructure, UFOReader, UFOWriter

from ufoLib2.constants import DEFAULT_LAYER_NAME
Expand All @@ -35,9 +35,12 @@
from ufoLib2.objects.misc import (
BoundingBox,
_deepcopy_unlazify_attrs,
_getstate_unlazify_attrs,
_object_lib,
_prune_object_libs,
_setstate_attrs,
)
from ufoLib2.serde import serde
from ufoLib2.typing import HasIdentifier, PathLike, T


Expand Down Expand Up @@ -73,6 +76,7 @@ def _set_kerning(self: Font, value: Mapping[KerningPair, float]) -> None:
self._kerning = _convert_Kerning(value)


@serde
@define(kw_only=True)
class Font:
"""A data class representing a single Unified Font Object (UFO).
Expand Down Expand Up @@ -163,9 +167,7 @@ class Font:
"""ImageSet: A mapping of image file paths to arbitrary image data."""

# init=False args, set by alternate open/read classmethod constructors
_path: Optional[PathLike] = field(
default=None, metadata=dict(copyable=False), eq=False, init=False
)
_path: Optional[PathLike] = field(default=None, eq=False, init=False)
_lazy: Optional[bool] = field(default=None, init=False, eq=False)
_reader: Optional[UFOReader] = field(default=None, init=False, eq=False)
_fileStructure: Optional[UFOFileStructure] = field(
Expand Down Expand Up @@ -318,6 +320,9 @@ def unlazify(self) -> None:

__deepcopy__ = _deepcopy_unlazify_attrs

__getstate__ = _getstate_unlazify_attrs
__setstate__ = _setstate_attrs

@property
def glyphOrder(self) -> list[str]:
"""The font's glyph order.
Expand Down
4 changes: 3 additions & 1 deletion src/ufoLib2/objects/glyph.py
Expand Up @@ -3,7 +3,7 @@
from copy import deepcopy
from typing import Any, Iterator, List, Mapping, Optional, cast

from attr import define, field
from attrs import define, field
from fontTools.misc.transform import Transform
from fontTools.pens.basePen import AbstractPen
from fontTools.pens.pointPen import (
Expand All @@ -20,9 +20,11 @@
from ufoLib2.objects.lib import Lib, _convert_Lib, _get_lib, _set_lib
from ufoLib2.objects.misc import BoundingBox, _object_lib, getBounds, getControlBounds
from ufoLib2.pointPens.glyphPointPen import GlyphPointPen
from ufoLib2.serde import serde
from ufoLib2.typing import GlyphSet, HasIdentifier


@serde
@define
class Glyph:
"""Represents a glyph, containing contours, components, anchors and various
Expand Down
4 changes: 3 additions & 1 deletion src/ufoLib2/objects/guideline.py
Expand Up @@ -2,11 +2,13 @@

from typing import Optional

from attr import define
from attrs import define

from ufoLib2.objects.misc import AttrDictMixin
from ufoLib2.serde import serde


@serde
@define
class Guideline(AttrDictMixin):
"""Represents a single guideline.
Expand Down
5 changes: 4 additions & 1 deletion src/ufoLib2/objects/image.py
Expand Up @@ -3,9 +3,11 @@
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, ClassVar, Iterator, Optional, Tuple

from attr import define, field
from attrs import define, field
from fontTools.misc.transform import Identity, Transform

from ufoLib2.serde import serde

from .misc import _convert_transform

# For Python 3.7 compatibility.
Expand All @@ -15,6 +17,7 @@
ImageMapping = Mapping


@serde
@define
class Image(ImageMapping):
"""Represents a background image reference.
Expand Down
2 changes: 2 additions & 0 deletions src/ufoLib2/objects/imageSet.py
Expand Up @@ -3,8 +3,10 @@
from fontTools.ufoLib import UFOReader, UFOWriter

from ufoLib2.objects.misc import DataStore
from ufoLib2.serde import serde


@serde
class ImageSet(DataStore):
"""Represents a mapping of POSIX filename strings to arbitrary image data.

Expand Down