Repository: fonttools/fonttools

Issue:
Breaking change with AbstractPen
I understand why and I probably need to embrace the typing future

but :)

This https://github.com/fonttools/fonttools/commit/f6a62e0db127c01f4d23f3398d70df21bbc6dccc PR is a breaking change.

```python
from fontTools.pens.basePen import AbstractPen

class D(AbstractPen):
    
    def moveTo(self, pt):
        print(pt)
    def lineTo(self, pt):
        print(pt)
    def curveToOne(self, *pts):
        print(pts)
    def closePath(self):
        print("close")
        
d = D()
```

results in:

```console
Traceback (most recent call last):
  File "<untitled>", line 15, in <module>
TypeError: Can't instantiate abstract class D with abstract methods addComponent, curveTo, endPath, qCurveTo
```

Maybe this had to be discussed more to envision the impact of such a change. Pens and pointPens are the core of lots of tools. 

For example a pen only dealing with components didnt need the requirement to implement all the extra `NotImplemented callbacks (like moveTo, lineTo, ... ) 

I dont know enough about `abc` to make it less strict  :) or maybe optionally at init of such a class to not raise an error. 

again, I dont have an issue with changing my code, but getting a breaking change is huge 




Discussion:
I admit that I didn't think things through when I suggested @madig to turn our Abstract[Point]Pen classes into Actual Abstract Base Classes.

I'm not sure what to do. It's true that this is a breaking change, even if the change may point out broken or dodgy code.

@typemytype showed me a real breaking example (adapted from real code):

```python
class SomeFilterPen(AbstractPointPen):

    def __init__(self, glyphSet, outPointPen):
        self.beginPath = outPointPen.beginPath
        self.endPath = outPointPen.endPath
        self.addPoint = outPointPen.addPoint

    def addComponent(self, baseGlyphName, transformation, identifier=None):
        ...

