internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: fonttools/fonttools

Issue:
[ttGlyphSet] glyphSet + offset not passed correctly to glyf glyph
Reported here: https://github.com/adobe-type-tools/afdko/issues/1560
[ttGlyphSet] glyphSet + offset not passed correctly to glyf glyph
Reported here: https://github.com/adobe-type-tools/afdko/issues/1560


Discussion:

Resolving patch:
diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py
--- a/Lib/fontTools/ttLib/ttGlyphSet.py
+++ b/Lib/fontTools/ttLib/ttGlyphSet.py
@@ -126,14 +126,14 @@ def draw(self, pen):
         how that works.
         """
         glyph, offset = self._getGlyphAndOffset()
-        glyph.draw(pen, offset)
+        glyph.draw(pen, self.glyphSet.glyfTable, offset)
 
     def drawPoints(self, pen):
         """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details
         how that works.
         """
         glyph, offset = self._getGlyphAndOffset()
-        glyph.drawPoints(pen, offset)
+        glyph.drawPoints(pen, self.glyphSet.glyfTable, offset)
 
     def _getGlyphAndOffset(self):
         if self.glyphSet.location and self.glyphSet.gvarTable is not None:
@@ -176,12 +176,12 @@ def _getGlyphInstance(self):
 
         glyph = copy(glyfTable[self.name])  # Shallow copy
         width, lsb, height, tsb = _setCoordinates(glyph, coordinates, glyfTable)
+        self.lsb = lsb
+        self.tsb = tsb
         if glyphSet.hvarTable is None:
             # no HVAR: let's set metrics from the phantom points
             self.width = width
-            self.lsb = lsb
             self.height = height
-            self.tsb = tsb
         return glyph
 
 


Test patch:
diff --git a/Tests/ttLib/data/issue2824.ttf b/Tests/ttLib/data/issue2824.ttf
new file mode 100644
Binary files /dev/null and b/Tests/ttLib/data/issue2824.ttf differ
diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py
--- a/Tests/ttLib/ttGlyphSet_test.py
+++ b/Tests/ttLib/ttGlyphSet_test.py
@@ -112,6 +112,18 @@ def getpath(testfile):
                     ("closePath", ()),
                 ],
             ),
+            (
+                "issue2824.ttf",
+                None,
+                [
+                    ("moveTo", ((309, 180),)),
+                    ("qCurveTo", ((274, 151), (187, 136), (104, 166), (74, 201))),
+                    ("qCurveTo", ((45, 236), (30, 323), (59, 407), (95, 436))),
+                    ("qCurveTo", ((130, 466), (217, 480), (301, 451), (330, 415))),
+                    ("qCurveTo", ((360, 380), (374, 293), (345, 210), (309, 180))),
+                    ("closePath", ()),
+                ],
+            ),
         ],
     )
     def test_glyphset(self, fontfile, location, expected):
Repository: fonttools/fonttools

Issue:
Calling instantiateVariableFont can lead to negative advance widths in hmtx
Hi!

Calling `instantiateVariableFont` can introduce negative advance widths in the `hmtx` table:

https://github.com/fonttools/fonttools/blob/796c1f13303f0af515166e95bff1191f8d4e7f16/Lib/fontTools/varLib/mutator.py#L121-L149

We could probably replace line 146 by `entry[0] = max(0, entry[0] + width_delta)`. I can open a PR if you want.

(Carefully crafted fonts shouldn’t trigger this problem, but I’ve got it using a subset font where advance of unused glyphes has been set to 0.)


Discussion:
I think the varLib.instancer already takes care of that, can you use that instead of mutator? It's supposed to be a replacement of the latter, which we want to deprecate at some point.
We can backport the fix to mutator as well, I can take a look tomorrow 
> I think the varLib.instancer already takes care of that, can you use that instead of mutator?

I can’t, because I get `Instancing CFF2 variable fonts is not supported yet` (as you explained in #2681 😁️).
you're right

> We could probably replace line 146 by entry[0] = max(0, entry[0] + width_delta). I can open a PR if you want.

yes please, thank you

Resolving patch:
diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py
--- a/Lib/fontTools/varLib/mutator.py
+++ b/Lib/fontTools/varLib/mutator.py
@@ -143,7 +143,7 @@ def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
 
 		if lsb_delta or width_delta:
 			if width_delta:
-				entry[0] += width_delta
+				entry[0] = max(0, entry[0] + width_delta)
 			if lsb_delta:
 				entry[1] = lsb
 			hmtx[gname] = tuple(entry)


Test patch:
Repository: fonttools/fonttools

Issue:
[instancer] STAT: ElidedFallbackNameID is removed from 'name' table
When creating a static instance from a VF that has an `ElidedFallbackNameID` in the `STAT` table, the name record referred to by `ElidedFallbackNameID` is deleted, but the field is not cleared.

I'm not sure what the fix should be:
1. don't delete the `name` table entry
2. clear the `ElidedFallbackNameID` field


Discussion:
who deletes that name table entry? I can't find the code that's responsible for that
Good question, I'll try to find out.
maybe here's the culprit:

https://github.com/fonttools/fonttools/blob/995e5e254da23e54b42a096eb62ec71b7d242cb7/Lib/fontTools/varLib/instancer/names.py#L295
I'm not entirely sure what the fix should be either. What are the implications of 1) or 2)
Hm, I don't think that's the culprit.

I'm investigating further, there's something odd going on in our VF, where nameID 2 == "Regular", yet "ElidedFallbackNameID" is 382, and nameID 382 is *also* "Regular". But nameID 2 only exists as (3, 1), and 382 was added as (1, 0) and (3, 1). It's 382 that gets zapped somehow.
Oh! I think it's the pruningUnusedNames context manager defined in instancer.names module, which wraps calls to instantiateSTAT. The method calls getVariationNameIDs (also defined in instancer.names module) but for STAT table it does _not_ also considers that ElidedFallbackNameID can "use" a name ID. So it assumes it's unused and later it will be dropped on exit.
assuming we want to retain that ElidedFallbackNameID, then you should add it to the set of used nameIDs as returned from getVariationNameIDs.
Do we want to retain that? When does it make sense to drop it?
It for sure makes sense to drop once the resulting font is no longer variable. I would think it may need to be retained if the result is a sliced VF.
summoning STAT expert @belluzj 
in any case, the pruningUnunsedNames logic should be amended to take into account that the ElidedFallbackNameID may in fact be still used and not drop it accordingly. The instantiateSTAT method can decide when is appropriate to unset that ElidedFallbackNameID
I'm working on a fix in `getVariationNameIDs`, I just need to check whether `stat.ElidedFallbackNameID` is None or missing in version 1.0 STAT tables.
probably just missing

Resolving patch:
diff --git a/Lib/fontTools/varLib/instancer/names.py b/Lib/fontTools/varLib/instancer/names.py
--- a/Lib/fontTools/varLib/instancer/names.py
+++ b/Lib/fontTools/varLib/instancer/names.py
@@ -37,6 +37,9 @@ def getVariationNameIDs(varfont):
             used.append(axis.AxisNameID)
         for value in stat.AxisValueArray.AxisValue if stat.AxisValueArray else ():
             used.append(value.ValueNameID)
+        elidedFallbackNameID = getattr(stat, "ElidedFallbackNameID", None)
+        if elidedFallbackNameID is not None:
+            used.append(stat.ElidedFallbackNameID)
     # nameIDs <= 255 are reserved by OT spec so we don't touch them
     return {nameID for nameID in used if nameID > 255}
 


Test patch:
Repository: fonttools/fonttools

Issue:
font.getGlyphID() returns None for missing glyphs unless they start with `glyph`
Don't know how this went unnoticed but I found this while debugging the cryptic error message in https://github.com/fonttools/fonttools/issues/3028

Basically this function can fall off and return None by default:
```
    def getGlyphID(self, glyphName): 
        """Returns the ID of the glyph with the given name.""" 
        try: 
            print(glyphName) 
            return self.getReverseGlyphMap()[glyphName] 
        except KeyError: 
            if glyphName[:5] == "glyph": 
                try: 
                    return int(glyphName[5:]) 
                except (NameError, ValueError): 
                    raise KeyError(glyphName) 
```

It should `raise KeyError(glyphName)` instead.


Discussion:
Is this code related to virtual glyph IDs? There was also a probably related comment in `getGlyphName()` that was removed in https://github.com/fonttools/fonttools/commit/322a7b2ead069404e6fb278ab0a904999ab78bb9:
https://github.com/fonttools/fonttools/blob/49b6004040902e8b13f1aa7327769872bafa12a9/Lib/fontTools/ttLib/ttFont.py#L521-L528
yes, it looks like that one is the commit which introduced the bug.

> It should raise KeyError(glyphName) instead.

sounds good, I shall do that

Resolving patch:
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -660,6 +660,7 @@ def getGlyphID(self, glyphName):
                     return int(glyphName[5:])
                 except (NameError, ValueError):
                     raise KeyError(glyphName)
+            raise
 
     def getGlyphIDMany(self, lst):
         """Converts a list of glyph names into a list of glyph IDs."""


Test patch:
diff --git a/Tests/ttLib/ttFont_test.py b/Tests/ttLib/ttFont_test.py
--- a/Tests/ttLib/ttFont_test.py
+++ b/Tests/ttLib/ttFont_test.py
@@ -262,3 +262,15 @@ def test_font_normalizeLocation_no_VF():
     ttf = TTFont()
     with pytest.raises(TTLibError, match="Not a variable font"):
         ttf.normalizeLocation({})
+
+
+def test_getGlyphID():
+    font = TTFont()
+    font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx"))
+
+    assert font.getGlyphID("space") == 3
+    assert font.getGlyphID("glyph12345") == 12345  # virtual glyph
+    with pytest.raises(KeyError):
+        font.getGlyphID("non_existent")
+    with pytest.raises(KeyError):
+        font.getGlyphID("glyph_prefix_but_invalid_id")
Repository: fonttools/fonttools

Issue:
Running ttx with -s (split tables) option fails on url safe source filenames that contain '%' character
When attempting to run the ttx command with the `-s` (Split tables) option on font filenames that have been made url safe fails with an unhandled exception. 
This is caused by the source filenames containing the '%' character.

Example
```console
>ttx -s -i MaterialSymbolsOutlined%5BFILL,GRAD,opsz,wght%5D.ttf

Dumping "MaterialSymbolsOutlined%5BFILL,GRAD,opsz,wght%5D.ttf" to "MaterialSymbolsOutlined%5BFILL,GRAD,opsz,wght%5D.ttx"...
ERROR: Unhandled exception has occurred
Traceback (most recent call last):
  File "/home/tim/.local/lib/python3.10/site-packages/fontTools/ttx.py", line 454, in main
    process(jobs, options)
  File "/home/tim/.local/lib/python3.10/site-packages/fontTools/ttx.py", line 436, in process
    action(input, output, options)
  File "/home/tim/.local/lib/python3.10/site-packages/fontTools/misc/loggingTools.py", line 375, in wrapper
    return func(*args, **kwds)
  File "/home/tim/.local/lib/python3.10/site-packages/fontTools/ttx.py", line 297, in ttDump
    ttf.saveXML(
  File "/home/tim/.local/lib/python3.10/site-packages/fontTools/ttLib/ttFont.py", line 264, in saveXML
    self._saveXML(writer, **kwargs)
  File "/home/tim/.local/lib/python3.10/site-packages/fontTools/ttLib/ttFont.py", line 320, in _saveXML
    tablePath = fileNameTemplate % tagToIdentifier(tag)
ValueError: unsupported format character 'B' (0x42) at index 25
```



Discussion:

Resolving patch:
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -323,12 +323,11 @@ def _saveXML(
             writer.newline()
         else:
             path, ext = os.path.splitext(writer.filename)
-            fileNameTemplate = path + ".%s" + ext
 
         for i in range(numTables):
             tag = tables[i]
             if splitTables:
-                tablePath = fileNameTemplate % tagToIdentifier(tag)
+                tablePath = path + "." + tagToIdentifier(tag) + ext
                 tableWriter = xmlWriter.XMLWriter(
                     tablePath, newlinestr=writer.newlinestr
                 )


Test patch:
Repository: fonttools/fonttools

Issue:
fix a bug in dropImpliedOnCurvePoints
I found a bug in dropImpliedOnCurvePoints whereby we don't correctly drop impliable oncurve points if this occurs on the first point of the second contour.. because we forgot to update the contour start = last + 1...


Discussion:

Resolving patch:
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -1602,6 +1602,7 @@ def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
                     continue
 
                 may_drop.add(i)
+            start = last + 1
         # we only want to drop if ALL interpolatable glyphs have the same implied oncurves
         if drop is None:
             drop = may_drop


Test patch:
diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py
--- a/Tests/ttLib/tables/_g_l_y_f_test.py
+++ b/Tests/ttLib/tables/_g_l_y_f_test.py
@@ -856,7 +856,17 @@ def test_dropImpliedOnCurvePoints_all_quad_off_curves():
                 ((-1, -1), 0),
                 ((-1, 0), flagOnCurve),
                 ((-1, 1), 0),
-            ]
+            ],
+            [
+                ((0, 2), flagOnCurve),
+                ((2, 2), 0),
+                ((2, 0), flagOnCurve),
+                ((2, -2), 0),
+                ((0, -2), flagOnCurve),
+                ((-2, -2), 0),
+                ((-2, 0), flagOnCurve),
+                ((-2, 2), 0),
+            ],
         ],
         Transform().scale(2.0),
     )
@@ -864,12 +874,25 @@ def test_dropImpliedOnCurvePoints_all_quad_off_curves():
     glyph3 = Glyph()
     glyph3.numberOfContours = 0
 
-    assert dropImpliedOnCurvePoints(glyph1, glyph2, glyph3) == {0, 2, 4, 6}
+    assert dropImpliedOnCurvePoints(glyph1, glyph2, glyph3) == {
+        0,
+        2,
+        4,
+        6,
+        8,
+        10,
+        12,
+        14,
+    }
 
-    assert glyph1.flags == glyph2.flags == array.array("B", [0, 0, 0, 0])
-    assert glyph1.coordinates == GlyphCoordinates([(1, 1), (1, -1), (-1, -1), (-1, 1)])
-    assert glyph2.coordinates == GlyphCoordinates([(2, 2), (2, -2), (-2, -2), (-2, 2)])
-    assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [3]
+    assert glyph1.flags == glyph2.flags == array.array("B", [0, 0, 0, 0, 0, 0, 0, 0])
+    assert glyph1.coordinates == GlyphCoordinates(
+        [(1, 1), (1, -1), (-1, -1), (-1, 1), (2, 2), (2, -2), (-2, -2), (-2, 2)]
+    )
+    assert glyph2.coordinates == GlyphCoordinates(
+        [(2, 2), (2, -2), (-2, -2), (-2, 2), (4, 4), (4, -4), (-4, -4), (-4, 4)]
+    )
+    assert glyph1.endPtsOfContours == glyph2.endPtsOfContours == [3, 7]
     assert glyph3.numberOfContours == 0
Repository: fonttools/fonttools

Issue:
[FontBuilder] _add_avar() is called with the wrong amounts of arguments
`_add_avar()` was changed for `avar-2`, but the call site in fontBuilder.py wasn't updated, so calling `builder.setupAvar()` fails.

I'll post a PR.


Discussion:

Resolving patch:
diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py
--- a/Lib/fontTools/fontBuilder.py
+++ b/Lib/fontTools/fontBuilder.py
@@ -707,7 +707,7 @@ def setupFvar(self, axes, instances):
 
         addFvar(self.font, axes, instances)
 
-    def setupAvar(self, axes):
+    def setupAvar(self, axes, mappings=None):
         """Adds an axis variations table to the font.
 
         Args:
