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

support defcon-like Font(path) default constructor #47

Merged
merged 11 commits into from
Dec 16, 2019
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
fonttools==3.39.0
attrs==19.1.0
attrs==19.2.0
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ setup_requires =
setuptools_scm
wheel
install_requires =
attrs >= 18.2.0
attrs >= 19.2.0
fonttools[ufo] >= 3.34.0

[options.extras_require]
Expand Down
145 changes: 109 additions & 36 deletions src/ufoLib2/objects/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ufoLib2.objects.imageSet import ImageSet
from ufoLib2.objects.info import Info
from ufoLib2.objects.layerSet import LayerSet
from ufoLib2.objects.misc import _deepcopy_unlazify_attrs


def _convert_Info(value: Union[Info, Mapping[str, Any]]) -> Info:
Expand All @@ -31,65 +32,95 @@ def _convert_Features(value: Union[Features, str]) -> Features:
return value if isinstance(value, Features) else Features(value)


@attr.s(slots=True, kw_only=True, repr=False)
@attr.s(slots=True, repr=False, eq=False)
class Font:
# this is the only positional argument, and it is added for compatibility with
# the defcon-style Font(path) constructor. If defcon compatibility is not a concern
# we recommend to use the alternative `open` classmethod constructor.
_path = attr.ib(default=None, metadata=dict(copyable=False))

layers = attr.ib(
default=attr.Factory(LayerSet),
validator=attr.validators.instance_of(LayerSet),
type=LayerSet,
kw_only=True,
)
info = attr.ib(
default=attr.Factory(Info), converter=_convert_Info, type=Info, kw_only=True
)
info = attr.ib(default=attr.Factory(Info), converter=_convert_Info, type=Info)
features = attr.ib(
default=attr.Factory(Features), converter=_convert_Features, type=Features
default=attr.Factory(Features),
converter=_convert_Features,
type=Features,
kw_only=True,
)
groups = attr.ib(default=attr.Factory(dict), type=dict)
kerning = attr.ib(default=attr.Factory(dict), type=dict)
lib = attr.ib(default=attr.Factory(dict), type=dict)
groups = attr.ib(default=attr.Factory(dict), type=dict, kw_only=True)
kerning = attr.ib(default=attr.Factory(dict), type=dict, kw_only=True)
lib = attr.ib(default=attr.Factory(dict), type=dict, kw_only=True)
data = attr.ib(
default=attr.Factory(DataSet), converter=_convert_DataSet, type=DataSet
default=attr.Factory(DataSet),
converter=_convert_DataSet,
type=DataSet,
kw_only=True,
)
images = attr.ib(
default=attr.Factory(ImageSet), converter=_convert_ImageSet, type=ImageSet
default=attr.Factory(ImageSet),
converter=_convert_ImageSet,
type=ImageSet,
kw_only=True,
)

_path = attr.ib(default=None, init=False)
_reader = attr.ib(default=None, init=False)
_lazy = attr.ib(default=None, kw_only=True)
_validate = attr.ib(default=True, kw_only=True)

_reader = attr.ib(default=None, kw_only=True, init=False)
_fileStructure = attr.ib(default=None, init=False)

def __attrs_post_init__(self):
if self._path is not None:
# if lazy argument is not set, default to lazy=True if path is provided
if self._lazy is None:
self._lazy = True
reader = UFOReader(self._path, validate=self._validate)
self.layers = LayerSet.read(reader, lazy=self._lazy)
self.data = DataSet.read(reader, lazy=self._lazy)
self.images = ImageSet.read(reader, lazy=self._lazy)
self.info = Info.read(reader)
self.features = Features(reader.readFeatures())
self.groups = reader.readGroups()
self.kerning = reader.readKerning()
self.lib = reader.readLib()
self._fileStructure = reader.fileStructure
if self._lazy:
# keep the reader around so we can close it when done
self._reader = reader

@classmethod
def open(cls, path, lazy=True, validate=True):
reader = UFOReader(path, validate=validate)
self = cls.read(reader, lazy=lazy)
self._path = path
self._fileStructure = reader.fileStructure
if lazy:
# keep the reader around so we can close it when done
self._reader = reader
else:
if not lazy:
reader.close()
return self

@classmethod
def read(cls, reader, lazy=True):
layers = LayerSet.read(reader, lazy=lazy)
data = DataSet.read(reader, lazy=lazy)
images = ImageSet.read(reader, lazy=lazy)
info = Info()
reader.readInfo(info)
features = Features(reader.readFeatures())
groups = reader.readGroups()
kerning = reader.readKerning()
lib = reader.readLib()
self = cls(
layers=layers,
info=info,
features=features,
groups=groups,
kerning=kerning,
lib=lib,
data=data,
images=images,
layers=LayerSet.read(reader, lazy=lazy),
data=DataSet.read(reader, lazy=lazy),
images=ImageSet.read(reader, lazy=lazy),
info=Info.read(reader),
features=Features(reader.readFeatures()),
groups=reader.readGroups(),
kerning=reader.readKerning(),
lib=reader.readLib(),
lazy=lazy,
)
self._fileStructure = reader.fileStructure
if lazy:
# keep the reader around so we can close it when done
self._reader = reader
return self

def __contains__(self, name):
Expand Down Expand Up @@ -133,10 +164,55 @@ def __repr__(self):
self.__class__.__module__, self.__class__.__name__, fontName, hex(id(self))
)

