Repository: fonttools/fonttools (Python)

Issue:
TTFont.getGlyphSet() returning _TTVarGlyphSet where type hints/typechecker expect _TTGlyphSet
the new `_TTVarGlyphSet` (#2738) that is returned from `TTFont.getGlyphSet()` is not a subclass of the old `_TTGlyphSet`, and neither is the `_TTVarGlyphGlyf` that it contains a subclass of the old `_TTGlyph`.
We have some internal code that uses type hints and is typechecked using pytype, which breaks with the latest fonttools 4.36.0 following the above change.

```
bad option 'fontTools.ttLib.ttGlyphSet._TTVarGlyphGlyf' in return type [bad-return-type]
           Expected: fontTools.ttLib.ttGlyphSet._TTGlyph
```

I think we should revise the class hierarchy of these glyphset/glyph objects and make sure that we continue to return a `_TTGlyphSet` that contains generic `_TTGlyph` objects and work around their respective differences inside subclasses.
I think it's doable.


Discussion:

Patch:
diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py
index 93d9769cf7..cfdae54297 100644
--- a/Lib/fontTools/ttLib/ttGlyphSet.py
+++ b/Lib/fontTools/ttLib/ttGlyphSet.py
@@ -107,19 +107,22 @@ def drawPoints(self, pen):
 
 
 
-class _TTVarGlyphSet(object):
+class _TTVarGlyphSet(_TTGlyphSet):
 
 	def __init__(self, font, location, normalized=False):
-		from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap
 		self._ttFont = font
+		self._glyphs = font['glyf']
+
 		if not normalized:
+			from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap
+
 			axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in font['fvar'].axes}
 			location = normalizeLocation(location, axes)
 			if 'avar' in font:
 				avar = font['avar']
 				avarSegments = avar.segments
 				new_location = {}
-				for axis_tag,value in location.items():
+				for axis_tag, value in location.items():
 					avarMapping = avarSegments.get(axis_tag, None)
 					if avarMapping is not None:
 						value = piecewiseLinearMap(value, avarMapping)
@@ -129,21 +132,11 @@ def __init__(self, font, location, normalized=False):
 
 		self.location = location
 
-	def keys(self):
-		return list(self._ttFont['glyf'].keys())
-
-	def has_key(self, glyphName):
-		return glyphName in self._ttFont['glyf']
-	__contains__ = has_key
-
 	def __getitem__(self, glyphName):
+		if glyphName not in self._glyphs:
+			raise KeyError(glyphName)
 		return _TTVarGlyphGlyf(self._ttFont, glyphName, self.location)
 
-	def get(self, glyphName, default=None):
-		try:
-			return self[glyphName]
-		except KeyError:
-			return default
 
 def _setCoordinates(glyph, coord, glyfTable):
 	# Handle phantom points for (left, right, top, bottom) positions.
@@ -174,16 +167,25 @@ def _setCoordinates(glyph, coord, glyfTable):
 	horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
 	verticalAdvanceWidth = otRound(topSideY - bottomSideY)
 	leftSideBearing = otRound(glyph.xMin - leftSideX)
-	return horizontalAdvanceWidth, leftSideBearing, verticalAdvanceWidth
+	topSideBearing = otRound(topSideY - glyph.yMax)
+	return (
+		horizontalAdvanceWidth,
+		leftSideBearing,
+		verticalAdvanceWidth,
+		topSideBearing,
+	)
 
 
-class _TTVarGlyphGlyf(object):
-
+class _TTVarGlyph(_TTGlyph):
 	def __init__(self, ttFont, glyphName, location):
 		self._ttFont = ttFont
 		self._glyphName = glyphName
 		self._location = location
-		self.width = None # draw fills it in
+		# draw() fills these in
+		self.width = self.height = self.lsb = self.tsb = None
+
+
+class _TTVarGlyphGlyf(_TTVarGlyph):
 
 	def draw(self, pen):
 		from fontTools.varLib.iup import iup_delta
@@ -210,8 +212,10 @@ def draw(self, pen):
 			coordinates += GlyphCoordinates(delta) * scalar
 
 		glyph = copy(glyf[self._glyphName]) # Shallow copy
-		horizontalAdvanceWidth, leftSideBearing, verticalAdvanceWidth = _setCoordinates(glyph, coordinates, glyf)
-		self.width = horizontalAdvanceWidth
-		self.height = verticalAdvanceWidth
-		offset = leftSideBearing - glyph.xMin if hasattr(glyph, "xMin") else 0
+		width, lsb, height, tsb = _setCoordinates(glyph, coordinates, glyf)
+		self.width = width
+		self.lsb = lsb
+		self.height = height
+		self.tsb = tsb
+		offset = lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
 		glyph.draw(pen, glyf, offset)


Test patch:
diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py
index d1fc80913b..bc0bf2ceb8 100644
--- a/Tests/ttLib/ttGlyphSet_test.py
+++ b/Tests/ttLib/ttGlyphSet_test.py
@@ -1,4 +1,5 @@
 from fontTools.ttLib import TTFont
+from fontTools.ttLib import ttGlyphSet
 from fontTools.pens.recordingPen import RecordingPen
 import os
 import pytest
@@ -14,6 +15,16 @@ def getpath(testfile):
     @pytest.mark.parametrize(
         "location, expected",
         [
+            (
+                None,
+                [
+                 ('moveTo', ((175, 0),)),
+                 ('lineTo', ((367, 0),)),
+                 ('lineTo', ((367, 1456),)),
+                 ('lineTo', ((175, 1456),)),
+                 ('closePath', ())
+                ]
+            ),
             (
                 {},
                 [
@@ -69,11 +80,32 @@ def getpath(testfile):
     def test_glyphset(
         self, location, expected
     ):
-
+        # TODO: also test loading CFF-flavored fonts
         font = TTFont(self.getpath("I.ttf"))
         glyphset = font.getGlyphSet(location=location)
+
+        assert isinstance(glyphset, ttGlyphSet._TTGlyphSet)
+        if location:
+            assert isinstance(glyphset, ttGlyphSet._TTVarGlyphSet)
+
+        assert list(glyphset.keys()) == [".notdef", "I"]
+
+        assert "I" in glyphset
+        assert glyphset.has_key("I")  # we should really get rid of this...
+
+        assert len(glyphset) == 2
+
         pen = RecordingPen()
         glyph = glyphset['I']
+
+        assert glyphset.get("foobar") is None
+
+        assert isinstance(glyph, ttGlyphSet._TTGlyph)
+        if location:
+            assert isinstance(glyph, ttGlyphSet._TTVarGlyphGlyf)
+        else:
+            assert isinstance(glyph, ttGlyphSet._TTGlyphGlyf)
+
         glyph.draw(pen)
         actual = pen.value