@@ -715,7 +715,12 @@ def setupAvar(self, axes):
         """
         from .varLib import _add_avar
 
-        _add_avar(self.font, OrderedDict(enumerate(axes)))  # Only values are used
+        if "fvar" not in self.font:
+            raise KeyError("'fvar' table is missing; can't add 'avar'.")
+
+        axisTags = [axis.axisTag for axis in self.font["fvar"].axes]
+        axes = OrderedDict(enumerate(axes))  # Only values are used
+        _add_avar(self.font, axes, mappings, axisTags)
 
     def setupGvar(self, variations):
         gvar = self.font["gvar"] = newTable("gvar")


Test patch:
diff --git a/Tests/fontBuilder/data/test_var.otf.ttx b/Tests/fontBuilder/data/test_var.otf.ttx
--- a/Tests/fontBuilder/data/test_var.otf.ttx
+++ b/Tests/fontBuilder/data/test_var.otf.ttx
@@ -273,6 +273,16 @@
     </GlobalSubrs>
   </CFF2>
 
+  <avar>
+    <version major="1" minor="0"/>
+    <segment axis="TEST">
+      <mapping from="-1.0" to="-1.0"/>
+      <mapping from="0.0" to="0.0"/>
+      <mapping from="0.4" to="0.6"/>
+      <mapping from="1.0" to="1.0"/>
+    </segment>
+  </avar>
+
   <fvar>
 
     <!-- Test Axis -->
diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py
--- a/Tests/fontBuilder/fontBuilder_test.py
+++ b/Tests/fontBuilder/fontBuilder_test.py
@@ -1,5 +1,6 @@
 import os
 import pytest
+from fontTools.designspaceLib import AxisDescriptor
 from fontTools.ttLib import TTFont
 from fontTools.pens.ttGlyphPen import TTGlyphPen
 from fontTools.pens.t2CharStringPen import T2CharStringPen
@@ -44,14 +45,20 @@ def _setupFontBuilder(isTTF, unitsPerEm=1024):
 def _setupFontBuilderFvar(fb):
     assert "name" in fb.font, "Must run setupNameTable() first."
 
-    axes = [
-        ("TEST", 0, 0, 100, "Test Axis"),
-    ]
+    testAxis = AxisDescriptor()
+    testAxis.name = "Test Axis"
+    testAxis.tag = "TEST"
+    testAxis.minimum = 0
+    testAxis.default = 0
+    testAxis.maximum = 100
+    testAxis.map = [(0, 0), (40, 60), (100, 100)]
+    axes = [testAxis]
     instances = [
         dict(location=dict(TEST=0), stylename="TotallyNormal"),
         dict(location=dict(TEST=100), stylename="TotallyTested"),
     ]
     fb.setupFvar(axes, instances)
+    fb.setupAvar(axes)
 
     return fb
Repository: fonttools/fonttools

Issue:
[designspaceLib] sources should not allow anisotropic location
This [diagram for the designspace format](https://fonttools.readthedocs.io/en/latest/_images/v5_class_diagram.png) indicates it is possible to allow anisotropic locations for sources.

![no_anisotropy_for_sources](https://github.com/fonttools/fonttools/assets/1243961/5582f335-6a33-4c77-ac01-55ff785c50d9)

- Mutatormath does not support anisotropic locations for **sources**. (it does for instances)
- It would surprise me if Varlib supports it (but love to hear)
- I can't think of any practical applications for this (but love to hear)
- If there are no strong arguments in favor, we could remove it from the requirements?




Discussion:
> If there are no strong arguments in favor, we could remove it from the requirements?

yes sure, you are the expert ;) I don't know how to update that diagram though. @belluzj can you help?
> It would surprise me if Varlib supports it (but love to hear)

no varLib doesn't support those 
Yes I discussed it with Erik by email, and I'll update the diagram. Just FYI for another time it's a PlantUML diagram using this tool: https://plantuml.com/class-diagram

The source code of the diagram is in the repo here: https://github.com/fonttools/fonttools/blob/main/Doc/source/designspaceLib/v5_class_diagram.puml

There's a nice VS Code extension with live preview of the PNG as you edit the code: https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml

Resolving patch:
diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py
--- a/Lib/fontTools/designspaceLib/__init__.py
+++ b/Lib/fontTools/designspaceLib/__init__.py
@@ -312,7 +312,7 @@ def location(self):
         return self.designLocation
 
     @location.setter
-    def location(self, location: Optional[AnisotropicLocationDict]):
+    def location(self, location: Optional[SimpleLocationDict]):
         self.designLocation = location or {}
 
     def setFamilyName(self, familyName, languageCode="en"):
@@ -329,15 +329,13 @@ def getFamilyName(self, languageCode="en"):
         """
         return self.localisedFamilyName.get(languageCode)
 
