diff --git a/Lib/fontParts/base/info.py b/Lib/fontParts/base/info.py index 4ca0c300..1978143f 100644 --- a/Lib/fontParts/base/info.py +++ b/Lib/fontParts/base/info.py @@ -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") @@ -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: @@ -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}'.") @@ -59,18 +99,14 @@ 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: @@ -78,7 +114,7 @@ def __getattribute__(self, attr): return value return super(BaseInfo, self).__getattribute__(attr) - def _getAttr(self, attr): + def _getAttr(self, attr: str) -> None: """ Subclasses may override this method. @@ -86,25 +122,23 @@ def _getAttr(self, attr): 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. @@ -112,17 +146,17 @@ def _setAttr(self, attr, value): 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: @@ -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) @@ -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. """ @@ -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 `_. @@ -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``. @@ -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 @@ -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. """ @@ -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 @@ -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) @@ -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)