SomeFilterPen(None)
```

This class behaved perfectly before, but now raises TypeError upon instantiation.
Oh. I'm fine with reverting the change

Resolving patch:
diff --git a/Lib/fontTools/pens/basePen.py b/Lib/fontTools/pens/basePen.py
--- a/Lib/fontTools/pens/basePen.py
+++ b/Lib/fontTools/pens/basePen.py
@@ -36,7 +36,6 @@
 sequence of length 2 will do.
 """
 
-import abc
 from typing import Any, Tuple
 
 from fontTools.misc.loggingTools import LogMixin
@@ -45,42 +44,18 @@
 			"decomposeSuperBezierSegment", "decomposeQuadraticSegment"]
 
 
-class AbstractPen(abc.ABC):
-	@classmethod
-	def __subclasshook__(cls, subclass: Any) -> bool:
-		if cls is not AbstractPen:
-			return NotImplemented
-		return (
-			hasattr(subclass, "moveTo")
-			and callable(subclass.moveTo)
-			and hasattr(subclass, "lineTo")
-			and callable(subclass.lineTo)
-			and hasattr(subclass, "curveTo")
-			and callable(subclass.curveTo)
-			and hasattr(subclass, "qCurveTo")
-			and callable(subclass.qCurveTo)
-			and hasattr(subclass, "closePath")
-			and callable(subclass.closePath)
-			and hasattr(subclass, "endPath")
-			and callable(subclass.endPath)
-			and hasattr(subclass, "addComponent")
-			and callable(subclass.addComponent)
-			or NotImplemented
-		)
-
-	@abc.abstractmethod
+class AbstractPen:
+
 	def moveTo(self, pt: Tuple[float, float]) -> None:
 		"""Begin a new sub path, set the current point to 'pt'. You must
 		end each sub path with a call to pen.closePath() or pen.endPath().
 		"""
 		raise NotImplementedError
 
-	@abc.abstractmethod
 	def lineTo(self, pt: Tuple[float, float]) -> None:
 		"""Draw a straight line from the current point to 'pt'."""
 		raise NotImplementedError
 
-	@abc.abstractmethod
 	def curveTo(self, *points: Tuple[float, float]) -> None:
 		"""Draw a cubic bezier with an arbitrary number of control points.
 
@@ -102,7 +77,6 @@ def curveTo(self, *points: Tuple[float, float]) -> None:
 		"""
 		raise NotImplementedError
 
-	@abc.abstractmethod
 	def qCurveTo(self, *points: Tuple[float, float]) -> None:
 		"""Draw a whole string of quadratic curve segments.
 
@@ -120,21 +94,18 @@ def qCurveTo(self, *points: Tuple[float, float]) -> None:
 		"""
 		raise NotImplementedError
 
-	@abc.abstractmethod
 	def closePath(self) -> None:
 		"""Close the current sub path. You must call either pen.closePath()
 		or pen.endPath() after each sub path.
 		"""
 		pass
 
-	@abc.abstractmethod
 	def endPath(self) -> None:
 		"""End the current sub path, but don't close it. You must call
 		either pen.closePath() or pen.endPath() after each sub path.
 		"""
 		pass
 
-	@abc.abstractmethod
 	def addComponent(
 		self,
 		glyphName: str,
diff --git a/Lib/fontTools/pens/pointPen.py b/Lib/fontTools/pens/pointPen.py
--- a/Lib/fontTools/pens/pointPen.py
+++ b/Lib/fontTools/pens/pointPen.py
@@ -12,7 +12,6 @@
 For instance, whether or not a point is smooth, and its name.
 """
 
-import abc
 import math
 from typing import Any, List, Optional, Tuple
 
@@ -28,36 +27,17 @@
 ]
 
 
-class AbstractPointPen(abc.ABC):
+class AbstractPointPen:
 	"""Baseclass for all PointPens."""
 
-	@classmethod
-	def __subclasshook__(cls, subclass: Any) -> bool:
-		if cls is not AbstractPointPen:
-			return NotImplemented
-		return (
-			hasattr(subclass, "beginPath")
-			and callable(subclass.beginPath)
-			and hasattr(subclass, "endPath")
-			and callable(subclass.endPath)
-			and hasattr(subclass, "addPoint")
-			and callable(subclass.addPoint)
-			and hasattr(subclass, "addComponent")
-			and callable(subclass.addComponent)
-			or NotImplemented
-		)
-
-	@abc.abstractmethod
 	def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
 		"""Start a new sub path."""
 		raise NotImplementedError
 
-	@abc.abstractmethod
 	def endPath(self) -> None:
 		"""End the current sub path."""
 		raise NotImplementedError
 
-	@abc.abstractmethod
 	def addPoint(
 		self,
 		pt: Tuple[float, float],
@@ -70,7 +50,6 @@ def addPoint(
 		"""Add a point to the current sub path."""
 		raise NotImplementedError
 
-	@abc.abstractmethod
 	def addComponent(
 		self,
 		baseGlyphName: str,


Test patch:
diff --git a/Tests/pens/basePen_test.py b/Tests/pens/basePen_test.py
--- a/Tests/pens/basePen_test.py
+++ b/Tests/pens/basePen_test.py
@@ -5,29 +5,6 @@
 import unittest
 
 
-def test_subclasshook():
-    class NullPen:
-        def moveTo(self, pt):
-            pass
-        def lineTo(self, pt):
-            pass
-        def curveTo(self, *points):
-            pass
-        def qCurveTo(self, *points):
-            pass
-        def closePath(self):
-            pass
-        def endPath(self):
-            pass
-        def addComponent(self, glyphName, transformation):
-            pass
-
-    assert issubclass(NullPen, AbstractPen)
-    assert isinstance(NullPen(), AbstractPen)
-    assert not issubclass(NullPen, AbstractPointPen)
-    assert not isinstance(NullPen(), AbstractPointPen)
-
-
 class _TestPen(BasePen):
     def __init__(self):
         BasePen.__init__(self, glyphSet={})
diff --git a/Tests/pens/pointPen_test.py b/Tests/pens/pointPen_test.py
--- a/Tests/pens/pointPen_test.py
+++ b/Tests/pens/pointPen_test.py
@@ -5,23 +5,6 @@
     SegmentToPointPen, GuessSmoothPointPen, ReverseContourPointPen
 
 
-def test_subclasshook():
-    class NullPen:
-        def beginPath(self, identifier, **kwargs) -> None:
-            pass
-        def endPath(self) -> None:
-            pass
-        def addPoint(self, pt, segmentType, smooth, name, identifier, **kwargs) -> None:
-            pass
-        def addComponent(self, baseGlyphName, transformation, identifier, **kwargs) -> None:
-            pass
-
-    assert issubclass(NullPen, AbstractPointPen)
-    assert isinstance(NullPen(), AbstractPointPen)
-    assert not issubclass(NullPen, AbstractPen)
-    assert not isinstance(NullPen(), AbstractPen)
-
-
 class _TestSegmentPen(AbstractPen):
 
     def __init__(self):