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

Annotate and document info.py. #812

Draft
wants to merge 9 commits into
base: v1
Choose a base branch
from
Draft
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
132 changes: 88 additions & 44 deletions Lib/fontParts/base/info.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Callable,
List,
Optional,
Union,
)

from fontTools.ufoLib import fontInfoAttributesVersion3
from fontTools.ufoLib import validateFontInfoVersion3ValueForAttribute
from fontMath import MathInfo
from fontMath.mathFunctions import setRoundIntegerFunction

from fontParts.base.base import BaseObject, dynamicProperty, interpolate, reference
from fontParts.base import normalizers
from fontParts.base.errors import FontPartsError
from fontParts.base.deprecated import DeprecatedInfo, RemovedInfo
from fontParts.base.annotations import (
TransformationType,
)

if TYPE_CHECKING:
from fontParts.base.font import BaseFont


# Notes


class BaseInfo(BaseObject, DeprecatedInfo, RemovedInfo):
from fontTools.ufoLib import fontInfoAttributesVersion3
"""Represent the basis for an info object."""

fontInfoAttributes = set(fontInfoAttributesVersion3)
fontInfoAttributes.remove("guidelines")
copyAttributes = tuple(fontInfoAttributes)

def _reprContents(self):
def _reprContents(self) -> List[str]:
contents = []
if self.font is not None:
contents.append("for font")
Expand All @@ -24,16 +48,34 @@ def _reprContents(self):

# Font

_font = None
_font: Optional[Callable[[], BaseFont]] = None

font: dynamicProperty = dynamicProperty(
"font",
"""Get or set the info's parent font object.

The value must be a :class:`BaseFont` instance or :obj:`None`.

:return: The :class:`BaseFont` instance containing the info
or :obj:`None`.
:raises AssertionError: If attempting to set the font when it
has already been set.

Example::

>>> font = info.font

font = dynamicProperty("font", "The info's parent font.")
""",
)

def _get_font(self):
def _get_font(self) -> Optional[BaseFont]:
if self._font is None:
return None
return self._font()

def _set_font(self, font):
def _set_font(
self, font: Optional[Union[BaseFont, Callable[[], BaseFont]]]
) -> None:
if self._font is not None and self._font != font:
raise AssertionError("font for info already set and is not same as font")
if font is not None:
Expand All @@ -45,9 +87,7 @@ def _set_font(self, font):
# ----------

@staticmethod
def _validateFontInfoAttributeValue(attr, value):
from fontTools.ufoLib import validateFontInfoVersion3ValueForAttribute

def _validateFontInfoAttributeValue(attr: str, value: Any):
valid = validateFontInfoVersion3ValueForAttribute(attr, value)
if not valid:
raise ValueError(f"Invalid value {value} for attribute '{attr}'.")
Expand All @@ -59,70 +99,64 @@ def _validateFontInfoAttributeValue(attr, value):

# has

def __hasattr__(self, attr):
from fontTools.ufoLib import fontInfoAttributesVersion3

def __hasattr__(self, attr: str) -> bool:
if attr in fontInfoAttributesVersion3:
return True
return super(BaseInfo, self).__hasattr__(attr)

# get

def __getattribute__(self, attr):
from fontTools.ufoLib import fontInfoAttributesVersion3

def __getattribute__(self, attr: str) -> None:
if attr != "guidelines" and attr in fontInfoAttributesVersion3:
value = self._getAttr(attr)
if value is not None:
value = self._validateFontInfoAttributeValue(attr, value)
return value
return super(BaseInfo, self).__getattribute__(attr)

def _getAttr(self, attr):
def _getAttr(self, attr: str) -> None:
"""
Subclasses may override this method.

If a subclass does not override this method,
it must implement '_get_attributeName' methods
for all Info methods.
"""
meth = f"_get_{attr}"
if not hasattr(self, meth):
methodName = f"_get_{attr}"
if not hasattr(self, methodName):
raise AttributeError(f"No getter for attribute '{attr}'.")
meth = getattr(self, meth)
value = meth()
method = getattr(self, methodName)
value = method()
return value

# set

def __setattr__(self, attr, value):
from fontTools.ufoLib import fontInfoAttributesVersion3

def __setattr__(self, attr: str, value: Any) -> None:
if attr != "guidelines" and attr in fontInfoAttributesVersion3:
if value is not None:
value = self._validateFontInfoAttributeValue(attr, value)
return self._setAttr(attr, value)
return super(BaseInfo, self).__setattr__(attr, value)

