diff --git a/Lib/fontParts/base/contour.py b/Lib/fontParts/base/contour.py index 291f41fc..7fd45b3c 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, cast, 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,19 @@ 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 +65,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 +96,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:: + + >>> glyph = contour.glyph + + """, + ) - def _get_glyph(self): + 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 +128,46 @@ 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:: + + >>> layer = contour.layer - def _get_layer(self): + """, + ) + + def _get_layer(self) -> Optional[BaseLayer]: if self._glyph is None: return None return self.glyph.layer @@ -88,20 +178,27 @@ 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 - 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,51 +206,95 @@ 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.") - value = normalizers.normalizeIndex(value) + normalizedValue = normalizers.normalizeIndex(value) + if normalizedValue is None: + return contourCount = len(glyph.contours) - if value < 0: - value = -(value % contourCount) - if value >= contourCount: - value = contourCount - self._set_index(value) + if normalizedValue < 0: + normalizedValue = -(normalizedValue % contourCount) + if normalizedValue >= contourCount: + normalizedValue = contourCount + self._set_index(normalizedValue) + + 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): - """ - 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) + 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() @@ -161,34 +302,64 @@ 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`. + 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 +394,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 across environments. + + Example:: + + >>> contour.autoStartSegment() - The behavior of this may vary accross environments. """ 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. + + .. important:: + + Subclasses must 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 nearest 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 nearest 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 +451,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 +476,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 +495,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 @@ -323,16 +542,43 @@ def _isCompatible(self, other, reporter): # 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 +586,93 @@ 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: A :class:`bool` indicating the desired winding + direction. :obj:`True` sets the direction to clockwise, + and :obj:`False` to counter-clockwise. 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. + + .. important:: + + Subclasses must override this method. + """ self.raiseNotImplementedError() @@ -383,21 +680,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,21 +717,38 @@ 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. + + Example:: >>> contour.contourInside(otherContour) True - ``contour`` must be a :class:`BaseContour`. """ 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. + + .. important:: + + Subclasses must override this method. + """ self.raiseNotImplementedError() @@ -427,19 +756,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 +804,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,20 +856,43 @@ 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. + + Example:: + + >>> contour.segments + (, ...) + + """, + ) + + 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. - def _get_segments(self): - """ - Subclasses may override this method. """ - points = list(self.points) + points = self.points if not points: - return [] - segments = [[]] + return () + segments: List[List[BasePoint]] = [[]] lastWasOffCurve = False firstIsMove = points[0].type == "move" for point in points: @@ -511,21 +914,37 @@ def _get_segments(self): segment = segments.pop(0) segments.append(segment) # wrap into segments - wrapped = [] + wrapped: List[BaseSegment] = [] for points in segments: - s = self.segmentClass() - s._setPoints(points) - self._setContourInSegment(s) - wrapped.append(s) - return wrapped + if self.segmentClass is None: + raise TypeError("segmentClass cannot be None.") + segment = self.segmentClass() + segment._setPoints(points) + self._setContourInSegment(segment) + wrapped.append(segment) + return tuple(wrapped) + + def __getitem__(self, index: int) -> BaseSegment: + """Get the segment at the specified 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. - def __getitem__(self, index): + """ 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,18 +953,54 @@ 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): - """ - Append a segment to the contour. + def appendSegment( + self, + type: Optional[str] = None, + points: Optional[PointCollectionType] = None, + smooth: bool = False, + segment: Optional[BaseSegment] = None, + ) -> None: + """Append the given segment to the contour. + + If `type` or `points` are specified, those values will be used instead + of the values in the given `segment` object. The specified `smooth` + state will be applied if ``segment=None``. + + :param type: An optional :attr:`BaseSegment.type` to be applied to + the segment as a :class:`str`. Defaults to :obj:`None`. + :param points: The optional :attr:`BaseSegment.points` to be applied to + the segment as a :class:`list` or :class:`tuple` + of :ref:`type-coordinate` items. Defaults to :obj:`None`. + :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the + segment as a :class:`bool`. Defaults to :obj:`False`. + :param segment: An optional :class:`BaseSegment` instance from which + attribute values will be copied. Defualts to :obj:`None`. + """ if segment is not None: if type is not None: @@ -553,26 +1008,70 @@ def appendSegment(self, type=None, points=None, smooth=False, segment=None): if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth + if type is None: + raise TypeError("Type cannot be None.") type = normalizers.normalizeSegmentType(type) - pts = [] - for pt in points: - pt = normalizers.normalizeCoordinateTuple(pt) - pts.append(pt) - points = pts + if points is not None: + normalizedPoints = [normalizers.normalizeCoordinateTuple(p) for p in points] + # Avoid mypy invariant List error. + castPoints = cast(PointCollectionType, normalizedPoints) smooth = normalizers.normalizeBoolean(smooth) - self._appendSegment(type=type, points=points, smooth=smooth) + self._appendSegment(type=type, points=castPoints, smooth=smooth) + + def _appendSegment( + self, type: str, points: PointCollectionType, smooth: bool, **kwargs: Any + ) -> None: + r"""Append the given segment to the native contour. + + This is the environment implementation of :meth:`BaseContour.appendSegment`. + + :param type: The :attr:`BaseSegment.type` to be applied to the segment as + a :class:`str`. The value will have been normalized + with :func:`normalizers.normalizeSegmentType`. + :param points: The :attr:`BaseSegment.points` to be applied to the segment as + a :class:`list` or :class:`tuple` of :ref:`type-coordinate` items. + The value will have been normalized + with :func:`normalizers.normalizeCoordinateTuple`. + :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the segment + as a :class:`bool`. The value will have been normalized + with :func:`normalizers.normalizeBoolean`. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. - def _appendSegment(self, type=None, points=None, smooth=False, **kwargs): - """ - Subclasses may override this method. """ self._insertSegment( len(self), type=type, points=points, smooth=smooth, **kwargs ) - def insertSegment(self, index, type=None, points=None, smooth=False, segment=None): - """ - Insert a segment into the contour. + def insertSegment( + self, + index: int, + type: Optional[str] = None, + points: Optional[PointCollectionType] = None, + smooth: bool = False, + segment: Optional[BaseSegment] = None, + ) -> None: + """Insert the given segment into the contour. + + If `type` or `points` are specified, those values will be used instead + of the values in the given `segment` object. The specified `smooth` + state will be applied if ``segment=None``. + + :param index: The :attr:`BaseSegment.index` to be applied to the segment + as a :class:`int`. + :param type: An optional :attr:`BaseSegment.type` to be applied to the + segment as a :class:`str`. Defaults to :obj:`None`. + :param points: The optional :attr:`BaseSegment.points` to be applied to + the segment as a :class:`list` or :class:`tuple` + of :ref:`type-coordinate` items. Defaults to :obj:`None`. + :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the + segment as a :class:`bool`. Defaults to :obj:`False`. + :param segment: An optional :class:`BaseSegment` instance from which + attribute values will be copied. Defualts to :obj:`None`. + """ if segment is not None: if type is not None: @@ -580,21 +1079,50 @@ def insertSegment(self, index, type=None, points=None, smooth=False, segment=Non if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth - index = normalizers.normalizeIndex(index) + normalizedIndex = normalizers.normalizeIndex(index) + if normalizedIndex is None: + raise TypeError("Index cannot be None.") + if type is None: + raise TypeError("Type cannot be None.") type = normalizers.normalizeSegmentType(type) - pts = [] - for pt in points: - pt = normalizers.normalizeCoordinateTuple(pt) - pts.append(pt) - points = pts + if points is not None: + normalizedPoints = [normalizers.normalizeCoordinateTuple(p) for p in points] + # Avoid mypy invariant List error. + castPoints = cast(PointCollectionType, normalizedPoints) smooth = normalizers.normalizeBoolean(smooth) - self._insertSegment(index=index, type=type, points=points, smooth=smooth) + self._insertSegment(index=index, type=type, points=castPoints, smooth=smooth) def _insertSegment( - self, index=None, type=None, points=None, smooth=False, **kwargs - ): - """ - Subclasses may override this method. + self, + index: int, + type: str, + points: PointCollectionType, + smooth: bool, + **kwargs: Any, + ) -> None: + r"""Insert the given segment into the native contour. + + This is the environment implementation of :meth:`BaseContour.insertSegment`. + + :param index: The :attr:`BaseSegment.index` to be applied to the segment + as a :class:`int`. The value will have been normalized + with :func:`normalizers.normalizeIndex`. + :param type: The :attr:`BaseSegment.type` to be applied to the segment as + a :class:`str`. The value will have been normalized + with :func:`normalizers.normalizeSegmentType`. + :param points: The :attr:`BaseSegment.points` to be applied to the segment as + a :class:`list` or :class:`tuple` of :ref:`type-coordinate` items. + The value will have been normalized + with :func:`normalizers.normalizeCoordinateTuple`. + :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the + segment as a :class:`bool`. The value will have been normalized + with :func:`normalizers.normalizeBoolean`. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ onCurve = points[-1] offCurve = points[:-1] @@ -608,36 +1136,76 @@ def _insertSegment( for offCurvePoint in reversed(offCurve): self.insertPoint(ptCount, offCurvePoint, type="offcurve") - def removeSegment(self, segment, preserveCurve=False): - """ - Remove segment from the contour. - If ``preserveCurve`` is set to ``True`` an attempt - will be made to preserve the shape of the curve - if the environment supports that functionality. + def removeSegment( + self, segment: Union[BaseSegment, int], preserveCurve: bool = False + ) -> None: + """Remove the given segment from the contour. + + If ``preserveCurve=True``, an attempt will be made to preserve the + overall shape of the curve after the segment is removed, provided the + environment supports such functionality. + + :param segment: The segment to remove as a :class:`BaseSegment` instance, + or an :class:`int` representing the segment's index. + :param preserveCurve: A :class:`bool` indicating whether to preserve + the curve's shape after the segment is removed. Defaults to :obj:`False`. + :raises ValueError: If the segment index is out of range or if the + specified segment is not part of the contour. + + Example:: + + >>> contour.removeSegment(mySegment) + >>> contour.removeSegment(2, preserveCurve=True) + """ if not isinstance(segment, int): - segment = self.segments.index(segment) - segment = normalizers.normalizeIndex(segment) - if segment >= self._len__segments(): - raise ValueError(f"No segment located at index {segment}.") + index = self.segments.index(segment) + normalizedIndex = normalizers.normalizeIndex(index) + if normalizedIndex is None: + return + if normalizedIndex >= self._len__segments(): + raise ValueError(f"No segment located at index {normalizedIndex}.") preserveCurve = normalizers.normalizeBoolean(preserveCurve) - self._removeSegment(segment, preserveCurve) + self._removeSegment(normalizedIndex, preserveCurve) - def _removeSegment(self, segment, preserveCurve, **kwargs): - """ - segment will be a valid segment index. - preserveCurve will be a boolean. + def _removeSegment(self, index: int, preserveCurve: bool, **kwargs: Any) -> None: + r"""Remove the given segment from the native contour. + + This is the environment implementation of :meth:`BaseContour.removeSegment`. + + :param index: The segment to remove as an :class:`int` representing + the segment's index. The value will have been normalized + with :func:`normalizers.normalizeIndex`. + :param preserveCurve: A :class:`bool` indicating whether to preserve + the curve's shape after the segment is removed. Defaults to :obj:`False`. + The value will have been normalized + with :func:`normalizers.normalizeBoolean`. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. - Subclasses may override this method. """ - segment = self.segments[segment] + segment = self.segments[index] for point in segment.points: self.removePoint(point, preserveCurve) - def setStartSegment(self, segment): - """ - Set the first segment on the contour. - segment can be a segment object or an index. + def setStartSegment(self, segment: Union[BaseSegment, int]) -> None: + """Set the first segment in the contour. + + :param segment: The segment to set as the first instance in the contour + as a :class:`BaseSegment` instance, or an :class:`int` representing + the segment's index. + :raises FontPartsError: If the contour is open. + :raises ValueError: If the segment index is out of range or if the + specified segment is not part of the contour. + + Example:: + + >>> contour.setStartSegment(mySegment) + >>> contour.setStartSegment(2) + """ if self.open: raise FontPartsError("An open contour can not change the starting segment.") @@ -656,9 +1224,19 @@ def setStartSegment(self, segment): ) self._setStartSegment(segmentIndex) - def _setStartSegment(self, segmentIndex, **kwargs): - """ - Subclasses may override this method. + def _setStartSegment(self, segmentIndex: int, **kwargs: Any) -> None: + r"""Set the first segment in the native contour. + + This is the environment implementation of :meth:`BaseContour.setStartSegment`. + + :param segmentIndex: An :class:`int` representing the index of the + segment to be set as the first instance in the contour. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ # get the previous segment and set # its on curve as the first point @@ -673,13 +1251,24 @@ def _setStartSegment(self, segmentIndex, **kwargs): # bPoints # ------- - bPoints = dynamicProperty("bPoints") + bPoints: dynamicProperty = dynamicProperty( + "bPoints", + """Get a list of all bPoints in the contour. + + This property is read-only. + + :return: A :class:`tuple` of :class`BaseBPoints`. + + """, + ) - def _get_bPoints(self): - bPoints = [] + def _get_bPoints(self) -> Tuple[BaseBPoint, ...]: + bPoints: List[BaseBPoint] = [] for point in self.points: if point.type not in ("move", "line", "curve"): continue + if self.bPointClass is None: + raise TypeError("bPointClass cannot be None.") bPoint = self.bPointClass() bPoint.contour = self bPoint._setPoint(point) @@ -687,10 +1276,29 @@ def _get_bPoints(self): return tuple(bPoints) def appendBPoint( - self, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None - ): - """ - Append a bPoint to the contour. + 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 the given bPoint to the contour. + + If `type`, `anchor`, `bcpIn` or `bcpOut` are specified, those values + will be used instead of the values in the given `segment` object. + + :param type: An optional :attr:`BaseBPoint.type` to be applied to + the bPoint as a :class:`str`. Defaults to :obj:`None`. + :param anchor: An optional :attr:`BaseBPoint.anchor` to be applied to + the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param bcpIn: An optional :attr:`BaseBPoint.bcpIn` to be applied to the + bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to the + bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param bPoint: An optional :class:`BaseBPoint` instance from which + attribute values will be copied. Defualts to :obj:`None`. + """ if bPoint is not None: if type is None: @@ -701,7 +1309,11 @@ def appendBPoint( bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut + if type is None: + raise TypeError("Type cannot be None.") type = normalizers.normalizeBPointType(type) + if anchor is None: + raise TypeError("Anchor cannot be None.") anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) @@ -711,17 +1323,66 @@ def appendBPoint( bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._appendBPoint(type, anchor, bcpIn=bcpIn, bcpOut=bcpOut) - def _appendBPoint(self, type, anchor, bcpIn=None, bcpOut=None, **kwargs): - """ - Subclasses may override this method. + def _appendBPoint( + self, + type: str, + anchor: PairCollectionType[IntFloatType], + bcpIn: PairCollectionType[IntFloatType], + bcpOut: PairCollectionType[IntFloatType], + **kwargs: Any, + ) -> None: + r"""Append the given bPoint to the native contour. + + This is the environment implementation of :meth:`BaseContour.appendBPoint`. + + :param type: The :attr:`BaseBPoint.type` to be applied to the bPoint as + a :class:`str`. The value will have been normalized + with :func:`normalizers.normalizeBPointType`. + :param anchor: The :attr:`BaseBPoint.anchor` to be applied to the bPoint + as a :ref:`type-coordinate`. The value will have been normalized + with :func:`normalizers.normalizeCoordinateTuple`. + :param bcpIn: The :attr:`BaseBPoint.bcpIn` to be applied to the bPoint + as a :ref:`type-coordinate`. The value will have been normalized + with :func:`normalizers.normalizeCoordinateTuple`. + :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to + the bPoint as a :ref:`type-coordinate`. The value will have been + normalized with :func:`normalizers.normalizeCoordinateTuple`. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + 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 - ): - """ - Insert a bPoint at index in the contour. + 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 the given bPoint into the contour. + + If `type`, `anchor`, `bcpIn` or `bcpOut` are specified, those values + will be used instead of the values in the given `segment` object. + + :param index: The :attr:`BaseBPoint.index` to be applied to the bPoint + as an :class:`int`. + :param type: An optional :attr:`BaseBPoint.type` to be applied to + the bPoint as a :class:`str`. Defaults to :obj:`None`. + :param anchor: An optional :attr:`BaseBPoint.anchor` to be applied to + the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param bcpIn: An optional :attr:`BaseBPoint.bcpIn` to be applied to the + bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to the + bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param bPoint: An optional :class:`BaseBPoint` instance from which + attribute values will be copied. Defualts to :obj:`None`. + """ if bPoint is not None: if type is None: @@ -732,8 +1393,14 @@ def insertBPoint( bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut - index = normalizers.normalizeIndex(index) + normalizedIndex = normalizers.normalizeIndex(index) + if normalizedIndex is None: + raise TypeError("Index cannot be None.") + if type is None: + raise TypeError("Type cannot be None.") type = normalizers.normalizeBPointType(type) + if anchor is None: + raise TypeError("Anchor cannot be None.") anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) @@ -742,12 +1409,43 @@ def insertBPoint( bcpOut = (0, 0) bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._insertBPoint( - index=index, type=type, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut + index=normalizedIndex, type=type, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut ) - def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): - """ - Subclasses may override this method. + def _insertBPoint( + self, + index: int, + type: str, + anchor: PairCollectionType[IntFloatType], + bcpIn: PairCollectionType[IntFloatType], + bcpOut: PairCollectionType[IntFloatType], + **kwargs: Any, + ) -> None: + r"""Insert the given bPoint into the native contour. + + This is the environment implementation of :meth:`BaseContour.insertBPoint`. + + :param index: The :attr:`BaseBPoint.index` to be applied to the bPoint + as an :class:`int`. The value will have been normalized + with :func:`normalizers.normalizeIndex`. + :param type: An optional :attr:`BaseBPoint.type` to be applied to + the bPoint as a :class:`str`. The value will have been normalized + with :func:`normalizers.normalizeBPointType`. + :param anchor: The :attr:`BaseBPoint.anchor` to be applied to the bPoint + as a :ref:`type-coordinate`. The value will have been normalized + with :func:`normalizers.normalizeCoordinateTuple`. + :param bcpIn: The :attr:`BaseBPoint.bcpIn` to be applied to the bPoint + as a :ref:`type-coordinate`. The value will have been normalized + with :func:`normalizers.normalizeCoordinateTuple`. + :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to + the bPoint as a :ref:`type-coordinate`. The value will have been + normalized with :func:`normalizers.normalizeCoordinateTuple`. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ # insert a simple line segment at the given anchor # look it up as a bPoint and change the bcpIn and bcpOut there @@ -764,23 +1462,43 @@ def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): bPoint.bcpOut = bcpOut bPoint.type = type - def removeBPoint(self, bPoint): - """ - Remove the bpoint from the contour. - bpoint can be a point object or an index. - """ - if not isinstance(bPoint, int): - bPoint = bPoint.index - bPoint = normalizers.normalizeIndex(bPoint) - if bPoint >= self._len__points(): - raise ValueError(f"No bPoint located at index {bPoint}.") - self._removeBPoint(bPoint) - - def _removeBPoint(self, index, **kwargs): + def removeBPoint(self, bPoint: Union[BaseBPoint, int]) -> None: + """Remove the given bPoint from the contour. + + :param bPoint: The bPoint to remove as a :class:`BaseBPoint` instance, + or an :class:`int` representing the bPoint's index. + :raises ValueError: If the bPoint index is out of range or if the + specified bPoint is not part of the contour. + + Example:: + + >>> contour.removeBPoint(myBPoint) + >>> contour.removeBPoint(2) + """ - index will be a valid index. + index = bPoint.index if not isinstance(bPoint, int) else bPoint + normalizedIndex = normalizers.normalizeIndex(index) + # Avoid mypy conflict with normalizeIndex -> Optional[int] + if normalizedIndex is None: + return + if normalizedIndex >= self._len__points(): + raise ValueError(f"No bPoint located at index {normalizedIndex}.") + self._removeBPoint(normalizedIndex) + + def _removeBPoint(self, index: int, **kwargs: Any) -> None: + r"""Remove the given bPoint from the native contour. + + This is the environment implementation of :meth:`BaseContour.removeBPoint`. + + :param index: The index representing the :class:`BaseBPoint` subclass + instance to remove as an :class:`int. The value will have been + normalized with :func:`normalizers.normalizeIndex`. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. - Subclasses may override this method. """ bPoint = self.bPoints[index] @@ -802,49 +1520,78 @@ 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", + """Get a list of all points in the contour. + + This property is read-only. + + :return: A :class:`tuple` of :class`BasePoints`. + + """, + ) + + def _get_points(self) -> Tuple[BasePoint, ...]: + """Get a list of all points in the native contour. + + This is the environment implementation of the :attr:`BaseContour.points` + property getter. + + :return: A :class:`tuple` of :class`BasePoint` subclass instances. + + .. note:: + + Subclasses may override this method. - 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): + def _len__points(self) -> int: return self._lenPoints() - def _lenPoints(self, **kwargs): - """ - This must return an integer indicating - the number of points in the contour. + def _lenPoints(self, **kwargs: Any) -> int: + r"""Return the number of points in the native contour. + + :param \**kwargs: Additional keyword arguments. + :return: An :class:`int` representing the number of :class:`BasePoint` + subclass instances belonging to the contour. + + .. important:: + + Subclasses must override this method. - Subclasses must override this method. """ self.raiseNotImplementedError() - def _getitem__points(self, index): - index = normalizers.normalizeIndex(index) - if index >= self._len__points(): - raise ValueError(f"No point located at index {index}.") - point = self._getPoint(index) + def _getitem__points(self, index: int) -> BasePoint: + normalizedIndex = normalizers.normalizeIndex(index) + if normalizedIndex is None or normalizedIndex >= self._len__points(): + raise ValueError(f"No point located at index {normalizedIndex}.") + point = self._getPoint(normalizedIndex) self._setContourInPoint(point) return point - def _getPoint(self, index, **kwargs): - """ - This must return a wrapped point. + def _getPoint(self, index: int, **kwargs: Any) -> BasePoint: + r"""Get the given point from the native contour. + + :param index: The index representing the :class:`BaseBPoint` subclass + instance to retrieve as an :class:`int. The value will have been + normalized with :func:`normalizers.normalizeIndex`. + :param \**kwargs: Additional keyword arguments. + :return: A :class:`BasePoint` subclass instance. - index will be a valid index. + .. important:: + + Subclasses must override this method. - Subclasses must override this method. """ self.raiseNotImplementedError() - def _getPointIndex(self, point): + def _getPointIndex(self, point: BasePoint) -> int: for i, other in enumerate(self.points): if point == other: return i @@ -852,15 +1599,32 @@ def _getPointIndex(self, point): def appendPoint( self, - position=None, - type="line", - smooth=False, - name=None, - identifier=None, - point=None, - ): - """ - Append a point to the contour. + 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 the given point to the contour. + + If `position`, `type` or `name` are specified, those values will be used + instead of the values in the given `segment` object. The specified + `smooth` state will be applied if ``point=None``. + + :param position: An optional position to be applied to the point as + a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param type: An optional :attr:`BasePoint.type` to be applied to + the point as a :class:`str`. Defaults to ``'line'``. + :param smooth: The :attr:`BasePoint.smooth` state to be applied to the + point as a :class:`bool`. Defaults to :obj:`False`. + :param name: An optional :attr:`BasePoint.name` to be applied to the + point as a :class:`str`. Defaults to :obj:`None`. + :param identifier: An optional :attr:`BasePoint.identifier` to be + applied to the point as a :class:`str`. Defaults to :obj:`None`. + :param point: An optional :class:`BasePoint` instance from which + attribute values will be copied. Defualts to :obj:`None`. + """ if point is not None: if position is None: @@ -882,16 +1646,35 @@ def appendPoint( def insertPoint( self, - index, - position=None, - type="line", - smooth=False, - name=None, - identifier=None, - point=None, - ): - """ - Insert a point into the contour. + 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 the given point into the contour. + + If `position`, `type` or `name` are specified, those values will be used + instead of the values in the given `segment` object. The specified + `smooth` state will be applied if ``point=None``. + + :param index: The :attr:`BasePoint.index` to be applied to the point + as an :class:`int`. + :param position: An optional position to be applied to the point as + a :ref:`type-coordinate`. Defaults to :obj:`None`. + :param type: An optional :attr:`BasePoint.type` to be applied to + the point as a :class:`str`. Defaults to ``'line'``. + :param smooth: The :attr:`BasePoint.smooth` state to be applied to the + point as a :class:`bool`. Defaults to :obj:`False`. + :param name: An optional :attr:`BasePoint.name` to be applied to the + point as a :class:`str`. Defaults to :obj:`None`. + :param identifier: An optional :attr:`BasePoint.identifier` to be + applied to the point as a :class:`str`. Defaults to :obj:`None`. + :param point: An optional :class:`BasePoint` instance from which + attribute values will be copied. Defualts to :obj:`None`. + """ if point is not None: if position is None: @@ -902,7 +1685,11 @@ def insertPoint( name = point.name if identifier is not None: identifier = point.identifier - index = normalizers.normalizeIndex(index) + normalizedIndex = normalizers.normalizeIndex(index) + if normalizedIndex is None: + raise TypeError("Index cannot be None.") + if position is None: + raise TypeError("Position cannot be None.") position = normalizers.normalizeCoordinateTuple(position) type = normalizers.normalizePointType(type) smooth = normalizers.normalizeBoolean(smooth) @@ -911,7 +1698,7 @@ def insertPoint( if identifier is not None: identifier = normalizers.normalizeIdentifier(identifier) self._insertPoint( - index, + normalizedIndex, position=position, type=type, smooth=smooth, @@ -921,54 +1708,117 @@ def insertPoint( 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. - smooth will be a valid boolean. - name will be a valid name or None. - identifier will be a valid identifier or None. - The identifier will not have been tested for uniqueness. - - Subclasses must override this method. + index: int, + position: PairCollectionType[IntFloatType], + type: str, + smooth: bool, + name: Optional[str], + identifier: Optional[str], + **kwargs: Any, + ) -> None: + r"""Insert the given point into the native contour. + + This is the environment implementation of :meth:`BaseContour.insertPoint`. + + :param index: The :attr:`BasePoint.index` to be applied to the point + as an :class:`int`. The value will have been normalized + with :func:`normalizers.normalizeIndex`. + :param position: The position to be applied to the point as + a :ref:`type-coordinate`. The value will have been normalized with + :func:`normalizers.normalizeCoordinateTuple`. + :param type: The :attr:`BasePoint.type` to be applied to the point as + a :class:`str`. The value will have been normalized + with :func:`normalizers.normalizePointType`. + :param smooth: The :attr:`BasePoint.smooth` state to be applied to the + point as a :class:`bool`. The value will have been normalized + with :func:`normalizers.normalizeBoolean`. + :param name: An optional :attr:`BasePoint.name` to be applied to the + point as a :class:`str`. The value will have been normalized + with :func:`normalizers.normalizePointName` + :param identifier: An optional :attr:`BasePoint.identifier` to be + applied to the point as a :class:`str`. The value will have been + normalized with :func:`normalizers.normalizeIdentifier`, but will + not have been tested for uniqueness. + :param \**kwargs: Additional keyword arguments. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. important:: + + Subclasses must override this method. + """ self.raiseNotImplementedError() - def removePoint(self, point, preserveCurve=False): - """ - Remove the point from the contour. - point can be a point object or an index. - If ``preserveCurve`` is set to ``True`` an attempt - will be made to preserve the shape of the curve - if the environment supports that functionality. + def removePoint( + self, point: Union[BasePoint, int], preserveCurve: bool = False + ) -> None: + """Remove the given point from the contour. + + If ``preserveCurve=True``, an attempt will be made to preserve the + overall shape of the curve after the segment is removed, provided the + environment supports such functionality. + + :param point: The point to remove as a :class:`BasePoint` instance, + or an :class:`int` representing the points's index. + :param preserveCurve: A :class:`bool` indicating whether to preserve + the curve's shape after the point is removed. Defaults to :obj:`False`. + :raises ValueError: If the point index is out of range or if the + specified point is not part of the contour. + + Example:: + + >>> contour.removePoint(myPoint) + >>> contour.removePoint(2, preserveCurve=True) + """ - if not isinstance(point, int): - point = self.points.index(point) - point = normalizers.normalizeIndex(point) - if point >= self._len__points(): - raise ValueError(f"No point located at index {point}.") + index = self.points.index(point) if not isinstance(point, int) else point + normalizedIndex = normalizers.normalizeIndex(index) + # Avoid mypy conflict with normalizeIndex -> Optional[int] + if normalizedIndex is None: + return + if normalizedIndex >= self._len__points(): + raise ValueError(f"No point located at index {normalizedIndex}.") preserveCurve = normalizers.normalizeBoolean(preserveCurve) - self._removePoint(point, preserveCurve) + self._removePoint(normalizedIndex, preserveCurve) - def _removePoint(self, index, preserveCurve, **kwargs): - """ - index will be a valid index. preserveCurve will be a boolean. + def _removePoint(self, index: int, preserveCurve: bool, **kwargs: Any) -> None: + r"""Remove the given point from the native contour. + + This is the environment implementation of :meth:`BaseContour.removePoint`. + + :param index: The index representing the :class:`BasePoint` subclass + instance to remove as an :class:`int. The value will have been + normalized with :func:`normalizers.normalizeIndex`. + :param preserveCurve: A :class:`bool` indicating whether to preserve + the curve's shape after the point is removed. The value will have been + normalized with :func:`normalizers.normalizeBoolean`. + :param \**kwargs: Additional keyword arguments. + :raises NotImplementedError: If the method has not been overridden by a + subclass. + + .. important:: + + Subclasses must override this method. - Subclasses must override this method. """ self.raiseNotImplementedError() - def setStartPoint(self, point): - """ - Set the first point on the contour. - point can be a point object or an index. + def setStartPoint(self, point: Union[BasePoint, int]) -> None: + """Set the first segment in the contour. + + :param segment: The point to set as the first instance in the contour + as a :class:`BasePoint` instance, or an :class:`int` representing + the point's index. + :raises FontPartsError: If the contour is open. + :raises ValueError: If the point index is out of range or if the + specified point is not part of the contour. + + Example:: + + >>> contour.setStartPoint(myPoint) + >>> contour.setStartPoint(2) + """ if self.open: raise FontPartsError("An open contour can not change the starting point.") @@ -985,9 +1835,19 @@ def setStartPoint(self, point): ) self._setStartPoint(pointIndex) - def _setStartPoint(self, pointIndex, **kwargs): - """ - Subclasses may override this method. + def _setStartPoint(self, pointIndex: int, **kwargs: Any) -> None: + r"""Set the first segment in the native contour. + + This is the environment implementation of :meth:`BaseContour.setStartPoint`. + + :param pointIndex: An :class:`int` representing the index of the point + to be set as the first instance in the contour. + :param \**kwargs: Additional keyword arguments. + + .. note:: + + Subclasses may override this method. + """ points = self.points points = points[pointIndex:] + points[:pointIndex] @@ -1010,152 +1870,270 @@ def _setStartPoint(self, pointIndex, **kwargs): # segments - selectedSegments = dynamicProperty( + selectedSegments: dynamicProperty = dynamicProperty( "base_selectedSegments", - """ - A list of segments selected in the contour. + """Get or set the selected segments in the contour. + + The value must be a :class:`tuple` or :class:`list` of + either :class:`BaseSegment` instances or :class:`int` values + representing segment indexes to select. + + :return: A :class:`tuple` of the currently selected :class:`BaseSegment` + instances. - Getting selected segment objects: + Getting selected segments:: >>> for segment in contour.selectedSegments: ... segment.move((10, 20)) - Setting selected segment objects: + Setting selected segments:: >>> contour.selectedSegments = someSegments - Setting also supports segment indexes: + Setting selection using indexes:: >>> contour.selectedSegments = [0, 2] + """, ) - 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): - """ - Subclasses may override this method. + def _get_selectedSegments(self) -> Tuple[BaseSegment, ...]: + """Get the selected segments in the native contour. + + This is the environment implementation of the + :attr:`BaseContour.selectedSegments` property getter. + + :return: A :class:`tuple` of the currently selected :class:`BaseSegment` + instances. Each value item will be normalized + with :func:`normalizers.normalizeSegment`. + + .. note:: + + Subclasses may override this method. + """ return self._getSelectedSubObjects(self.segments) - def _set_base_selectedSegments(self, value): + def _set_base_selectedSegments( + self, value: CollectionType[Union[BaseSegment, int]] + ) -> None: normalized = [] - for i in value: - if isinstance(i, int): - i = normalizers.normalizeSegmentIndex(i) + for segment in value: + normalizedSegment: Union[BaseSegment, int] + if isinstance(segment, int): + normalizedIndex = normalizers.normalizeIndex(segment) + # Avoid mypy conflict with normalizeIndex -> Optional[int] + if normalizedIndex is None: + continue + normalizedSegment = normalizedIndex else: - i = normalizers.normalizeSegment(i) - normalized.append(i) + normalizedSegment = normalizers.normalizeSegment(segment) + normalized.append(normalizedSegment) self._set_selectedSegments(normalized) - def _set_selectedSegments(self, value): - """ - Subclasses may override this method. + def _set_selectedSegments( + self, value: CollectionType[Union[BaseSegment, int]] + ) -> None: + """Set the selected segments in the native contour. + + This is the environment implementation of the + :attr:`BaseContour.selectedSegments` property setter. + + :param value: The segments to select as a :class:`tuple` + or :class:`list` of either :class:`BaseContour` instances + or :class:`int` values representing segment indexes. Each value item + will have been normalized with :func:`normalizers.normalizeSegment` + or :func:`normalizers.normalizeIndex`. + + .. note:: + + Subclasses may override this method. + """ return self._setSelectedSubObjects(self.segments, value) # points - selectedPoints = dynamicProperty( + selectedPoints: dynamicProperty = dynamicProperty( "base_selectedPoints", - """ - A list of points selected in the contour. + """Get or set the selected points in the contour. + + The value must be a :class:`tuple` or :class:`list` of + either :class:`BasePoint` instances or :class:`int` values + representing point indexes to select. + + :return: A :class:`tuple` of the currently selected :class:`BasePoint` + instances. - Getting selected point objects: + Getting selected points:: >>> for point in contour.selectedPoints: ... point.move((10, 20)) - Setting selected point objects: + Setting selected points:: >>> contour.selectedPoints = somePoints - Setting also supports point indexes: + Setting selection using indexes:: >>> contour.selectedPoints = [0, 2] + """, ) - 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): - """ - Subclasses may override this method. + def _get_selectedPoints(self) -> Tuple[BasePoint, ...]: + """Get the selected points in the native contour. + + This is the environment implementation of + the :attr:`BaseContour.selectedPoints` property getter. + + :return: A :class:`tuple` of the currently selected :class:`BasePoint` + instances. Each value item will be normalized + with :func:`normalizers.normalizePoint`. + + .. note:: + + Subclasses may override this method. + """ return self._getSelectedSubObjects(self.points) - def _set_base_selectedPoints(self, value): + def _set_base_selectedPoints( + self, value: CollectionType[Union[BasePoint, int]] + ) -> None: normalized = [] - for i in value: - if isinstance(i, int): - i = normalizers.normalizePointIndex(i) + for point in value: + normalizedPoint: Union[BasePoint, int] + if isinstance(point, int): + normalizedIndex = normalizers.normalizeIndex(point) + # Avoid mypy conflict with normalizeIndex -> Optional[int] + if normalizedIndex is None: + continue + normalizedPoint = normalizedIndex else: - i = normalizers.normalizePoint(i) - normalized.append(i) + normalizedPoint = normalizers.normalizePoint(point) + normalized.append(normalizedPoint) self._set_selectedPoints(normalized) - def _set_selectedPoints(self, value): - """ - Subclasses may override this method. + def _set_selectedPoints(self, value: CollectionType[Union[BasePoint, int]]) -> None: + """Set the selected points in the native contour. + + This is the environment implementation of + the :attr:`BaseContour.selectedPoints` property setter. + + :param value: The points to select as a :class:`tuple` or :class:`list` + of either :class:`BasePoint` instances or :class:`int` values + representing point indexes to select. Each value item will have been + normalized with :func:`normalizers.normalizePoint` + or :func:`normalizers.normalizeIndex`. + + .. note:: + + Subclasses may override this method. + """ return self._setSelectedSubObjects(self.points, value) # bPoints - selectedBPoints = dynamicProperty( + selectedBPoints: dynamicProperty = dynamicProperty( "base_selectedBPoints", - """ - A list of bPoints selected in the contour. + """Get or set the selected bPoints in the contour. + + The value must be a :class:`tuple` or :class:`list` of + either :class:`BaseBPoint` instances or :class:`int` values + representing bPoint indexes to select. + + :return: A :class:`tuple` of the currently selected :class:`BaseBPoint` + instances. - Getting selected bPoint objects: + Getting selected bPoints:: >>> for bPoint in contour.selectedBPoints: ... bPoint.move((10, 20)) - Setting selected bPoint objects: + Setting selected bPoints:: >>> contour.selectedBPoints = someBPoints - Setting also supports bPoint indexes: + Setting selection using indexes:: >>> contour.selectedBPoints = [0, 2] + """, ) - 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): - """ - Subclasses may override this method. + def _get_selectedBPoints(self) -> Tuple[BaseBPoint, ...]: + """Get the selected bPoints in the native contour. + + This is the environment implementation of + the :attr:`BaseContour.selectedBPoints` property getter. + + :return: A :class:`tuple` of the currently selected :class:`BaseBPoint` + instances. Each value item will be normalized + with :func:`normalizers.normalizeBPoint`. + + .. note:: + + Subclasses may override this method. + """ return self._getSelectedSubObjects(self.bPoints) - def _set_base_selectedBPoints(self, value): + def _set_base_selectedBPoints( + self, value: CollectionType[Union[BaseBPoint, int]] + ) -> None: normalized = [] - for i in value: - if isinstance(i, int): - i = normalizers.normalizeBPointIndex(i) + for bPoint in value: + normalizedBPoint: Union[BaseBPoint, int] + if isinstance(bPoint, int): + normalizedIndex = normalizers.normalizeIndex(bPoint) + # Avoid mypy conflict with normalizeIndex -> Optional[int] + if normalizedIndex is None: + continue + normalizedBPoint = normalizedIndex else: - i = normalizers.normalizeBPoint(i) - normalized.append(i) + normalizedBPoint = normalizers.normalizeBPoint(bPoint) + normalized.append(normalizedBPoint) self._set_selectedBPoints(normalized) - def _set_selectedBPoints(self, value): - """ - Subclasses may override this method. + def _set_selectedBPoints( + self, value: CollectionType[Union[BaseBPoint, int]] + ) -> None: + """Set the selected bPoints in the native contour. + + This is the environment implementation of + the :attr:`BaseContour.selectedBPoints` property setter. + + :param value: The bPoints to select as a :class:`tuple` or :class:`list` + of either :class:`BaseBPoint` instances or :class:`int` values + representing bPoint indexes to select. Each value item will have been + normalized with :func:`normalizers.normalizeBPoint` + or :func:`normalizers.normalizeIndex`. + + .. note:: + + Subclasses may override this method. + """ return self._setSelectedSubObjects(self.bPoints, value) diff --git a/Lib/fontParts/base/deprecated.py b/Lib/fontParts/base/deprecated.py index 157efd4e..3ab6c1be 100644 --- a/Lib/fontParts/base/deprecated.py +++ b/Lib/fontParts/base/deprecated.py @@ -325,7 +325,7 @@ def _generateIdentifierforPoint(self, point): ), DeprecationWarning, ) - return self._getIdentifierforPoint(point) + return self._getIdentifierForPoint(point) def generateIdentifierforPoint(self, point): warnings.warn( diff --git a/Lib/fontParts/fontshell/contour.py b/Lib/fontParts/fontshell/contour.py index cabdb5db..5972e39a 100644 --- a/Lib/fontParts/fontshell/contour.py +++ b/Lib/fontParts/fontshell/contour.py @@ -34,7 +34,7 @@ def _getIdentifier(self): contour = self.naked() return contour.generateIdentifier() - def _getIdentifierforPoint(self, point): + def _getIdentifierForPoint(self, point): contour = self.naked() point = point.naked() return contour.generateIdentifierForPoint(point) 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 diff --git a/Lib/fontParts/test/test_deprecated.py b/Lib/fontParts/test/test_deprecated.py index 7249b35e..33a69e59 100644 --- a/Lib/fontParts/test/test_deprecated.py +++ b/Lib/fontParts/test/test_deprecated.py @@ -1031,7 +1031,7 @@ def test_contour_deprecated__generateIdentiferforPoint(self): DeprecationWarning, "Contour._generateIdentifierforPoint()" ): i = contour._generateIdentifierforPoint(contour[0][0]) - self.assertEqual(i, contour._getIdentifierforPoint(contour[0][0])) + self.assertEqual(i, contour._getIdentifierForPoint(contour[0][0])) def test_contour_deprecated_generateIdentiferForPoint(self): contour = self.getContour_bounds()