From db41c34ec5b82b60382d7eea5cb2fe1fd839d753 Mon Sep 17 00:00:00 2001 From: Knut Nergaard Date: Wed, 20 Nov 2024 14:08:58 +0100 Subject: [PATCH 1/8] Improve `contour.py`. - Add type annotation. - Add/improve documentation. - changed `BaseSegments.segments` to return `tuple` instead of `list`. --- Lib/fontParts/base/contour.py | 884 ++++++++++++++++++++++++++-------- 1 file changed, 671 insertions(+), 213 deletions(-) diff --git a/Lib/fontParts/base/contour.py b/Lib/fontParts/base/contour.py index 291f41fc..33e6b5ad 100644 --- a/Lib/fontParts/base/contour.py +++ b/Lib/fontParts/base/contour.py @@ -1,3 +1,6 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Union + from fontParts.base.errors import FontPartsError from fontParts.base.base import ( BaseObject, @@ -10,8 +13,26 @@ ) from fontParts.base import normalizers from fontParts.base.compatibility import ContourCompatibilityReporter -from fontParts.base.bPoint import absoluteBCPIn, absoluteBCPOut from fontParts.base.deprecated import DeprecatedContour, RemovedContour +from fontParts.base.annotations import ( + QuadrupleType, + PairCollectionType, + CollectionType, + SextupleCollectionType, + IntFloatType, + PenType, + PointPenType, +) + +if TYPE_CHECKING: + from fontParts.base.point import BasePoint + from fontParts.base.bPoint import BaseBPoint + from fontParts.base.segment import BaseSegment + from fontParts.base.glyph import BaseGlyph + from fontParts.base.layer import BaseLayer + from fontParts.base.font import BaseFont + +PointCollectionType = CollectionType[PairCollectionType[IntFloatType]] class BaseContour( @@ -23,10 +44,18 @@ class BaseContour( DeprecatedContour, RemovedContour, ): + """Represent the basis for a contour object. + + :cvar segmentClass: A class representing contour segments. This will + usually be a :class:`BaseSegment` subclass. + :cvar bPointClass: A class representing contour bPoints. This will + usually be a :class:`BaseBPoint` subclass. + + """ segmentClass = None bPointClass = None - def _reprContents(self): + def _reprContents(self) -> List[str]: contents = [] if self.identifier is not None: contents.append(f"identifier='{self.identifier!r}'") @@ -35,7 +64,23 @@ def _reprContents(self): contents += self.glyph._reprContents() return contents - def copyData(self, source): + def copyData(self, source: BaseContour) -> None: + """Copy data from another contour instance. + + This will copy the contents of the following attributes from `source` + into the current contour instance: + + - :attr:`BaseContour.points` + - :attr:`BaseContour.bPoints` + + :param source: The source :class:`BaseContour` instance from which + to copy data. + + Example:: + + >>> contour.copyData(sourceContour) + + """ super(BaseContour, self).copyData(source) for sourcePoint in source.points: self.appendPoint((0, 0)) @@ -50,14 +95,30 @@ def copyData(self, source): _glyph = None - glyph = dynamicProperty("glyph", "The contour's parent :class:`BaseGlyph`.") + glyph: dynamicProperty = dynamicProperty( + "glyph", + """Get or set the contour's parent glyph object. + + The value must be a :class:`BaseGlyph` instance or :obj:`None`. + + :return: The :class:`BaseGlyph` instance containing the contour + or :obj:`None`. + :raises AssertionError: If attempting to set the glyph when it + has already been set. + + Example:: - def _get_glyph(self): + >>> glyph = contour.glyph + + """, + ) + + def _get_glyph(self) -> Optional[BaseGlyph]: if self._glyph is None: return None return self._glyph() - def _set_glyph(self, glyph): + def _set_glyph(self, glyph: Optional[BaseGlyph]) -> None: if self._glyph is not None: raise AssertionError("glyph for contour already set") if glyph is not None: @@ -66,18 +127,45 @@ def _set_glyph(self, glyph): # Font - font = dynamicProperty("font", "The contour's parent font.") + font: dynamicProperty = dynamicProperty( + "font", + """Get the contour's parent font object. + + This property is read-only. + + :return: The :class:`BaseFont` instance containing the contour + or :obj:`None`. + + Example:: + + >>> font = contour.font - def _get_font(self): + """,) + + def _get_font(self) -> Optional[BaseFont]: if self._glyph is None: return None return self.glyph.font # Layer - layer = dynamicProperty("layer", "The contour's parent layer.") + layer: dynamicProperty = dynamicProperty( + "layer", + """Get the contour's parent layer object. + + This property is read-only. + + :return: The :class:`BaseLayer` instance containing the contour + or :obj:`None`. + + Example:: - def _get_layer(self): + >>> layer = contour.layer + + """, + ) + + def _get_layer(self) -> Optional[BaseLayer]: if self._glyph is None: return None return self.glyph.layer @@ -88,20 +176,26 @@ def _get_layer(self): # index - index = dynamicProperty( + index: dynamicProperty = dynamicProperty( "base_index", - """ - The index of the contour within the parent glyph's contours. + """Get or set the index of the contour. + + The value must be an :class:`int`. + + :return: An :class:`int` representing the contour's index within an + ordered list of the parent glyph's contours, or :obj:`None` if the + contour does not belong to a glyph. + :raises FontPartsError: If the contour does not belong to a glyph. + + Example:: >>> contour.index - 1 - >>> contour.index = 0 + 0 - The value will always be a :ref:`type-int`. """, ) - def _get_base_index(self): + def _get_base_index(self) -> Optional[int]: glyph = self.glyph if glyph is None: return None @@ -109,7 +203,7 @@ def _get_base_index(self): value = normalizers.normalizeIndex(value) return value - def _set_base_index(self, value): + def _set_base_index(self, value: int) -> None: glyph = self.glyph if glyph is None: raise FontPartsError("The contour does not belong to a glyph.") @@ -121,39 +215,81 @@ def _set_base_index(self, value): value = contourCount self._set_index(value) - def _get_index(self): - """ - Subclasses may override this method. + def _get_index(self) -> Optional[int]: + """Get the index of the native contour. + + This is the environment implementation of the :attr:`BaseContour.index` + property getter. + + :return: An :class:`int` representing the contour's index within an + ordered list of the parent glyph's contours, or :obj:`None` if the + contour does not belong to a glyph. The value will be + normalized with :func:`normalizers.normalizeIndex`. + + .. note:: + + Subclasses may override this method. + """ glyph = self.glyph return glyph.contours.index(self) - def _set_index(self, value): - """ - Subclasses must override this method. + def _set_index(self, value: int) -> None: + """Set the index of the contour. + + This is the environment implementation of the :attr:`BaseContour.index` + property setter. + + :param value: The index to set as an :class:`int`. The value will have + been normalized with :func:`normalizers.normalizeIndex`. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. important:: + + Subclasses must override this method. + """ self.raiseNotImplementedError() # identifier - def getIdentifierForPoint(self, point): - """ - Create a unique identifier for and assign it to ``point``. - If the point already has an identifier, the existing - identifier will be returned. + def getIdentifierForPoint(self, point: BasePoint) -> str: + """Generate and assign a unique identifier to the given point. + + If `point` already has an identifier, the existing identifier is returned. + Otherwise, a new unique identifier is created and assigned to `point`. + + :param point: The :class:`BasePoint` instance to which the identifier + should be assigned. + :return: A :class:`str` representing the newly assigned identifier. + + Example:: >>> contour.getIdentifierForPoint(point) 'ILHGJlygfds' - ``point`` must be a :class:`BasePoint`. The returned value - will be a :ref:`type-identifier`. """ point = normalizers.normalizePoint(point) return self._getIdentifierforPoint(point) - def _getIdentifierforPoint(self, point): - """ - Subclasses must override this method. + def _getIdentifierForPoint(self, point: BasePoint) -> str: + """Generate and assign a unique identifier to the given native point. + + This is the environment implementation + of :meth:`BaseContour.getIdentifierForPoint`. + + :param point: The :class:`BasePoint` subclass instance to which the + identifier should be assigned. The value will have been normalized + with :func:`normalizers.normalizePoint`. + :return: A :class:`str` representing the newly assigned identifier. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. important:: + + Subclasses must override this method. + """ self.raiseNotImplementedError() @@ -161,34 +297,67 @@ def _getIdentifierforPoint(self, point): # Pens # ---- - def draw(self, pen): - """ - Draw the contour's outline data to the given :ref:`type-pen`. + def draw(self, pen: PenType) -> None: + """Draw the contour's outline data to the given pen. + + :param pen: The :class:`fontTools.pens.basePen.AbstractPen` to which the + outline data should be drawn. + + Example:: >>> contour.draw(pen) + """ self._draw(pen) - def _draw(self, pen, **kwargs): - """ - Subclasses may override this method. + def _draw(self, pen: PenType, **kwargs: Any) -> None: + r"""Draw the native contour's outline data to the given pen. + + This is the environment implementation of :meth:`BaseContour.draw`. + + :param pen: The :class:`fontTools.pens.basePen.AbstractPen` to which the + outline data should be drawn. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ from fontTools.ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter) - def drawPoints(self, pen): - """ - Draw the contour's outline data to the given :ref:`type-point-pen`. + adapter = PointToSegmentPen(pen) + self.drawPoints(adapter) + + def drawPoints(self, pen: PointPenType) -> None: + """Draw the contour's outline data to the given point pen. + + :param pen: The :class:`fontTools.pens.basePen.AbstractPointPen` to + which the outline data should be drawn. + + Example:: >>> contour.drawPoints(pointPen) + """ self._drawPoints(pen) - def _drawPoints(self, pen, **kwargs): - """ - Subclasses may override this method. + def _drawPoints(self, pen: PointPenType, **kwargs: Any) -> None: + r"""Draw the native contour's outline data to the given point pen. + + This is the environment implementation of :meth:`BaseContour.drawPoints`. + + :param pen: The :class:`fontTools.pens.basePen.AbstractPointPen` to + which the outline data should be drawn. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ # The try: ... except TypeError: ... # handles backwards compatibility with @@ -223,32 +392,55 @@ def _drawPoints(self, pen, **kwargs): # Data normalization # ------------------ - def autoStartSegment(self): - """ - Automatically calculate and set the first segment - in this contour. + def autoStartSegment(self) -> None: + """Automatically calculate and set the contour's first segment. The behavior of this may vary accross environments. + + Example:: + + >>> contour.autoStartSegment() + """ self._autoStartSegment() - def _autoStartSegment(self, **kwargs): - """ - Subclasses may override this method. + def _autoStartSegment(self, **kwargs: Any) -> None: + r"""Automatically calculate and set the native contour's first segment. + + This is the environment implementation of :meth:`BaseContour.autoStartSegment`. + + :param \**kwargs: Additional keyword arguments. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. note:: + + Subclasses may override this method. - XXX port this from robofab """ self.raiseNotImplementedError() - def round(self): - """ - Round coordinates in all points to integers. + def round(self) -> None: + """Round all point coordinates in the contour to the neares integer. + + Example:: + + >>> contour.round() + """ self._round() - def _round(self, **kwargs): - """ - Subclasses may override this method. + def _round(self, **kwargs: Any) -> None: + r"""Round all point coordinates in the native contour to the neares integer. + + This is the environment implementation of :meth:`BaseContour.round`. + + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ for point in self.points: point.round() @@ -257,9 +449,21 @@ def _round(self, **kwargs): # Transformation # -------------- - def _transformBy(self, matrix, **kwargs): - """ - Subclasses may override this method. + def _transformBy(self, + matrix: SextupleCollectionType[IntFloatType], + **kwargs: Any) -> None: + r"""Transform the contour according to the given matrix. + + This is the environment implementation of :meth:`BaseContour.transformBy`. + + :param matrix: The :ref:`type-transformation` to apply. The value will + be normalized with :func:`normalizers.normalizeTransformationMatrix`. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ for point in self.points: point.transformBy(matrix) @@ -270,9 +474,16 @@ def _transformBy(self, matrix, **kwargs): compatibilityReporterClass = ContourCompatibilityReporter - def isCompatible(self, other): - """ - Evaluate interpolation compatibility with **other**. :: + def isCompatible(self, other: BaseContour) -> tuple[bool, str]: + """Evaluate interpolation compatibility with another contour. + + :param other: The other :class:`BaseContour` instance to check + compatibility with. + :return: A :class:`tuple` where the first element is a :class:`bool` + indicating compatibility, and the second element is a :class:`str` + of compatibility notes. + + Example:: >>> compatible, report = self.isCompatible(otherContour) >>> compatible @@ -282,18 +493,24 @@ def isCompatible(self, other): [Fatal] Contour: [0] contains 4 segments | [0] contains 3 segments [Fatal] Contour: [0] is closed | [0] is open - This will return a ``bool`` indicating if the contour is - compatible for interpolation with **other** and a - :ref:`type-string` of compatibility notes. """ return super(BaseContour, self).isCompatible(other, BaseContour) - def _isCompatible(self, other, reporter): - """ - This is the environment implementation of - :meth:`BaseContour.isCompatible`. + def _isCompatible(self, + other: BaseContour, + reporter: ContourCompatibilityReporter) -> None: + """Evaluate interpolation compatibility with another native contour. + + This is the environment implementation of :meth:`BaseContour.isCompatible`. + + :param other: The other :class:`BaseContour` instance to check + compatibility with. + :param reporter: An object used to report compatibility issues. + + .. note:: + + Subclasses may override this method. - Subclasses may override this method. """ contour1 = self contour2 = other @@ -318,21 +535,47 @@ def _isCompatible(self, other, reporter): if segmentCompatibility.warning: reporter.warning = True reporter.segments.append(segmentCompatibility) - # ---- # Open # ---- - open = dynamicProperty("base_open", "Boolean indicating if the contour is open.") + open: dynamicProperty = dynamicProperty( + "base_open", + """Determine whether the contour is open. + + This property is read-only. + + :return: :obj:`True` if the contour is open, otherwise :obj:`False`. + + Example:: + + >>> contour.open + True + + """ + ) - def _get_base_open(self): + def _get_base_open(self) -> bool: value = self._get_open() value = normalizers.normalizeBoolean(value) return value - def _get_open(self): - """ - Subclasses must override this method. + def _get_open(self) -> bool: + """Determine whether the native contour is open. + + This is the environment implementation of the :attr:`BaseContour.open` + property getter. + + :return: :obj:`True` if the contour is open, otherwise :obj:`False`. + The value will have been normalized + with :func:`normalizers.normalizeBoolean`. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. important:: + + Subclasses must override this method. + """ self.raiseNotImplementedError() @@ -340,42 +583,92 @@ def _get_open(self): # Direction # --------- - clockwise = dynamicProperty( + clockwise: dynamicProperty = dynamicProperty( "base_clockwise", - ("Boolean indicating if the contour's " "winding direction is clockwise."), + """Specify or determine whether the contour's winding direction is clockwise. + + The value must be a :class:`bool` indicating the contour's winding + direction. + + :return: :obj:`True` if the contour's winding direction is clockwise, + otherwise :obj:`False`. + + """, ) - def _get_base_clockwise(self): + def _get_base_clockwise(self) -> bool: value = self._get_clockwise() value = normalizers.normalizeBoolean(value) return value - def _set_base_clockwise(self, value): + def _set_base_clockwise(self, value: bool) -> None: value = normalizers.normalizeBoolean(value) self._set_clockwise(value) - def _get_clockwise(self): - """ - Subclasses must override this method. + def _get_clockwise(self) -> bool: + """Determine whether the native contour's winding direction is clockwise. + + This is the environment implementation of the :attr:`BaseContour.clockwise` + property getter. + + :return: :obj:`True` if the contour's winding direction is clockwise, + otherwise :obj:`False`. The value will have been normalized with + :func:`normalizers.normalizeBoolean`. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. important:: + + Subclasses must override this method. + """ self.raiseNotImplementedError() - def _set_clockwise(self, value): - """ - Subclasses may override this method. + def _set_clockwise(self, value: bool) -> None: + """Specify whether the native contour's winding direction is clockwise. + + This is the environment implementation of the :attr:`BaseContour.clockwise` + property setter. + + :param value: The winding direction to indicate as a :class:`bool`. + The value will have been normalized + with :func:`normalizers.normalizeBoolean`. + + .. note:: + + Subclasses may override this method. + """ if self.clockwise != value: self.reverse() - def reverse(self): - """ - Reverse the direction of the contour. + def reverse(self) -> None: + """Reverse the direction of the contour. + + Example:: + + >>> contour.clockwise + False + >>> contour.reverse() + >>> contour.clockwise + True + """ self._reverseContour() - def _reverse(self, **kwargs): - """ - Subclasses may override this method. + def _reverse(self, **kwargs) -> None: + r"""Reverse the direction of the contour. + + This is the environment implementation of :meth:`BaseContour.reverse`. + + :param \**kwargs: Additional keyword arguments. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. note:: + + Subclasses may override this method. + """ self.raiseNotImplementedError() @@ -383,21 +676,36 @@ def _reverse(self, **kwargs): # Point and Contour Inside # ------------------------ - def pointInside(self, point): - """ - Determine if ``point`` is in the black or white of the contour. + def pointInside(self, point: PairCollectionType[IntFloatType]) -> bool: + """Check if `point` is within the filled area of the contour. + + :param point: The point to check as a :ref:`type-coordinate`. + :return: :obj:`True` if `point` is inside the filled area of the + contour, :obj:`False` otherwise. + + Example:: >>> contour.pointInside((40, 65)) True - ``point`` must be a :ref:`type-coordinate`. """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point) - def _pointInside(self, point): - """ - Subclasses may override this method. + def _pointInside(self, point: PairCollectionType[IntFloatType]) -> bool: + """Check if `point` is within the filled area of the native contour. + + This is the environment implementation of :meth:`BaseContour.pointInside`. + + :param point: The point to check as a :ref:`type-coordinate`. The value + will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. + :return: :obj:`True` if `point` is inside the filled area of the + contour, :obj:`False` otherwise. + + .. note:: + + Subclasses may override this method. + """ from fontTools.pens.pointInsidePen import PointInsidePen @@ -405,9 +713,12 @@ def _pointInside(self, point): self.draw(pen) return pen.getResult() - def contourInside(self, otherContour): - """ - Determine if ``otherContour`` is in the black or white of this contour. + def contourInside(self, otherContour: BaseContour) -> bool: + """Check if `otherContour` is within the current contour's filled area. + + :param point: The :class:`BaseContour` instance to check. + :return: :obj:`True` if `otherContour` is inside the filled area of the + current contour instance, :obj:`False` otherwise. >>> contour.contourInside(otherContour) True @@ -417,9 +728,22 @@ def contourInside(self, otherContour): otherContour = normalizers.normalizeContour(otherContour) return self._contourInside(otherContour) - def _contourInside(self, otherContour): - """ - Subclasses may override this method. + def _contourInside(self, otherContour: BaseContour) -> bool: + """Check if `otherContour` is within the current native contour's filled area. + + This is the environment implementation of :meth:`BaseContour.contourInside`. + + :param point: The :class:`BaseContour` instance to check. The value will have + been normalized with :func:`normalizers.normalizeContour`. + :return: :obj:`True` if `otherContour` is inside the filled area of the + current contour instance, :obj:`False` otherwise. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. note:: + + Subclasses may override this method. + """ self.raiseNotImplementedError() @@ -427,19 +751,47 @@ def _contourInside(self, otherContour): # Bounds and Area # --------------- - bounds = dynamicProperty( - "bounds", ("The bounds of the contour: " "(xMin, yMin, xMax, yMax) or None.") + bounds: dynamicProperty = dynamicProperty( + "bounds", + """Get the bounds of the contour. + + This property is read-only. + + :return: A :class:`tuple` of four :class:`int` or :class:`float` values + in the form ``(x minimum, y minimum, x maximum, y maximum)`` + representing the bounds of the contour, or :obj:`None` if the contour + is open. + + Example:: + + >>> contour.bounds + (10, 30, 765, 643) + + + """ ) - def _get_base_bounds(self): + def _get_base_bounds(self) -> Optional[QuadrupleType[float]]: value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value - def _get_bounds(self): - """ - Subclasses may override this method. + def _get_bounds(self) -> Optional[QuadrupleType[float]]: + """Get the bounds of the contour. + + This is the environment implementation of the :attr:`BaseContour.bounds` + property getter. + + :return: A :class:`tuple` of four :class:`int` or :class:`float` values + in the form ``(x minimum, y minimum, x maximum, y maximum)`` + representing the bounds of the contour, or :obj:`None` if the contour + is open. + + .. note:: + + Subclasses may override this method. + """ from fontTools.pens.boundsPen import BoundsPen @@ -447,19 +799,42 @@ def _get_bounds(self): self.draw(pen) return pen.bounds - area = dynamicProperty( - "area", ("The area of the contour: " "A positive number or None.") + area: dynamicProperty = dynamicProperty( + "area", + """Get the area of the contour + + This property is read-only. + + :return: A positive :class:`int` or a :class:` float value representing + the area of the contour, or :obj:`None` if the contour is open. + + Example:: + + >>> contour.area + 583 + + """ ) - def _get_base_area(self): + def _get_base_area(self) -> Optional[float]: value = self._get_area() if value is not None: value = normalizers.normalizeArea(value) return value - def _get_area(self): - """ - Subclasses may override this method. + def _get_area(self) -> Optional[float]: + """Get the area of the native contour + + This is the environment implementation of the :attr:`BaseContour.area` + property getter. + + :return: A positive :class:`int` or a :class:` float value representing + the area of the contour, or :obj:`None` if the contour is open. + + .. note:: + + Subclasses may override this method. + """ from fontTools.pens.areaPen import AreaPen @@ -476,19 +851,37 @@ def _get_area(self): # other than registering segmentClass. Subclasses may choose to # implement this API independently if desired. - def _setContourInSegment(self, segment): + def _setContourInSegment(self, segment: BaseSegment) -> None: if segment.contour is None: segment.contour = self - segments = dynamicProperty("segments") + segments: dynamicProperty = dynamicProperty( + "segments", + """Get the countour's segments. + + This property is read-only. + + :return: A :class:`tuple` of :class:`BaseSegment` instances. - def _get_segments(self): """ - Subclasses may override this method. + ) + + def _get_segments(self) -> Tuple[BaseSegment]: + """Get the native countour's segments. + + This is the environment implementation of the :attr:`BaseContour.segments` + property getter. + + :return: A :class:`tuple` of :class:`BaseSegment` subclass instances. + + .. note:: + + Subclasses may override this method. + """ - points = list(self.points) + points = self.points if not points: - return [] + return () segments = [[]] lastWasOffCurve = False firstIsMove = points[0].type == "move" @@ -517,15 +910,29 @@ def _get_segments(self): s._setPoints(points) self._setContourInSegment(s) wrapped.append(s) - return wrapped + return tuple(wrapped) + + def __getitem__(self, index: int) -> BaseSegment: + """Get the segment at the specified index. - def __getitem__(self, index): + :param index: The zero-based index of the point to retrieve as + an :class:`int`. + :return: The :class:`BaseSegment` instance located at the specified `index`. + :raises IndexError: If the specified `index` is out of range. + + """ return self.segments[index] - def __iter__(self): + def __iter__(self) -> Iterator[BaseSegment]: + """Return an iterator over the segments in the contour. + + :return: An iterator over the :class:`BaseSegment` instances belonging to + the contour. + + """ return self._iterSegments() - def _iterSegments(self): + def _iterSegments(self) -> Iterator[BaseSegment]: segments = self.segments count = len(segments) index = 0 @@ -534,16 +941,36 @@ def _iterSegments(self): count -= 1 index += 1 - def __len__(self): - return self._len__segments() + def __len__(self) -> int: + """Return the number of segments in the contour. + + :return: An :class:`int` representing the number of :class:`BaseSegment` + instances belonging to the contour. - def _len__segments(self, **kwargs): """ - Subclasses may override this method. + return self._len__segments() + + def _len__segments(self, **kwargs: Any) -> int: + r"""Return the number of segments in the native contour. + + This is the environment implementation of :meth:`BaseContour.__len__`. + + :return: An :class:`int` representing the number of :class:`BaseSegment` + subclass instances belonging to the contour. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ return len(self.segments) - def appendSegment(self, type=None, points=None, smooth=False, segment=None): + def appendSegment(self, + type: Optional[str] = None, + points: Optional[PointCollectionType] = None, + smooth: bool = False, + segment: Optional[BaseSegment] = None) -> None: """ Append a segment to the contour. """ @@ -562,7 +989,11 @@ def appendSegment(self, type=None, points=None, smooth=False, segment=None): smooth = normalizers.normalizeBoolean(smooth) self._appendSegment(type=type, points=points, smooth=smooth) - def _appendSegment(self, type=None, points=None, smooth=False, **kwargs): + def _appendSegment(self, + type: Optional[str] = None, + points: Optional[PointCollectionType] = None, + smooth: bool = False, + **kwargs: Any) -> None: """ Subclasses may override this method. """ @@ -570,7 +1001,12 @@ def _appendSegment(self, type=None, points=None, smooth=False, **kwargs): len(self), type=type, points=points, smooth=smooth, **kwargs ) - def insertSegment(self, index, type=None, points=None, smooth=False, segment=None): + def insertSegment(self, + index: int, + type: Optional[str] = None, + points: Optional[PointCollectionType] = None, + smooth: bool = False, + segment: Optional[BaseSegment] = None) -> None: """ Insert a segment into the contour. """ @@ -590,9 +1026,12 @@ def insertSegment(self, index, type=None, points=None, smooth=False, segment=Non smooth = normalizers.normalizeBoolean(smooth) self._insertSegment(index=index, type=type, points=points, smooth=smooth) - def _insertSegment( - self, index=None, type=None, points=None, smooth=False, **kwargs - ): + def _insertSegment(self, + index: int, + type: Optional[str], + points: Optional[PointCollectionType], + smooth: bool, + **kwargs: Any) -> None: """ Subclasses may override this method. """ @@ -608,7 +1047,9 @@ def _insertSegment( for offCurvePoint in reversed(offCurve): self.insertPoint(ptCount, offCurvePoint, type="offcurve") - def removeSegment(self, segment, preserveCurve=False): + def removeSegment(self, + segment: Union[int, BaseSegment], + preserveCurve: bool = False) -> None: """ Remove segment from the contour. If ``preserveCurve`` is set to ``True`` an attempt @@ -623,7 +1064,7 @@ def removeSegment(self, segment, preserveCurve=False): preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removeSegment(segment, preserveCurve) - def _removeSegment(self, segment, preserveCurve, **kwargs): + def _removeSegment(self, segment: int, preserveCurve: bool, **kwargs: Any) -> None: """ segment will be a valid segment index. preserveCurve will be a boolean. @@ -634,7 +1075,7 @@ def _removeSegment(self, segment, preserveCurve, **kwargs): for point in segment.points: self.removePoint(point, preserveCurve) - def setStartSegment(self, segment): + def setStartSegment(self, segment: Union[int, BaseSegment]) -> None: """ Set the first segment on the contour. segment can be a segment object or an index. @@ -656,7 +1097,7 @@ def setStartSegment(self, segment): ) self._setStartSegment(segmentIndex) - def _setStartSegment(self, segmentIndex, **kwargs): + def _setStartSegment(self, segmentIndex: int, **kwargs: Any) -> None: """ Subclasses may override this method. """ @@ -673,9 +1114,9 @@ def _setStartSegment(self, segmentIndex, **kwargs): # bPoints # ------- - bPoints = dynamicProperty("bPoints") + bPoints: dynamicProperty = dynamicProperty("bPoints") - def _get_bPoints(self): + def _get_bPoints(self) -> Tuple[BaseBPoint, ...]: bPoints = [] for point in self.points: if point.type not in ("move", "line", "curve"): @@ -686,9 +1127,12 @@ def _get_bPoints(self): bPoints.append(bPoint) return tuple(bPoints) - def appendBPoint( - self, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None - ): + def appendBPoint(self, + type: Optional[str] = None, + anchor: Optional[PairCollectionType[IntFloatType]] = None, + bcpIn: Optional[PairCollectionType[IntFloatType]] = None, + bcpOut: Optional[PairCollectionType[IntFloatType]] = None, + bPoint: Optional[BaseBPoint] = None) -> None: """ Append a bPoint to the contour. """ @@ -711,15 +1155,24 @@ def appendBPoint( bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._appendBPoint(type, anchor, bcpIn=bcpIn, bcpOut=bcpOut) - def _appendBPoint(self, type, anchor, bcpIn=None, bcpOut=None, **kwargs): + def _appendBPoint(self, + type: Optional[str], + anchor: PairCollectionType[IntFloatType], + bcpIn: Optional[PairCollectionType[IntFloatType]], + bcpOut: Optional[PairCollectionType[IntFloatType]], + **kwargs: Any) -> None: """ Subclasses may override this method. """ self.insertBPoint(len(self.bPoints), type, anchor, bcpIn=bcpIn, bcpOut=bcpOut) - def insertBPoint( - self, index, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None - ): + def insertBPoint(self, + index: int, + type: Optional[str] = None, + anchor: Optional[PairCollectionType[IntFloatType]] = None, + bcpIn: Optional[PairCollectionType[IntFloatType]] = None, + bcpOut: Optional[PairCollectionType[IntFloatType]] = None, + bPoint: Optional[BaseBPoint] = None) -> None: """ Insert a bPoint at index in the contour. """ @@ -745,7 +1198,13 @@ def insertBPoint( index=index, type=type, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut ) - def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): + def _insertBPoint(self, + index: int, + type: str, + anchor: PairCollectionType[IntFloatType], + bcpIn: PairCollectionType[IntFloatType], + bcpOut: PairCollectionType[IntFloatType], + **kwargs: Any) -> None: """ Subclasses may override this method. """ @@ -764,7 +1223,7 @@ def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): bPoint.bcpOut = bcpOut bPoint.type = type - def removeBPoint(self, bPoint): + def removeBPoint(self, bPoint: Union[int, BaseBPoint]) -> None: """ Remove the bpoint from the contour. bpoint can be a point object or an index. @@ -776,7 +1235,7 @@ def removeBPoint(self, bPoint): raise ValueError(f"No bPoint located at index {bPoint}.") self._removeBPoint(bPoint) - def _removeBPoint(self, index, **kwargs): + def _removeBPoint(self, index: int, **kwargs: Any) -> None: """ index will be a valid index. @@ -802,22 +1261,22 @@ def _removeBPoint(self, index, **kwargs): # Points # ------ - def _setContourInPoint(self, point): + def _setContourInPoint(self, point: BasePoint) -> None: if point.contour is None: point.contour = self - points = dynamicProperty("points") + points: dynamicProperty = dynamicProperty("points") - def _get_points(self): + def _get_points(self) -> Tuple[BasePoint, ...]: """ Subclasses may override this method. """ return tuple(self._getitem__points(i) for i in range(self._len__points())) - def _len__points(self): + def _len__points(self) -> int: return self._lenPoints() - def _lenPoints(self, **kwargs): + def _lenPoints(self, **kwargs: Any) -> int: """ This must return an integer indicating the number of points in the contour. @@ -826,7 +1285,7 @@ def _lenPoints(self, **kwargs): """ self.raiseNotImplementedError() - def _getitem__points(self, index): + def _getitem__points(self, index: int) -> BasePoint: index = normalizers.normalizeIndex(index) if index >= self._len__points(): raise ValueError(f"No point located at index {index}.") @@ -834,7 +1293,7 @@ def _getitem__points(self, index): self._setContourInPoint(point) return point - def _getPoint(self, index, **kwargs): + def _getPoint(self, index: int, **kwargs: Any) -> BasePoint: """ This must return a wrapped point. @@ -844,21 +1303,19 @@ def _getPoint(self, index, **kwargs): """ self.raiseNotImplementedError() - def _getPointIndex(self, point): + def _getPointIndex(self, point: BasePoint) -> int: for i, other in enumerate(self.points): if point == other: return i raise FontPartsError("The point could not be found.") - def appendPoint( - self, - position=None, - type="line", - smooth=False, - name=None, - identifier=None, - point=None, - ): + def appendPoint(self, + position: Optional[PairCollectionType[IntFloatType]] = None, + type: str = "line", + smooth: bool = False, + name: Optional[str] = None, + identifier: Optional[str] = None, + point: Optional[BasePoint] = None) -> None: """ Append a point to the contour. """ @@ -880,16 +1337,14 @@ def appendPoint( identifier=identifier, ) - def insertPoint( - self, - index, - position=None, - type="line", - smooth=False, - name=None, - identifier=None, - point=None, - ): + def insertPoint(self, + index: int, + position: Optional[PairCollectionType[IntFloatType]] = None, + type: str = "line", + smooth: bool = False, + name: Optional[str] = None, + identifier: Optional[str] = None, + point: Optional[BasePoint] = None) -> None: """ Insert a point into the contour. """ @@ -919,16 +1374,14 @@ def insertPoint( identifier=identifier, ) - def _insertPoint( - self, - index, - position, - type="line", - smooth=False, - name=None, - identifier=None, - **kwargs, - ): + def _insertPoint(self, + index: int, + position: PairCollectionType[IntFloatType], + type: str = "line", + smooth: bool = False, + name: Optional[str] = None, + identifier: Optional[str] = None, + **kwargs: Any) -> None: """ position will be a valid position (x, y). type will be a valid type. @@ -941,7 +1394,9 @@ def _insertPoint( """ self.raiseNotImplementedError() - def removePoint(self, point, preserveCurve=False): + def removePoint(self, + point: Union[int, BasePoint], + preserveCurve: bool = False) -> None: """ Remove the point from the contour. point can be a point object or an index. @@ -957,7 +1412,10 @@ def removePoint(self, point, preserveCurve=False): preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removePoint(point, preserveCurve) - def _removePoint(self, index, preserveCurve, **kwargs): + def _removePoint(self, + index: int, + preserveCurve: bool, + **kwargs: Any) -> None: """ index will be a valid index. preserveCurve will be a boolean. @@ -965,7 +1423,7 @@ def _removePoint(self, index, preserveCurve, **kwargs): """ self.raiseNotImplementedError() - def setStartPoint(self, point): + def setStartPoint(self, point: Union[int, Any]) -> None: """ Set the first point on the contour. point can be a point object or an index. @@ -985,7 +1443,7 @@ def setStartPoint(self, point): ) self._setStartPoint(pointIndex) - def _setStartPoint(self, pointIndex, **kwargs): + def _setStartPoint(self, pointIndex: int, **kwargs: Any) -> None: """ Subclasses may override this method. """ @@ -1010,7 +1468,7 @@ def _setStartPoint(self, pointIndex, **kwargs): # segments - selectedSegments = dynamicProperty( + selectedSegments: dynamicProperty = dynamicProperty( "base_selectedSegments", """ A list of segments selected in the contour. @@ -1030,20 +1488,20 @@ def _setStartPoint(self, pointIndex, **kwargs): """, ) - def _get_base_selectedSegments(self): + def _get_base_selectedSegments(self) -> Tuple[BaseSegment, ...]: selected = tuple( normalizers.normalizeSegment(segment) for segment in self._get_selectedSegments() ) return selected - def _get_selectedSegments(self): + def _get_selectedSegments(self) -> Tuple[BaseSegment, ...]: """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.segments) - def _set_base_selectedSegments(self, value): + def _set_base_selectedSegments(self, value: Tuple[BaseSegment, ...]) -> None: normalized = [] for i in value: if isinstance(i, int): @@ -1053,7 +1511,7 @@ def _set_base_selectedSegments(self, value): normalized.append(i) self._set_selectedSegments(normalized) - def _set_selectedSegments(self, value): + def _set_selectedSegments(self, value: CollectionType[BaseSegment]) -> None: """ Subclasses may override this method. """ @@ -1061,7 +1519,7 @@ def _set_selectedSegments(self, value): # points - selectedPoints = dynamicProperty( + selectedPoints: dynamicProperty = dynamicProperty( "base_selectedPoints", """ A list of points selected in the contour. @@ -1081,19 +1539,19 @@ def _set_selectedSegments(self, value): """, ) - def _get_base_selectedPoints(self): + def _get_base_selectedPoints(self) -> Tuple[BasePoint, ...]: selected = tuple( normalizers.normalizePoint(point) for point in self._get_selectedPoints() ) return selected - def _get_selectedPoints(self): + def _get_selectedPoints(self) -> Tuple[BasePoint, ...]: """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.points) - def _set_base_selectedPoints(self, value): + def _set_base_selectedPoints(self, value: CollectionType[BasePoint]) -> None: normalized = [] for i in value: if isinstance(i, int): @@ -1103,7 +1561,7 @@ def _set_base_selectedPoints(self, value): normalized.append(i) self._set_selectedPoints(normalized) - def _set_selectedPoints(self, value): + def _set_selectedPoints(self, value: CollectionType[BasePoint]) -> None: """ Subclasses may override this method. """ @@ -1111,7 +1569,7 @@ def _set_selectedPoints(self, value): # bPoints - selectedBPoints = dynamicProperty( + selectedBPoints: dynamicProperty = dynamicProperty( "base_selectedBPoints", """ A list of bPoints selected in the contour. @@ -1131,20 +1589,20 @@ def _set_selectedPoints(self, value): """, ) - def _get_base_selectedBPoints(self): + def _get_base_selectedBPoints(self) -> Tuple[BaseBPoint, ...]: selected = tuple( normalizers.normalizeBPoint(bPoint) for bPoint in self._get_selectedBPoints() ) return selected - def _get_selectedBPoints(self): + def _get_selectedBPoints(self) -> Tuple[BaseBPoint, ...]: """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.bPoints) - def _set_base_selectedBPoints(self, value): + def _set_base_selectedBPoints(self, value: CollectionType[BaseBPoint]) -> None: normalized = [] for i in value: if isinstance(i, int): @@ -1154,7 +1612,7 @@ def _set_base_selectedBPoints(self, value): normalized.append(i) self._set_selectedBPoints(normalized) - def _set_selectedBPoints(self, value): + def _set_selectedBPoints(self, value: CollectionType[BaseBPoint]) -> None: """ Subclasses may override this method. """ From 28991ccbdc89d6de278929982f1bbf8210794859 Mon Sep 17 00:00:00 2001 From: Knut Nergaard Date: Wed, 20 Nov 2024 14:24:58 +0100 Subject: [PATCH 2/8] Change `test_segments_empty` to accomodate change in `BaseContour._get_segments`. --- Lib/fontParts/test/test_contour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontParts/test/test_contour.py b/Lib/fontParts/test/test_contour.py index ea4b03ef..e2a2cc7d 100644 --- a/Lib/fontParts/test/test_contour.py +++ b/Lib/fontParts/test/test_contour.py @@ -491,7 +491,7 @@ def test_segments_offcurves_middle(self): def test_segments_empty(self): contour, _ = self.objectGenerator("contour") segments = contour.segments - self.assertEqual(segments, []) + self.assertEqual(segments, ()) def test_segment_insert_open(self): # at index 0 From 0f83f4290de457d4716d38a9b6fd8c5a625015a2 Mon Sep 17 00:00:00 2001 From: Knut Nergaard Date: Wed, 20 Nov 2024 20:53:16 +0100 Subject: [PATCH 3/8] Add support for pathlib.Path to `normalizeFilePath`. --- Lib/fontParts/base/normalizers.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Lib/fontParts/base/normalizers.py b/Lib/fontParts/base/normalizers.py index f0497390..398efad3 100644 --- a/Lib/fontParts/base/normalizers.py +++ b/Lib/fontParts/base/normalizers.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Optional, Tuple, Type, Union from collections import Counter from fontTools.misc.fixedTools import otRound +from pathlib import Path from fontParts.base.annotations import ( T, @@ -1138,17 +1139,23 @@ def normalizeGlyphNote(value: str) -> str: # File Path -def normalizeFilePath(value: str) -> str: +def normalizeFilePath(value: Union[str, Path]) -> str: """Normalize a file path. - :param value: The file path to normalize as a :class:`str`. + Relative paths are resolved automatically. + + :param value: The file path to normalize as a :class:`str` or :class:`pathlib.Path`. :return: A :class:`str` representing the noramlized file path. :raises TypeError if `value` is not a :class:`str`. """ - if not isinstance(value, str): - raise TypeError(f"File paths must be strings, not {type(value).__name__}.") - return value + if not isinstance(value, (str, Path)): + raise TypeError( + f"File paths must be strings or Path, not {type(value).__name__}." + ) + if isinstance(value, Path) or value.startswith('.'): + return str(Path(value).resolve()) + return str(value) # Interpolation From 284e79f5831a7e5560720dcad71252bcad887984 Mon Sep 17 00:00:00 2001 From: knutnergaard Date: Wed, 20 Nov 2024 19:54:08 +0000 Subject: [PATCH 4/8] Format fixes by ruff --- Lib/fontParts/base/normalizers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontParts/base/normalizers.py b/Lib/fontParts/base/normalizers.py index 398efad3..c4635a65 100644 --- a/Lib/fontParts/base/normalizers.py +++ b/Lib/fontParts/base/normalizers.py @@ -1153,7 +1153,7 @@ def normalizeFilePath(value: Union[str, Path]) -> str: raise TypeError( f"File paths must be strings or Path, not {type(value).__name__}." ) - if isinstance(value, Path) or value.startswith('.'): + if isinstance(value, Path) or value.startswith("."): return str(Path(value).resolve()) return str(value) From 629d60d3d2eaf9e77c8d0e3a35c73559562aa970 Mon Sep 17 00:00:00 2001 From: Knut Nergaard Date: Wed, 20 Nov 2024 21:05:30 +0100 Subject: [PATCH 5/8] reversed `contour.py` edits --- Lib/fontParts/base/contour.py | 884 ++++++++-------------------------- 1 file changed, 213 insertions(+), 671 deletions(-) diff --git a/Lib/fontParts/base/contour.py b/Lib/fontParts/base/contour.py index 33e6b5ad..291f41fc 100644 --- a/Lib/fontParts/base/contour.py +++ b/Lib/fontParts/base/contour.py @@ -1,6 +1,3 @@ -from __future__ import annotations -from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Union - from fontParts.base.errors import FontPartsError from fontParts.base.base import ( BaseObject, @@ -13,26 +10,8 @@ ) from fontParts.base import normalizers from fontParts.base.compatibility import ContourCompatibilityReporter +from fontParts.base.bPoint import absoluteBCPIn, absoluteBCPOut from fontParts.base.deprecated import DeprecatedContour, RemovedContour -from fontParts.base.annotations import ( - QuadrupleType, - PairCollectionType, - CollectionType, - SextupleCollectionType, - IntFloatType, - PenType, - PointPenType, -) - -if TYPE_CHECKING: - from fontParts.base.point import BasePoint - from fontParts.base.bPoint import BaseBPoint - from fontParts.base.segment import BaseSegment - from fontParts.base.glyph import BaseGlyph - from fontParts.base.layer import BaseLayer - from fontParts.base.font import BaseFont - -PointCollectionType = CollectionType[PairCollectionType[IntFloatType]] class BaseContour( @@ -44,18 +23,10 @@ class BaseContour( DeprecatedContour, RemovedContour, ): - """Represent the basis for a contour object. - - :cvar segmentClass: A class representing contour segments. This will - usually be a :class:`BaseSegment` subclass. - :cvar bPointClass: A class representing contour bPoints. This will - usually be a :class:`BaseBPoint` subclass. - - """ segmentClass = None bPointClass = None - def _reprContents(self) -> List[str]: + def _reprContents(self): contents = [] if self.identifier is not None: contents.append(f"identifier='{self.identifier!r}'") @@ -64,23 +35,7 @@ def _reprContents(self) -> List[str]: contents += self.glyph._reprContents() return contents - def copyData(self, source: BaseContour) -> None: - """Copy data from another contour instance. - - This will copy the contents of the following attributes from `source` - into the current contour instance: - - - :attr:`BaseContour.points` - - :attr:`BaseContour.bPoints` - - :param source: The source :class:`BaseContour` instance from which - to copy data. - - Example:: - - >>> contour.copyData(sourceContour) - - """ + def copyData(self, source): super(BaseContour, self).copyData(source) for sourcePoint in source.points: self.appendPoint((0, 0)) @@ -95,30 +50,14 @@ def copyData(self, source: BaseContour) -> None: _glyph = None - glyph: dynamicProperty = dynamicProperty( - "glyph", - """Get or set the contour's parent glyph object. - - The value must be a :class:`BaseGlyph` instance or :obj:`None`. - - :return: The :class:`BaseGlyph` instance containing the contour - or :obj:`None`. - :raises AssertionError: If attempting to set the glyph when it - has already been set. - - Example:: + glyph = dynamicProperty("glyph", "The contour's parent :class:`BaseGlyph`.") - >>> glyph = contour.glyph - - """, - ) - - def _get_glyph(self) -> Optional[BaseGlyph]: + def _get_glyph(self): if self._glyph is None: return None return self._glyph() - def _set_glyph(self, glyph: Optional[BaseGlyph]) -> None: + def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for contour already set") if glyph is not None: @@ -127,45 +66,18 @@ def _set_glyph(self, glyph: Optional[BaseGlyph]) -> None: # Font - font: dynamicProperty = dynamicProperty( - "font", - """Get the contour's parent font object. - - This property is read-only. - - :return: The :class:`BaseFont` instance containing the contour - or :obj:`None`. - - Example:: - - >>> font = contour.font + font = dynamicProperty("font", "The contour's parent font.") - """,) - - def _get_font(self) -> Optional[BaseFont]: + def _get_font(self): if self._glyph is None: return None return self.glyph.font # Layer - layer: dynamicProperty = dynamicProperty( - "layer", - """Get the contour's parent layer object. - - This property is read-only. - - :return: The :class:`BaseLayer` instance containing the contour - or :obj:`None`. - - Example:: + layer = dynamicProperty("layer", "The contour's parent layer.") - >>> layer = contour.layer - - """, - ) - - def _get_layer(self) -> Optional[BaseLayer]: + def _get_layer(self): if self._glyph is None: return None return self.glyph.layer @@ -176,26 +88,20 @@ def _get_layer(self) -> Optional[BaseLayer]: # index - index: dynamicProperty = dynamicProperty( + index = dynamicProperty( "base_index", - """Get or set the index of the contour. - - The value must be an :class:`int`. - - :return: An :class:`int` representing the contour's index within an - ordered list of the parent glyph's contours, or :obj:`None` if the - contour does not belong to a glyph. - :raises FontPartsError: If the contour does not belong to a glyph. - - Example:: + """ + The index of the contour within the parent glyph's contours. >>> contour.index - 0 + 1 + >>> contour.index = 0 + The value will always be a :ref:`type-int`. """, ) - def _get_base_index(self) -> Optional[int]: + def _get_base_index(self): glyph = self.glyph if glyph is None: return None @@ -203,7 +109,7 @@ def _get_base_index(self) -> Optional[int]: value = normalizers.normalizeIndex(value) return value - def _set_base_index(self, value: int) -> None: + def _set_base_index(self, value): glyph = self.glyph if glyph is None: raise FontPartsError("The contour does not belong to a glyph.") @@ -215,81 +121,39 @@ def _set_base_index(self, value: int) -> None: value = contourCount self._set_index(value) - def _get_index(self) -> Optional[int]: - """Get the index of the native contour. - - This is the environment implementation of the :attr:`BaseContour.index` - property getter. - - :return: An :class:`int` representing the contour's index within an - ordered list of the parent glyph's contours, or :obj:`None` if the - contour does not belong to a glyph. The value will be - normalized with :func:`normalizers.normalizeIndex`. - - .. note:: - - Subclasses may override this method. - + def _get_index(self): + """ + Subclasses may override this method. """ glyph = self.glyph return glyph.contours.index(self) - def _set_index(self, value: int) -> None: - """Set the index of the contour. - - This is the environment implementation of the :attr:`BaseContour.index` - property setter. - - :param value: The index to set as an :class:`int`. The value will have - been normalized with :func:`normalizers.normalizeIndex`. - :raises NotImplementedError: If the method has not been overridden by a - subclass. - - .. important:: - - Subclasses must override this method. - + def _set_index(self, value): + """ + Subclasses must override this method. """ self.raiseNotImplementedError() # identifier - def getIdentifierForPoint(self, point: BasePoint) -> str: - """Generate and assign a unique identifier to the given point. - - If `point` already has an identifier, the existing identifier is returned. - Otherwise, a new unique identifier is created and assigned to `point`. - - :param point: The :class:`BasePoint` instance to which the identifier - should be assigned. - :return: A :class:`str` representing the newly assigned identifier. - - Example:: + def getIdentifierForPoint(self, point): + """ + Create a unique identifier for and assign it to ``point``. + If the point already has an identifier, the existing + identifier will be returned. >>> contour.getIdentifierForPoint(point) 'ILHGJlygfds' + ``point`` must be a :class:`BasePoint`. The returned value + will be a :ref:`type-identifier`. """ point = normalizers.normalizePoint(point) return self._getIdentifierforPoint(point) - def _getIdentifierForPoint(self, point: BasePoint) -> str: - """Generate and assign a unique identifier to the given native point. - - This is the environment implementation - of :meth:`BaseContour.getIdentifierForPoint`. - - :param point: The :class:`BasePoint` subclass instance to which the - identifier should be assigned. The value will have been normalized - with :func:`normalizers.normalizePoint`. - :return: A :class:`str` representing the newly assigned identifier. - :raises NotImplementedError: If the method has not been overridden by a - subclass. - - .. important:: - - Subclasses must override this method. - + def _getIdentifierforPoint(self, point): + """ + Subclasses must override this method. """ self.raiseNotImplementedError() @@ -297,67 +161,34 @@ def _getIdentifierForPoint(self, point: BasePoint) -> str: # Pens # ---- - def draw(self, pen: PenType) -> None: - """Draw the contour's outline data to the given pen. - - :param pen: The :class:`fontTools.pens.basePen.AbstractPen` to which the - outline data should be drawn. - - Example:: + def draw(self, pen): + """ + Draw the contour's outline data to the given :ref:`type-pen`. >>> contour.draw(pen) - """ self._draw(pen) - def _draw(self, pen: PenType, **kwargs: Any) -> None: - r"""Draw the native contour's outline data to the given pen. - - This is the environment implementation of :meth:`BaseContour.draw`. - - :param pen: The :class:`fontTools.pens.basePen.AbstractPen` to which the - outline data should be drawn. - :param \**kwargs: Additional keyword arguments. - - .. note:: - - Subclasses may override this method. - + def _draw(self, pen, **kwargs): + """ + Subclasses may override this method. """ from fontTools.ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter) - adapter = PointToSegmentPen(pen) - self.drawPoints(adapter) - - def drawPoints(self, pen: PointPenType) -> None: - """Draw the contour's outline data to the given point pen. - - :param pen: The :class:`fontTools.pens.basePen.AbstractPointPen` to - which the outline data should be drawn. - - Example:: + def drawPoints(self, pen): + """ + Draw the contour's outline data to the given :ref:`type-point-pen`. >>> contour.drawPoints(pointPen) - """ self._drawPoints(pen) - def _drawPoints(self, pen: PointPenType, **kwargs: Any) -> None: - r"""Draw the native contour's outline data to the given point pen. - - This is the environment implementation of :meth:`BaseContour.drawPoints`. - - :param pen: The :class:`fontTools.pens.basePen.AbstractPointPen` to - which the outline data should be drawn. - :param \**kwargs: Additional keyword arguments. - - .. note:: - - Subclasses may override this method. - + def _drawPoints(self, pen, **kwargs): + """ + Subclasses may override this method. """ # The try: ... except TypeError: ... # handles backwards compatibility with @@ -392,55 +223,32 @@ def _drawPoints(self, pen: PointPenType, **kwargs: Any) -> None: # Data normalization # ------------------ - def autoStartSegment(self) -> None: - """Automatically calculate and set the contour's first segment. + def autoStartSegment(self): + """ + Automatically calculate and set the first segment + in this contour. The behavior of this may vary accross environments. - - Example:: - - >>> contour.autoStartSegment() - """ self._autoStartSegment() - def _autoStartSegment(self, **kwargs: Any) -> None: - r"""Automatically calculate and set the native contour's first segment. - - This is the environment implementation of :meth:`BaseContour.autoStartSegment`. - - :param \**kwargs: Additional keyword arguments. - :raises NotImplementedError: If the method has not been overridden by a - subclass. - - .. note:: - - Subclasses may override this method. + def _autoStartSegment(self, **kwargs): + """ + Subclasses may override this method. + XXX port this from robofab """ self.raiseNotImplementedError() - def round(self) -> None: - """Round all point coordinates in the contour to the neares integer. - - Example:: - - >>> contour.round() - + def round(self): + """ + Round coordinates in all points to integers. """ self._round() - def _round(self, **kwargs: Any) -> None: - r"""Round all point coordinates in the native contour to the neares integer. - - This is the environment implementation of :meth:`BaseContour.round`. - - :param \**kwargs: Additional keyword arguments. - - .. note:: - - Subclasses may override this method. - + def _round(self, **kwargs): + """ + Subclasses may override this method. """ for point in self.points: point.round() @@ -449,21 +257,9 @@ def _round(self, **kwargs: Any) -> None: # Transformation # -------------- - def _transformBy(self, - matrix: SextupleCollectionType[IntFloatType], - **kwargs: Any) -> None: - r"""Transform the contour according to the given matrix. - - This is the environment implementation of :meth:`BaseContour.transformBy`. - - :param matrix: The :ref:`type-transformation` to apply. The value will - be normalized with :func:`normalizers.normalizeTransformationMatrix`. - :param \**kwargs: Additional keyword arguments. - - .. note:: - - Subclasses may override this method. - + def _transformBy(self, matrix, **kwargs): + """ + Subclasses may override this method. """ for point in self.points: point.transformBy(matrix) @@ -474,16 +270,9 @@ def _transformBy(self, compatibilityReporterClass = ContourCompatibilityReporter - def isCompatible(self, other: BaseContour) -> tuple[bool, str]: - """Evaluate interpolation compatibility with another contour. - - :param other: The other :class:`BaseContour` instance to check - compatibility with. - :return: A :class:`tuple` where the first element is a :class:`bool` - indicating compatibility, and the second element is a :class:`str` - of compatibility notes. - - Example:: + def isCompatible(self, other): + """ + Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherContour) >>> compatible @@ -493,24 +282,18 @@ def isCompatible(self, other: BaseContour) -> tuple[bool, str]: [Fatal] Contour: [0] contains 4 segments | [0] contains 3 segments [Fatal] Contour: [0] is closed | [0] is open + This will return a ``bool`` indicating if the contour is + compatible for interpolation with **other** and a + :ref:`type-string` of compatibility notes. """ return super(BaseContour, self).isCompatible(other, BaseContour) - def _isCompatible(self, - other: BaseContour, - reporter: ContourCompatibilityReporter) -> None: - """Evaluate interpolation compatibility with another native contour. - - This is the environment implementation of :meth:`BaseContour.isCompatible`. - - :param other: The other :class:`BaseContour` instance to check - compatibility with. - :param reporter: An object used to report compatibility issues. - - .. note:: - - Subclasses may override this method. + def _isCompatible(self, other, reporter): + """ + This is the environment implementation of + :meth:`BaseContour.isCompatible`. + Subclasses may override this method. """ contour1 = self contour2 = other @@ -535,47 +318,21 @@ def _isCompatible(self, if segmentCompatibility.warning: reporter.warning = True reporter.segments.append(segmentCompatibility) + # ---- # Open # ---- - open: dynamicProperty = dynamicProperty( - "base_open", - """Determine whether the contour is open. - - This property is read-only. - - :return: :obj:`True` if the contour is open, otherwise :obj:`False`. - - Example:: - - >>> contour.open - True - - """ - ) + open = dynamicProperty("base_open", "Boolean indicating if the contour is open.") - def _get_base_open(self) -> bool: + def _get_base_open(self): value = self._get_open() value = normalizers.normalizeBoolean(value) return value - def _get_open(self) -> bool: - """Determine whether the native contour is open. - - This is the environment implementation of the :attr:`BaseContour.open` - property getter. - - :return: :obj:`True` if the contour is open, otherwise :obj:`False`. - The value will have been normalized - with :func:`normalizers.normalizeBoolean`. - :raises NotImplementedError: If the method has not been overridden by a - subclass. - - .. important:: - - Subclasses must override this method. - + def _get_open(self): + """ + Subclasses must override this method. """ self.raiseNotImplementedError() @@ -583,92 +340,42 @@ def _get_open(self) -> bool: # Direction # --------- - clockwise: dynamicProperty = dynamicProperty( + clockwise = dynamicProperty( "base_clockwise", - """Specify or determine whether the contour's winding direction is clockwise. - - The value must be a :class:`bool` indicating the contour's winding - direction. - - :return: :obj:`True` if the contour's winding direction is clockwise, - otherwise :obj:`False`. - - """, + ("Boolean indicating if the contour's " "winding direction is clockwise."), ) - def _get_base_clockwise(self) -> bool: + def _get_base_clockwise(self): value = self._get_clockwise() value = normalizers.normalizeBoolean(value) return value - def _set_base_clockwise(self, value: bool) -> None: + def _set_base_clockwise(self, value): value = normalizers.normalizeBoolean(value) self._set_clockwise(value) - def _get_clockwise(self) -> bool: - """Determine whether the native contour's winding direction is clockwise. - - This is the environment implementation of the :attr:`BaseContour.clockwise` - property getter. - - :return: :obj:`True` if the contour's winding direction is clockwise, - otherwise :obj:`False`. The value will have been normalized with - :func:`normalizers.normalizeBoolean`. - :raises NotImplementedError: If the method has not been overridden by a - subclass. - - .. important:: - - Subclasses must override this method. - + def _get_clockwise(self): + """ + Subclasses must override this method. """ self.raiseNotImplementedError() - def _set_clockwise(self, value: bool) -> None: - """Specify whether the native contour's winding direction is clockwise. - - This is the environment implementation of the :attr:`BaseContour.clockwise` - property setter. - - :param value: The winding direction to indicate as a :class:`bool`. - The value will have been normalized - with :func:`normalizers.normalizeBoolean`. - - .. note:: - - Subclasses may override this method. - + def _set_clockwise(self, value): + """ + Subclasses may override this method. """ if self.clockwise != value: self.reverse() - def reverse(self) -> None: - """Reverse the direction of the contour. - - Example:: - - >>> contour.clockwise - False - >>> contour.reverse() - >>> contour.clockwise - True - + def reverse(self): + """ + Reverse the direction of the contour. """ self._reverseContour() - def _reverse(self, **kwargs) -> None: - r"""Reverse the direction of the contour. - - This is the environment implementation of :meth:`BaseContour.reverse`. - - :param \**kwargs: Additional keyword arguments. - :raises NotImplementedError: If the method has not been overridden by a - subclass. - - .. note:: - - Subclasses may override this method. - + def _reverse(self, **kwargs): + """ + Subclasses may override this method. """ self.raiseNotImplementedError() @@ -676,36 +383,21 @@ def _reverse(self, **kwargs) -> None: # Point and Contour Inside # ------------------------ - def pointInside(self, point: PairCollectionType[IntFloatType]) -> bool: - """Check if `point` is within the filled area of the contour. - - :param point: The point to check as a :ref:`type-coordinate`. - :return: :obj:`True` if `point` is inside the filled area of the - contour, :obj:`False` otherwise. - - Example:: + def pointInside(self, point): + """ + Determine if ``point`` is in the black or white of the contour. >>> contour.pointInside((40, 65)) True + ``point`` must be a :ref:`type-coordinate`. """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point) - def _pointInside(self, point: PairCollectionType[IntFloatType]) -> bool: - """Check if `point` is within the filled area of the native contour. - - This is the environment implementation of :meth:`BaseContour.pointInside`. - - :param point: The point to check as a :ref:`type-coordinate`. The value - will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. - :return: :obj:`True` if `point` is inside the filled area of the - contour, :obj:`False` otherwise. - - .. note:: - - Subclasses may override this method. - + def _pointInside(self, point): + """ + Subclasses may override this method. """ from fontTools.pens.pointInsidePen import PointInsidePen @@ -713,12 +405,9 @@ def _pointInside(self, point: PairCollectionType[IntFloatType]) -> bool: self.draw(pen) return pen.getResult() - def contourInside(self, otherContour: BaseContour) -> bool: - """Check if `otherContour` is within the current contour's filled area. - - :param point: The :class:`BaseContour` instance to check. - :return: :obj:`True` if `otherContour` is inside the filled area of the - current contour instance, :obj:`False` otherwise. + def contourInside(self, otherContour): + """ + Determine if ``otherContour`` is in the black or white of this contour. >>> contour.contourInside(otherContour) True @@ -728,22 +417,9 @@ def contourInside(self, otherContour: BaseContour) -> bool: otherContour = normalizers.normalizeContour(otherContour) return self._contourInside(otherContour) - def _contourInside(self, otherContour: BaseContour) -> bool: - """Check if `otherContour` is within the current native contour's filled area. - - This is the environment implementation of :meth:`BaseContour.contourInside`. - - :param point: The :class:`BaseContour` instance to check. The value will have - been normalized with :func:`normalizers.normalizeContour`. - :return: :obj:`True` if `otherContour` is inside the filled area of the - current contour instance, :obj:`False` otherwise. - :raises NotImplementedError: If the method has not been overridden by a - subclass. - - .. note:: - - Subclasses may override this method. - + def _contourInside(self, otherContour): + """ + Subclasses may override this method. """ self.raiseNotImplementedError() @@ -751,47 +427,19 @@ def _contourInside(self, otherContour: BaseContour) -> bool: # Bounds and Area # --------------- - bounds: dynamicProperty = dynamicProperty( - "bounds", - """Get the bounds of the contour. - - This property is read-only. - - :return: A :class:`tuple` of four :class:`int` or :class:`float` values - in the form ``(x minimum, y minimum, x maximum, y maximum)`` - representing the bounds of the contour, or :obj:`None` if the contour - is open. - - Example:: - - >>> contour.bounds - (10, 30, 765, 643) - - - """ + bounds = dynamicProperty( + "bounds", ("The bounds of the contour: " "(xMin, yMin, xMax, yMax) or None.") ) - def _get_base_bounds(self) -> Optional[QuadrupleType[float]]: + def _get_base_bounds(self): value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value - def _get_bounds(self) -> Optional[QuadrupleType[float]]: - """Get the bounds of the contour. - - This is the environment implementation of the :attr:`BaseContour.bounds` - property getter. - - :return: A :class:`tuple` of four :class:`int` or :class:`float` values - in the form ``(x minimum, y minimum, x maximum, y maximum)`` - representing the bounds of the contour, or :obj:`None` if the contour - is open. - - .. note:: - - Subclasses may override this method. - + def _get_bounds(self): + """ + Subclasses may override this method. """ from fontTools.pens.boundsPen import BoundsPen @@ -799,42 +447,19 @@ def _get_bounds(self) -> Optional[QuadrupleType[float]]: self.draw(pen) return pen.bounds - area: dynamicProperty = dynamicProperty( - "area", - """Get the area of the contour - - This property is read-only. - - :return: A positive :class:`int` or a :class:` float value representing - the area of the contour, or :obj:`None` if the contour is open. - - Example:: - - >>> contour.area - 583 - - """ + area = dynamicProperty( + "area", ("The area of the contour: " "A positive number or None.") ) - def _get_base_area(self) -> Optional[float]: + def _get_base_area(self): value = self._get_area() if value is not None: value = normalizers.normalizeArea(value) return value - def _get_area(self) -> Optional[float]: - """Get the area of the native contour - - This is the environment implementation of the :attr:`BaseContour.area` - property getter. - - :return: A positive :class:`int` or a :class:` float value representing - the area of the contour, or :obj:`None` if the contour is open. - - .. note:: - - Subclasses may override this method. - + def _get_area(self): + """ + Subclasses may override this method. """ from fontTools.pens.areaPen import AreaPen @@ -851,37 +476,19 @@ def _get_area(self) -> Optional[float]: # other than registering segmentClass. Subclasses may choose to # implement this API independently if desired. - def _setContourInSegment(self, segment: BaseSegment) -> None: + def _setContourInSegment(self, segment): if segment.contour is None: segment.contour = self - segments: dynamicProperty = dynamicProperty( - "segments", - """Get the countour's segments. - - This property is read-only. - - :return: A :class:`tuple` of :class:`BaseSegment` instances. + segments = dynamicProperty("segments") + def _get_segments(self): """ - ) - - def _get_segments(self) -> Tuple[BaseSegment]: - """Get the native countour's segments. - - This is the environment implementation of the :attr:`BaseContour.segments` - property getter. - - :return: A :class:`tuple` of :class:`BaseSegment` subclass instances. - - .. note:: - - Subclasses may override this method. - + Subclasses may override this method. """ - points = self.points + points = list(self.points) if not points: - return () + return [] segments = [[]] lastWasOffCurve = False firstIsMove = points[0].type == "move" @@ -910,29 +517,15 @@ def _get_segments(self) -> Tuple[BaseSegment]: s._setPoints(points) self._setContourInSegment(s) wrapped.append(s) - return tuple(wrapped) - - def __getitem__(self, index: int) -> BaseSegment: - """Get the segment at the specified index. + return wrapped - :param index: The zero-based index of the point to retrieve as - an :class:`int`. - :return: The :class:`BaseSegment` instance located at the specified `index`. - :raises IndexError: If the specified `index` is out of range. - - """ + def __getitem__(self, index): return self.segments[index] - def __iter__(self) -> Iterator[BaseSegment]: - """Return an iterator over the segments in the contour. - - :return: An iterator over the :class:`BaseSegment` instances belonging to - the contour. - - """ + def __iter__(self): return self._iterSegments() - def _iterSegments(self) -> Iterator[BaseSegment]: + def _iterSegments(self): segments = self.segments count = len(segments) index = 0 @@ -941,36 +534,16 @@ def _iterSegments(self) -> Iterator[BaseSegment]: count -= 1 index += 1 - def __len__(self) -> int: - """Return the number of segments in the contour. - - :return: An :class:`int` representing the number of :class:`BaseSegment` - instances belonging to the contour. - - """ + def __len__(self): return self._len__segments() - def _len__segments(self, **kwargs: Any) -> int: - r"""Return the number of segments in the native contour. - - This is the environment implementation of :meth:`BaseContour.__len__`. - - :return: An :class:`int` representing the number of :class:`BaseSegment` - subclass instances belonging to the contour. - :param \**kwargs: Additional keyword arguments. - - .. note:: - - Subclasses may override this method. - + def _len__segments(self, **kwargs): + """ + Subclasses may override this method. """ return len(self.segments) - def appendSegment(self, - type: Optional[str] = None, - points: Optional[PointCollectionType] = None, - smooth: bool = False, - segment: Optional[BaseSegment] = None) -> None: + def appendSegment(self, type=None, points=None, smooth=False, segment=None): """ Append a segment to the contour. """ @@ -989,11 +562,7 @@ def appendSegment(self, smooth = normalizers.normalizeBoolean(smooth) self._appendSegment(type=type, points=points, smooth=smooth) - def _appendSegment(self, - type: Optional[str] = None, - points: Optional[PointCollectionType] = None, - smooth: bool = False, - **kwargs: Any) -> None: + def _appendSegment(self, type=None, points=None, smooth=False, **kwargs): """ Subclasses may override this method. """ @@ -1001,12 +570,7 @@ def _appendSegment(self, len(self), type=type, points=points, smooth=smooth, **kwargs ) - def insertSegment(self, - index: int, - type: Optional[str] = None, - points: Optional[PointCollectionType] = None, - smooth: bool = False, - segment: Optional[BaseSegment] = None) -> None: + def insertSegment(self, index, type=None, points=None, smooth=False, segment=None): """ Insert a segment into the contour. """ @@ -1026,12 +590,9 @@ def insertSegment(self, smooth = normalizers.normalizeBoolean(smooth) self._insertSegment(index=index, type=type, points=points, smooth=smooth) - def _insertSegment(self, - index: int, - type: Optional[str], - points: Optional[PointCollectionType], - smooth: bool, - **kwargs: Any) -> None: + def _insertSegment( + self, index=None, type=None, points=None, smooth=False, **kwargs + ): """ Subclasses may override this method. """ @@ -1047,9 +608,7 @@ def _insertSegment(self, for offCurvePoint in reversed(offCurve): self.insertPoint(ptCount, offCurvePoint, type="offcurve") - def removeSegment(self, - segment: Union[int, BaseSegment], - preserveCurve: bool = False) -> None: + def removeSegment(self, segment, preserveCurve=False): """ Remove segment from the contour. If ``preserveCurve`` is set to ``True`` an attempt @@ -1064,7 +623,7 @@ def removeSegment(self, preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removeSegment(segment, preserveCurve) - def _removeSegment(self, segment: int, preserveCurve: bool, **kwargs: Any) -> None: + def _removeSegment(self, segment, preserveCurve, **kwargs): """ segment will be a valid segment index. preserveCurve will be a boolean. @@ -1075,7 +634,7 @@ def _removeSegment(self, segment: int, preserveCurve: bool, **kwargs: Any) -> No for point in segment.points: self.removePoint(point, preserveCurve) - def setStartSegment(self, segment: Union[int, BaseSegment]) -> None: + def setStartSegment(self, segment): """ Set the first segment on the contour. segment can be a segment object or an index. @@ -1097,7 +656,7 @@ def setStartSegment(self, segment: Union[int, BaseSegment]) -> None: ) self._setStartSegment(segmentIndex) - def _setStartSegment(self, segmentIndex: int, **kwargs: Any) -> None: + def _setStartSegment(self, segmentIndex, **kwargs): """ Subclasses may override this method. """ @@ -1114,9 +673,9 @@ def _setStartSegment(self, segmentIndex: int, **kwargs: Any) -> None: # bPoints # ------- - bPoints: dynamicProperty = dynamicProperty("bPoints") + bPoints = dynamicProperty("bPoints") - def _get_bPoints(self) -> Tuple[BaseBPoint, ...]: + def _get_bPoints(self): bPoints = [] for point in self.points: if point.type not in ("move", "line", "curve"): @@ -1127,12 +686,9 @@ def _get_bPoints(self) -> Tuple[BaseBPoint, ...]: bPoints.append(bPoint) return tuple(bPoints) - def appendBPoint(self, - type: Optional[str] = None, - anchor: Optional[PairCollectionType[IntFloatType]] = None, - bcpIn: Optional[PairCollectionType[IntFloatType]] = None, - bcpOut: Optional[PairCollectionType[IntFloatType]] = None, - bPoint: Optional[BaseBPoint] = None) -> None: + def appendBPoint( + self, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None + ): """ Append a bPoint to the contour. """ @@ -1155,24 +711,15 @@ def appendBPoint(self, bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._appendBPoint(type, anchor, bcpIn=bcpIn, bcpOut=bcpOut) - def _appendBPoint(self, - type: Optional[str], - anchor: PairCollectionType[IntFloatType], - bcpIn: Optional[PairCollectionType[IntFloatType]], - bcpOut: Optional[PairCollectionType[IntFloatType]], - **kwargs: Any) -> None: + def _appendBPoint(self, type, anchor, bcpIn=None, bcpOut=None, **kwargs): """ Subclasses may override this method. """ self.insertBPoint(len(self.bPoints), type, anchor, bcpIn=bcpIn, bcpOut=bcpOut) - def insertBPoint(self, - index: int, - type: Optional[str] = None, - anchor: Optional[PairCollectionType[IntFloatType]] = None, - bcpIn: Optional[PairCollectionType[IntFloatType]] = None, - bcpOut: Optional[PairCollectionType[IntFloatType]] = None, - bPoint: Optional[BaseBPoint] = None) -> None: + def insertBPoint( + self, index, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None + ): """ Insert a bPoint at index in the contour. """ @@ -1198,13 +745,7 @@ def insertBPoint(self, index=index, type=type, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut ) - def _insertBPoint(self, - index: int, - type: str, - anchor: PairCollectionType[IntFloatType], - bcpIn: PairCollectionType[IntFloatType], - bcpOut: PairCollectionType[IntFloatType], - **kwargs: Any) -> None: + def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): """ Subclasses may override this method. """ @@ -1223,7 +764,7 @@ def _insertBPoint(self, bPoint.bcpOut = bcpOut bPoint.type = type - def removeBPoint(self, bPoint: Union[int, BaseBPoint]) -> None: + def removeBPoint(self, bPoint): """ Remove the bpoint from the contour. bpoint can be a point object or an index. @@ -1235,7 +776,7 @@ def removeBPoint(self, bPoint: Union[int, BaseBPoint]) -> None: raise ValueError(f"No bPoint located at index {bPoint}.") self._removeBPoint(bPoint) - def _removeBPoint(self, index: int, **kwargs: Any) -> None: + def _removeBPoint(self, index, **kwargs): """ index will be a valid index. @@ -1261,22 +802,22 @@ def _removeBPoint(self, index: int, **kwargs: Any) -> None: # Points # ------ - def _setContourInPoint(self, point: BasePoint) -> None: + def _setContourInPoint(self, point): if point.contour is None: point.contour = self - points: dynamicProperty = dynamicProperty("points") + points = dynamicProperty("points") - def _get_points(self) -> Tuple[BasePoint, ...]: + def _get_points(self): """ Subclasses may override this method. """ return tuple(self._getitem__points(i) for i in range(self._len__points())) - def _len__points(self) -> int: + def _len__points(self): return self._lenPoints() - def _lenPoints(self, **kwargs: Any) -> int: + def _lenPoints(self, **kwargs): """ This must return an integer indicating the number of points in the contour. @@ -1285,7 +826,7 @@ def _lenPoints(self, **kwargs: Any) -> int: """ self.raiseNotImplementedError() - def _getitem__points(self, index: int) -> BasePoint: + def _getitem__points(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__points(): raise ValueError(f"No point located at index {index}.") @@ -1293,7 +834,7 @@ def _getitem__points(self, index: int) -> BasePoint: self._setContourInPoint(point) return point - def _getPoint(self, index: int, **kwargs: Any) -> BasePoint: + def _getPoint(self, index, **kwargs): """ This must return a wrapped point. @@ -1303,19 +844,21 @@ def _getPoint(self, index: int, **kwargs: Any) -> BasePoint: """ self.raiseNotImplementedError() - def _getPointIndex(self, point: BasePoint) -> int: + def _getPointIndex(self, point): for i, other in enumerate(self.points): if point == other: return i raise FontPartsError("The point could not be found.") - def appendPoint(self, - position: Optional[PairCollectionType[IntFloatType]] = None, - type: str = "line", - smooth: bool = False, - name: Optional[str] = None, - identifier: Optional[str] = None, - point: Optional[BasePoint] = None) -> None: + def appendPoint( + self, + position=None, + type="line", + smooth=False, + name=None, + identifier=None, + point=None, + ): """ Append a point to the contour. """ @@ -1337,14 +880,16 @@ def appendPoint(self, identifier=identifier, ) - def insertPoint(self, - index: int, - position: Optional[PairCollectionType[IntFloatType]] = None, - type: str = "line", - smooth: bool = False, - name: Optional[str] = None, - identifier: Optional[str] = None, - point: Optional[BasePoint] = None) -> None: + def insertPoint( + self, + index, + position=None, + type="line", + smooth=False, + name=None, + identifier=None, + point=None, + ): """ Insert a point into the contour. """ @@ -1374,14 +919,16 @@ def insertPoint(self, identifier=identifier, ) - def _insertPoint(self, - index: int, - position: PairCollectionType[IntFloatType], - type: str = "line", - smooth: bool = False, - name: Optional[str] = None, - identifier: Optional[str] = None, - **kwargs: Any) -> None: + def _insertPoint( + self, + index, + position, + type="line", + smooth=False, + name=None, + identifier=None, + **kwargs, + ): """ position will be a valid position (x, y). type will be a valid type. @@ -1394,9 +941,7 @@ def _insertPoint(self, """ self.raiseNotImplementedError() - def removePoint(self, - point: Union[int, BasePoint], - preserveCurve: bool = False) -> None: + def removePoint(self, point, preserveCurve=False): """ Remove the point from the contour. point can be a point object or an index. @@ -1412,10 +957,7 @@ def removePoint(self, preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removePoint(point, preserveCurve) - def _removePoint(self, - index: int, - preserveCurve: bool, - **kwargs: Any) -> None: + def _removePoint(self, index, preserveCurve, **kwargs): """ index will be a valid index. preserveCurve will be a boolean. @@ -1423,7 +965,7 @@ def _removePoint(self, """ self.raiseNotImplementedError() - def setStartPoint(self, point: Union[int, Any]) -> None: + def setStartPoint(self, point): """ Set the first point on the contour. point can be a point object or an index. @@ -1443,7 +985,7 @@ def setStartPoint(self, point: Union[int, Any]) -> None: ) self._setStartPoint(pointIndex) - def _setStartPoint(self, pointIndex: int, **kwargs: Any) -> None: + def _setStartPoint(self, pointIndex, **kwargs): """ Subclasses may override this method. """ @@ -1468,7 +1010,7 @@ def _setStartPoint(self, pointIndex: int, **kwargs: Any) -> None: # segments - selectedSegments: dynamicProperty = dynamicProperty( + selectedSegments = dynamicProperty( "base_selectedSegments", """ A list of segments selected in the contour. @@ -1488,20 +1030,20 @@ def _setStartPoint(self, pointIndex: int, **kwargs: Any) -> None: """, ) - def _get_base_selectedSegments(self) -> Tuple[BaseSegment, ...]: + def _get_base_selectedSegments(self): selected = tuple( normalizers.normalizeSegment(segment) for segment in self._get_selectedSegments() ) return selected - def _get_selectedSegments(self) -> Tuple[BaseSegment, ...]: + def _get_selectedSegments(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.segments) - def _set_base_selectedSegments(self, value: Tuple[BaseSegment, ...]) -> None: + def _set_base_selectedSegments(self, value): normalized = [] for i in value: if isinstance(i, int): @@ -1511,7 +1053,7 @@ def _set_base_selectedSegments(self, value: Tuple[BaseSegment, ...]) -> None: normalized.append(i) self._set_selectedSegments(normalized) - def _set_selectedSegments(self, value: CollectionType[BaseSegment]) -> None: + def _set_selectedSegments(self, value): """ Subclasses may override this method. """ @@ -1519,7 +1061,7 @@ def _set_selectedSegments(self, value: CollectionType[BaseSegment]) -> None: # points - selectedPoints: dynamicProperty = dynamicProperty( + selectedPoints = dynamicProperty( "base_selectedPoints", """ A list of points selected in the contour. @@ -1539,19 +1081,19 @@ def _set_selectedSegments(self, value: CollectionType[BaseSegment]) -> None: """, ) - def _get_base_selectedPoints(self) -> Tuple[BasePoint, ...]: + def _get_base_selectedPoints(self): selected = tuple( normalizers.normalizePoint(point) for point in self._get_selectedPoints() ) return selected - def _get_selectedPoints(self) -> Tuple[BasePoint, ...]: + def _get_selectedPoints(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.points) - def _set_base_selectedPoints(self, value: CollectionType[BasePoint]) -> None: + def _set_base_selectedPoints(self, value): normalized = [] for i in value: if isinstance(i, int): @@ -1561,7 +1103,7 @@ def _set_base_selectedPoints(self, value: CollectionType[BasePoint]) -> None: normalized.append(i) self._set_selectedPoints(normalized) - def _set_selectedPoints(self, value: CollectionType[BasePoint]) -> None: + def _set_selectedPoints(self, value): """ Subclasses may override this method. """ @@ -1569,7 +1111,7 @@ def _set_selectedPoints(self, value: CollectionType[BasePoint]) -> None: # bPoints - selectedBPoints: dynamicProperty = dynamicProperty( + selectedBPoints = dynamicProperty( "base_selectedBPoints", """ A list of bPoints selected in the contour. @@ -1589,20 +1131,20 @@ def _set_selectedPoints(self, value: CollectionType[BasePoint]) -> None: """, ) - def _get_base_selectedBPoints(self) -> Tuple[BaseBPoint, ...]: + def _get_base_selectedBPoints(self): selected = tuple( normalizers.normalizeBPoint(bPoint) for bPoint in self._get_selectedBPoints() ) return selected - def _get_selectedBPoints(self) -> Tuple[BaseBPoint, ...]: + def _get_selectedBPoints(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.bPoints) - def _set_base_selectedBPoints(self, value: CollectionType[BaseBPoint]) -> None: + def _set_base_selectedBPoints(self, value): normalized = [] for i in value: if isinstance(i, int): @@ -1612,7 +1154,7 @@ def _set_base_selectedBPoints(self, value: CollectionType[BaseBPoint]) -> None: normalized.append(i) self._set_selectedBPoints(normalized) - def _set_selectedBPoints(self, value: CollectionType[BaseBPoint]) -> None: + def _set_selectedBPoints(self, value): """ Subclasses may override this method. """ From cd5e7111928c15c86a7cbc33b52cbda6a1077ac1 Mon Sep 17 00:00:00 2001 From: Knut Nergaard Date: Wed, 20 Nov 2024 21:08:33 +0100 Subject: [PATCH 6/8] Reversed `test_contour.py` edits. --- Lib/fontParts/test/test_contour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontParts/test/test_contour.py b/Lib/fontParts/test/test_contour.py index e2a2cc7d..ea4b03ef 100644 --- a/Lib/fontParts/test/test_contour.py +++ b/Lib/fontParts/test/test_contour.py @@ -491,7 +491,7 @@ def test_segments_offcurves_middle(self): def test_segments_empty(self): contour, _ = self.objectGenerator("contour") segments = contour.segments - self.assertEqual(segments, ()) + self.assertEqual(segments, []) def test_segment_insert_open(self): # at index 0 From 9f6827d8cb447f6c3c49ec725bfbee238f9bb330 Mon Sep 17 00:00:00 2001 From: Knut Nergaard Date: Wed, 20 Nov 2024 21:38:14 +0100 Subject: [PATCH 7/8] Update normalizers.py Add `pathlib.Path` to exception documentation in `normalizeFilePath`. --- Lib/fontParts/base/normalizers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontParts/base/normalizers.py b/Lib/fontParts/base/normalizers.py index c4635a65..4642bd48 100644 --- a/Lib/fontParts/base/normalizers.py +++ b/Lib/fontParts/base/normalizers.py @@ -1146,7 +1146,7 @@ def normalizeFilePath(value: Union[str, Path]) -> str: :param value: The file path to normalize as a :class:`str` or :class:`pathlib.Path`. :return: A :class:`str` representing the noramlized file path. - :raises TypeError if `value` is not a :class:`str`. + :raises TypeError if `value` is not a :class:`str` or :class:`pathlib.Path`. """ if not isinstance(value, (str, Path)): From 3e09e07f6c40a5f236de6fa02a883d1afa4b7abb Mon Sep 17 00:00:00 2001 From: Knut Nergaard Date: Thu, 21 Nov 2024 03:09:36 +0100 Subject: [PATCH 8/8] Update normalizers.py - broadened scope of `Path.resolve` for improved robustness - document implicit exception. - improved error message. - corrected typo in docstring. --- Lib/fontParts/base/normalizers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/fontParts/base/normalizers.py b/Lib/fontParts/base/normalizers.py index 4642bd48..14018bfb 100644 --- a/Lib/fontParts/base/normalizers.py +++ b/Lib/fontParts/base/normalizers.py @@ -1145,17 +1145,16 @@ def normalizeFilePath(value: Union[str, Path]) -> str: Relative paths are resolved automatically. :param value: The file path to normalize as a :class:`str` or :class:`pathlib.Path`. - :return: A :class:`str` representing the noramlized file path. + :return: A :class:`str` representing the normalized file path. :raises TypeError if `value` is not a :class:`str` or :class:`pathlib.Path`. + :raises FileNotFoundError: If the file path cannot be resolved because it does not exist. """ if not isinstance(value, (str, Path)): raise TypeError( - f"File paths must be strings or Path, not {type(value).__name__}." + f"File paths must be strings or Path objects, not {type(value).__name__}." ) - if isinstance(value, Path) or value.startswith("."): - return str(Path(value).resolve()) - return str(value) + return str(Path(value).resolve()) # Interpolation