-    def getFullDesignLocation(
-        self, doc: "DesignSpaceDocument"
-    ) -> AnisotropicLocationDict:
+    def getFullDesignLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict:
         """Get the complete design location of this source, from its
         :attr:`designLocation` and the document's axis defaults.
 
         .. versionadded:: 5.0
         """
-        result: AnisotropicLocationDict = {}
+        result: SimpleLocationDict = {}
         for axis in doc.axes:
             if axis.name in self.designLocation:
                 result[axis.name] = self.designLocation[axis.name]


Test patch:
Repository: fonttools/fonttools

Issue:
varLib.interpolatable --json with scipy installed: Object of type int64 is not JSON serializable
Hello.

It seems that, the moment I install `scipy` into my project, `varLib.interpolatable` starts using it, and then fails outputting JSON with:

```shell
TypeError: ('Object of type int64 is not JSON serializable')
```

To clarify, the above happens when running the following with `scipy` installed:
```shell
fonttools varLib.interpolatable font1.ttf font2.ttf --json
```

I assume this may be due to some conditional `scipy` imports? E.g.:
https://github.com/fonttools/fonttools/blob/90fed08964bba2b81f0e1ec9222c95b0bee05384/Lib/fontTools/varLib/interpolatableHelpers.py#L174-L188
    