def __eq__(self, other):
# same as attrs-defined __eq__ method, only that it un-lazifies fonts if needed
if other.__class__ is not self.__class__:
return NotImplemented

for font in (self, other):
if font._lazy:
font.unlazify()

return (
self.layers,
self.info,
self.features,
self.groups,
self.kerning,
self.lib,
self.data,
self.images,
) == (
other.layers,
other.info,
other.features,
other.groups,
other.kerning,
other.lib,
other.data,
other.images,
)

def __ne__(self, other):
result = self.__eq__(other)
if result is NotImplemented:
return NotImplemented
return not result

@property
def reader(self):
return self._reader

def unlazify(self):
if self._lazy:
assert self._reader is not None
self.layers.unlazify()
self.data.unlazify()
self.images.unlazify()
self._lazy = False

__deepcopy__ = _deepcopy_unlazify_attrs

@property
def glyphOrder(self):
return list(self.lib.get("public.glyphOrder", []))
Expand All @@ -162,10 +238,7 @@ def guidelines(self, value):

@property
def path(self):
try:
return self._path
except AttributeError:
return
return self._path

def addGlyph(self, glyph):
self.layers.defaultLayer.addGlyph(glyph)
Expand Down
6 changes: 6 additions & 0 deletions src/ufoLib2/objects/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,9 @@ def _validate_weight_class(self, attribute, value):
macintoshFONDName = attr.ib(default=None, type=Optional[str])
macintoshFONDFamilyID = attr.ib(default=None, type=Optional[int])
year = attr.ib(default=None, type=Optional[int])

@classmethod
def read(cls, reader):
self = cls()
reader.readInfo(self)
return self
10 changes: 8 additions & 2 deletions src/ufoLib2/objects/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from ufoLib2.constants import DEFAULT_LAYER_NAME
from ufoLib2.objects.glyph import Glyph
from ufoLib2.objects.misc import _NOT_LOADED
from ufoLib2.objects.misc import _NOT_LOADED, _deepcopy_unlazify_attrs


def _convert_glyphs(
Expand All @@ -27,7 +27,7 @@ class Layer:
color = attr.ib(default=None, type=Optional[str])
lib = attr.ib(default=attr.Factory(dict), type=dict)

_glyphSet = attr.ib(default=None, init=False)
_glyphSet = attr.ib(default=None, init=False, eq=False)

@classmethod
def read(cls, name, glyphSet, lazy=True):
Expand All @@ -46,6 +46,12 @@ def read(cls, name, glyphSet, lazy=True):
glyphSet.readLayerInfo(self)
return self

def unlazify(self):
for _ in self:
pass

__deepcopy__ = _deepcopy_unlazify_attrs

def __contains__(self, name):
return name in self._glyphs

Expand Down
10 changes: 8 additions & 2 deletions src/ufoLib2/objects/layerSet.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ufoLib2.constants import DEFAULT_LAYER_NAME
from ufoLib2.objects.layer import Layer
from ufoLib2.objects.misc import _NOT_LOADED
from ufoLib2.objects.misc import _NOT_LOADED, _deepcopy_unlazify_attrs


def _convert_layers(value: Iterable[Layer]) -> "OrderedDict[str, Layer]":
Expand All @@ -30,7 +30,7 @@ class LayerSet:
)
defaultLayer = attr.ib(default=None, type=Layer)

_reader = attr.ib(default=None, init=False)
_reader = attr.ib(default=None, init=False, eq=False)

def __attrs_post_init__(self):
if not self._layers:
Expand Down Expand Up @@ -83,6 +83,12 @@ def read(cls, reader, lazy=True):

return self

def unlazify(self):
for layer in self:
layer.unlazify()

__deepcopy__ = _deepcopy_unlazify_attrs

@staticmethod
def _loadLayer(reader, layerName, lazy=True, default=False):
# UFOReader.getGlyphSet method doesn't support 'defaultLayer'
Expand Down
24 changes: 23 additions & 1 deletion src/ufoLib2/objects/misc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Mapping, MutableMapping
from copy import deepcopy
from typing import Sequence, Union

import attr
Expand All @@ -8,6 +9,21 @@
_NOT_LOADED = object()


def _deepcopy_unlazify_attrs(self, memo):

if getattr(self, "_lazy", True) and hasattr(self, "unlazify"):
self.unlazify()
return self.__class__(
**{
(a.name if a.name[0] != "_" else a.name[1:]): deepcopy(
getattr(self, a.name), memo
)
for a in attr.fields(self.__class__)
if a.init and a.metadata.get("copyable", True)
},
)


@attr.s(slots=True, repr=False)
class DataStore(MutableMapping):
listdir = None
Expand All @@ -17,7 +33,7 @@ class DataStore(MutableMapping):

_data = attr.ib(default=attr.Factory(dict), type=dict)

_reader = attr.ib(default=None, init=False, repr=False)
_reader = attr.ib(default=None, init=False, repr=False, eq=False)
_scheduledForDeletion = attr.ib(default=attr.Factory(set), init=False, repr=False)

@classmethod
Expand All @@ -32,6 +48,12 @@ def read(cls, reader, lazy=True):
self._reader = reader
return self

def unlazify(self):
for _ in self.items():
pass

__deepcopy__ = _deepcopy_unlazify_attrs

# MutableMapping methods

def __len__(self):
Expand Down
Loading