def _setAttr(self, attr, value):
def _setAttr(self, attr: str, value: Any) -> None:
"""
Subclasses may override this method.

If a subclass does not override this method,
it must implement '_set_attributeName' methods
for all Info methods.
"""
meth = f"_set_{attr}"
if not hasattr(self, meth):
methodName = f"_set_{attr}"
if not hasattr(self, methodName):
raise AttributeError(f"No setter for attribute '{attr}'.")
meth = getattr(self, meth)
meth(value)
method = getattr(self, methodName)
method(value)

# -------------
# Normalization
# -------------

def round(self):
def round(self) -> None:
"""
Round the following attributes to integers:

Expand Down Expand Up @@ -177,12 +211,10 @@ def round(self):
"""
self._round()

def _round(self, **kwargs):
def _round(self, **kwargs: Any) -> None:
"""
Subclasses may override this method.
"""
from fontMath.mathFunctions import setRoundIntegerFunction

setRoundIntegerFunction(normalizers.normalizeVisualRounding)

mathInfo = self._toMathInfo(guidelines=False)
Expand All @@ -193,14 +225,14 @@ def _round(self, **kwargs):
# Updating
# --------

def update(self, other):
def update(self, other: BaseInfo) -> None:
"""
Update this object with the values
from **otherInfo**.
"""
self._update(other)

def _update(self, other):
def _update(self, other: BaseInfo) -> None:
"""
Subclasses may override this method.
"""
Expand All @@ -216,7 +248,7 @@ def _update(self, other):
# Interpolation
# -------------

def toMathInfo(self, guidelines=True):
def toMathInfo(self, guidelines=True) -> MathInfo:
"""
Returns the info as an object that follows the
`MathGlyph protocol <https://github.com/typesupply/fontMath>`_.
Expand All @@ -225,7 +257,7 @@ def toMathInfo(self, guidelines=True):
"""
return self._toMathInfo(guidelines=guidelines)

def fromMathInfo(self, mathInfo, guidelines=True):
def fromMathInfo(self, mathInfo, guidelines=True) -> BaseInfo:
"""
Replaces the contents of this info object with the contents of ``mathInfo``.

Expand All @@ -236,11 +268,10 @@ def fromMathInfo(self, mathInfo, guidelines=True):
"""
return self._fromMathInfo(mathInfo, guidelines=guidelines)

def _toMathInfo(self, guidelines=True):
def _toMathInfo(self, guidelines=True) -> MathInfo:
"""
Subclasses may override this method.
"""
import fontMath

# A little trickery is needed here because MathInfo
# handles font level guidelines. Those are not in this
Expand All @@ -258,11 +289,11 @@ def _toMathInfo(self, guidelines=True):
color=guideline.color,
)
self.guidelines.append(d)
info = fontMath.MathInfo(self)
info = MathInfo(self)
del self.guidelines
return info

def _fromMathInfo(self, mathInfo, guidelines=True):
def _fromMathInfo(self, mathInfo, guidelines=True) -> None:
"""
Subclasses may override this method.
"""
Expand All @@ -278,7 +309,14 @@ def _fromMathInfo(self, mathInfo, guidelines=True):
# XXX identifier is lost
)

def interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True):
def interpolate(
self,
factor: TransformationType,
minInfo: BaseInfo,
maxInfo: BaseInfo,
round: bool = True,
suppressError: bool = True,
) -> None:
"""
Interpolate all pairs between minInfo and maxInfo.
The interpolation occurs on a 0 to 1.0 range where minInfo
Expand Down Expand Up @@ -310,11 +348,17 @@ def interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True):
factor, minInfo, maxInfo, round=round, suppressError=suppressError
)

def _interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True):
def _interpolate(
self,
factor: TransformationType,
minInfo: BaseInfo,
maxInfo: BaseInfo,
round: bool = True,
suppressError: bool = True,
) -> None:
"""
Subclasses may override this method.
"""
from fontMath.mathFunctions import setRoundIntegerFunction

setRoundIntegerFunction(normalizers.normalizeVisualRounding)

Expand All @@ -326,5 +370,5 @@ def _interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True)
f"Info from font '{minInfo.font.name}' and font '{maxInfo.font.name}' could not be interpolated."
)
if round:
result = result.round()
result = result.round() # type: ignore[func-returns-value]
self._fromMathInfo(result)