https://github.com/fonttools/fonttools/blob/90fed08964bba2b81f0e1ec9222c95b0bee05384/Lib/fontTools/varLib/interpolatableHelpers.py#L306-L308

Happy to dig deeper, and maybe provide a minimal reproduction, with some guidance on how to best supply that?

Thank you :)


Discussion:
I assume that the solution here would be, when `scipy` is installed, to supply a custom JSON encoder that can handle `numpy` types?

So here:
https://github.com/fonttools/fonttools/blob/90fed08964bba2b81f0e1ec9222c95b0bee05384/Lib/fontTools/varLib/interpolatable.py#L915

You could do something like...

```python
json_encoder = None
try:
    import scipy
    import numpy

    class NumpyEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, numpy.integer):
                return int(obj)
            if isinstance(obj, numpy.floating):
                return float(obj)
            if isinstance(obj, numpy.ndarray):
                return obj.tolist()
            return super(NumpyEncoder, self).default(obj)
   
    json_encoder = NumpyEncoder
except ImportError:
    pass

print(json.dumps(problems, cls=json_encoder), file=f) 
```
I can confirm that the above approach resolves the issue. Should I put together a PR?
I think we want to convert back to regular Python types or whatever it is that the non-scipy path is using. Thanks for the report!
Would be great if you can send a PR with the approach I suggested.
Ah, yes, that would keep all the `scipy`/`numpy` code contained to the conditional import blocks. Much tidier. Will set up a PR in the coming days, thank you.

Resolving patch:
diff --git a/Lib/fontTools/varLib/interpolatableHelpers.py b/Lib/fontTools/varLib/interpolatableHelpers.py
--- a/Lib/fontTools/varLib/interpolatableHelpers.py
+++ b/Lib/fontTools/varLib/interpolatableHelpers.py
@@ -143,6 +143,9 @@ def min_cost_perfect_bipartite_matching_scipy(G):
     n = len(G)
     rows, cols = linear_sum_assignment(G)
     assert (rows == list(range(n))).all()
+    # Convert numpy array and integer to Python types,
+    # to ensure that this is JSON-serializable.
+    cols = list(int(e) for e in cols)
     return list(cols), matching_cost(G, cols)
 
 


Test patch: