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

Issue:
[ttx] Flexible manual page install directory 
https://sourceforge.net/p/fonttools/bugs/51/

"""
The BSDs are patching setup.py during their build process so that they get the manual page installed in /usr/man instead of /usr/share/man. The setup.py script should either know where to install manual pages on BSD or have a --mandir argument that can be used to set the right directory for the manual page. 
"""



Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -3,8 +3,12 @@
 from __future__ import print_function
 import io
 import sys
+import os
+from os.path import isfile, join as pjoin
+from glob import glob
 from setuptools import setup, find_packages, Command
 from distutils import log
+from distutils.util import convert_path
 import subprocess as sp
 import contextlib
 
@@ -259,6 +263,49 @@ def run(self):
 		pass
 
 
+def find_data_files(manpath="share/man"):
+	""" Find FontTools's data_files (just man pages at this point).
+
+	By default, we install man pages to "share/man" directory relative to the
+	base installation directory for data_files. The latter can be changed with
+	the --install-data option of 'setup.py install' sub-command.
+
+	E.g., if the data files installation directory is "/usr", the default man
+	page installation directory will be "/usr/share/man".
+
+	You can override this via the $FONTTOOLS_MANPATH environment variable.
+
+	E.g., on some BSD systems man pages are installed to 'man' instead of
+	'share/man'; you can export $FONTTOOLS_MANPATH variable just before
+	installing:
+
+	$ FONTTOOLS_MANPATH="man" pip install -v .
+	    [...]
+	    running install_data
+	    copying Doc/man/ttx.1 -> /usr/man/man1
+
+	When installing from PyPI, for this variable to have effect you need to
+	force pip to install from the source distribution instead of the wheel
+	package (otherwise setup.py is not run), by using the --no-binary option:
+
+	$ FONTTOOLS_MANPATH="man" pip install --no-binary=fonttools fonttools
+
+	Note that you can only override the base man path, i.e. without the
+	section number (man1, man3, etc.). The latter is always implied to be 1,
+	for "general commands".
+	"""
+
+	# get base installation directory for man pages
+	manpagebase = os.environ.get('FONTTOOLS_MANPATH', convert_path(manpath))
+	# all our man pages go to section 1
+	manpagedir = pjoin(manpagebase, 'man1')
+
+	manpages = [f for f in glob(pjoin('Doc', 'man', 'man1', '*.1')) if isfile(f)]
+
+	data_files = [(manpagedir, manpages)]
+	return data_files
+
+
 setup(
 	name="fonttools",
 	version="3.5.0.dev0",
@@ -274,9 +321,7 @@ def run(self):
 	package_dir={'': 'Lib'},
 	packages=find_packages("Lib"),
 	include_package_data=True,
-	data_files=[
-		('share/man/man1', ["Doc/ttx.1"])
-	] if sys.platform.startswith('linux') else [],
+	data_files=find_data_files(),
 	setup_requires=pytest_runner + wheel + bumpversion,
 	tests_require=[
 		'pytest>=2.8',


Test patch:
Repository: fonttools/fonttools

Issue:
varLib.mutator: glyph processing order
https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/varLib/mutator.py#L42

`for glyphname,variations in gvar.variations.items():`

If a composite glyph is processed before its base glyph, the composite glyph bounds may be calculated incorrectly because the deltas haven’t been applied to the base glyph yet.

Nested composites could complicate this further ...


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
@@ -1,6 +1,7 @@
 """_g_l_y_f.py -- Converter classes for the 'glyf' table."""
 
 from __future__ import print_function, division, absolute_import
+from collections import namedtuple
 from fontTools.misc.py23 import *
 from fontTools.misc import sstruct
 from fontTools import ttLib
@@ -297,6 +298,9 @@ def flagEncodeCoords(flag, x, y, xBytes, yBytes):
 UNSCALED_COMPONENT_OFFSET	= 0x1000  # composite designed not to have the component offset scaled (designed for MS)
 
 
+CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours', 'maxComponentDepth'])
+
+
 class Glyph(object):
 
 	def __init__(self, data=""):
@@ -436,7 +440,7 @@ def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
 						glyfTable, maxComponentDepth + 1)
 			nPoints = nPoints + nP
 			nContours = nContours + nC
-		return nPoints, nContours, maxComponentDepth
+		return CompositeMaxpValues(nPoints, nContours, maxComponentDepth)
 
 	def getMaxpValues(self):
 		assert self.numberOfContours > 0
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
@@ -39,7 +39,16 @@ def main(args=None):
 	print("Normalized location:", loc)
 
 	gvar = varfont['gvar']
-	for glyphname,variations in gvar.variations.items():
+	glyf = varfont['glyf']
+	# get list of glyph names in gvar sorted by component depth
+	glyphnames = sorted(
+		gvar.variations.keys(),
+		key=lambda name: (
+			glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
+			if glyf[name].isComposite() else 0,
+			name))
+	for glyphname in glyphnames:
+		variations = gvar.variations[glyphname]
 		coordinates,_ = _GetCoordinates(varfont, glyphname)
 		for var in variations:
 			scalar = supportScalar(loc, var.axes)


Test patch:
Repository: fonttools/fonttools

Issue:
GlyphCoordinates loses precision using array('f') instead of array('d')
In a commit from 2013 (f2c2b4d38bd7bba23db71936262db984e4b7aebb) @behdad extended the `GlyphCoordinates` class in the `_g_l_y_f` table module to transparently support float coordinataes.

The GlyphCoordinates class wraps an `array.array` object, containing a single two-dimensional array of x and y coordinates. The type code used is `'f'`, which in C corresponds to `float` type, which uses maximum 4 bytes. The python floats are `double` precision floats (max 8 bytes), and are represented with the type code `'d'` in the array.array type code.

https://docs.python.org/3/library/array.html

Using `'f'` instead of `'d'` to store float coordinates means that we risk truncating a double into a float and thus we get the wrong results when we call `round` (e.g. in `GlyphCoordintes.toInt()` method):

```python
>>> from fontTools.misc.py23 import round
>>> from array import array
>>> afloat = array('f', [242.50000000000003])
>>> afloat
array('f', [242.5])
>>> adouble = array('d', [242.50000000000003])
>>> adouble
array('d', [242.50000000000003])
>>> round(afloat[0])
242
>>> round(adouble[0])
243
```

This problem is affecting all fontmake generated TTFs. The fonttools' `TTGlyphPen` receives floats from ufo2ft (and cu2qu) and does not do any rounding; it lets the glyf module do the rounding, but this may produce incorrect results because of the above mentioned double-to-float truncation.

I see two way to fix this:

1) we use doubles (type code `'d'`) instead of floats (`'f'`) in the `GlyphCoordinates` array;
2) we call `round` in the TTGlyphPen before we create `GlyphCoordinates` instances.

/cc @behdad 


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
@@ -1170,13 +1170,13 @@ def array(self):
 		return self._a
 
 	def isFloat(self):
-		return self._a.typecode == 'f'
+		return self._a.typecode == 'd'
 
 	def _ensureFloat(self):
 		if self.isFloat():
 			return
 		# The conversion to list() is to work around Jython bug
-		self._a = array.array("f", list(self._a))
+		self._a = array.array("d", list(self._a))
 
 	def _checkFloat(self, p):
 		if self.isFloat():


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
@@ -143,3 +143,13 @@ def test__bool__(self):
         assert bool(g) == True
         g = GlyphCoordinates([(0,.5), (0,0)])
         assert bool(g) == True
+
+    def test_double_precision_float(self):
+        # https://github.com/fonttools/fonttools/issues/963
+        afloat = 242.50000000000003
+        g = GlyphCoordinates([(afloat, 0)])
+        g.toInt()
+        # this would return 242 if the internal array.array typecode is 'f',
+        # since the Python float is truncated to a C float.
+        # when using typecode 'd' it should return the correct value 243
+        assert g[0][0] == round(afloat)
Repository: fonttools/fonttools

Issue:
[varLib] cpsp feature is not variable
In the attached demo build folder, the master fonts contain 1 kern pair and 1 cpsp lookup. When building a variable font with varLib, the kerning pair becomes variable, but the cpsp value is static.

The cpsp values in the wdth -1 master has XPlacement = 1, XAdvance = 2, and in the wdth 0 master XPlacement = 40, XAdvance = 80. The variable font only contains the 40/80 values.

More precisely, it looks like there are entries for the 40 and 80 in the variation item store in GDEF (item index 0 and 1), but they are not used in the cpsp lookup (Lookup Index 0).

[Variable Test cpsp.zip](https://github.com/fonttools/fonttools/files/1124294/Variable.Test.cpsp.zip)



Discussion:

Resolving patch:
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -202,6 +202,7 @@ def merge(merger, self, lst):
 	if all(v.Format == 1 for v in lst) and all(self.Coverage.glyphs == v.Coverage.glyphs for v in lst):
 		self.Value = otBase.ValueRecord(valueFormat)
 		merger.mergeThings(self.Value, [v.Value for v in lst])
+		self.ValueFormat = self.Value.getFormat()
 		return
 
 	# Upgrade everything to Format=2
@@ -231,6 +232,7 @@ def merge(merger, self, lst):
 	# Merge everything else; though, there shouldn't be anything else. :)
 	merger.mergeObjects(self, lst,
 			    exclude=('Format', 'Coverage', 'ValueRecord', 'Value', 'ValueCount'))
+	self.ValueFormat = reduce(int.__or__, [v.getFormat() for v in self.Value], 0)
 
 @AligningMerger.merger(ot.PairSet)
 def merge(merger, self, lst):


Test patch:
Repository: fonttools/fonttools

Issue:
[ttFont] xmlWriter import error with saveXML split tables = True requests
Attempt to dump split OT tables with ttx, executed in Python 3 venv with PyPI release of fontTools:


```
$ ttx -s -d regular-ttx Hack-Regular.ttf
Dumping "Hack-Regular.ttf" to "regular-ttx/Hack-Regular.ttx"...
ERROR: Unhandled exception has occurred
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/fontTools/ttx.py", line 389, in main
    process(jobs, options)
  File "/usr/local/lib/python3.6/site-packages/fontTools/ttx.py", line 363, in process
    action(input, output, options)
  File "/usr/local/lib/python3.6/site-packages/fontTools/misc/loggingTools.py", line 372, in wrapper
    return func(*args, **kwds)
  File "/usr/local/lib/python3.6/site-packages/fontTools/ttx.py", line 260, in ttDump
    newlinestr=options.newlinestr)
  File "/usr/local/lib/python3.6/site-packages/fontTools/ttLib/ttFont.py", line 221, in saveXML
    self._saveXML(writer, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/fontTools/ttLib/ttFont.py", line 264, in _saveXML
    tableWriter = xmlWriter.XMLWriter(tablePath,
NameError: name 'xmlWriter' is not defined
```

PR for fix incoming...


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
@@ -1,4 +1,5 @@
 from __future__ import print_function, division, absolute_import
+from fontTools.misc import xmlWriter
 from fontTools.misc.py23 import *
 from fontTools.misc.loggingTools import deprecateArgument
 from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
@@ -216,7 +217,6 @@ def saveXML(self, fileOrPath, newlinestr=None, **kwargs):
 		to skip, but only when the 'tables' argument is false.
 		"""
 
-		from fontTools.misc import xmlWriter
 		writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
 		self._saveXML(writer, **kwargs)
 		writer.close()


Test patch:
Repository: fonttools/fonttools

Issue:
Change ufoLib creator value?
https://github.com/fonttools/fonttools/blob/13cf8ef1ab2184f6d8dd4e475f1032b413c4ae26/Lib/fontTools/ufoLib/__init__.py#L789

Should this change to `org.fonttools.ufolib`?


Discussion:
hm, yeah I thought about that. Problem is, we don't own `org.fonttools` unfortunately.
For similar reverse domain UFO keys in the past we used `"com.github.fonttools.*"`, for example in ufo2ft for the data files in TTX format
Ah, yeah, that then. Would make sense to be sure that one can know where the UFOLib is coming from. I'll work up a PR.

Resolving patch:
diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py
--- a/Lib/fontTools/ufoLib/__init__.py
+++ b/Lib/fontTools/ufoLib/__init__.py
@@ -786,7 +786,7 @@ def __init__(
 		self,
 		path,
 		formatVersion=3,
-		fileCreator="org.robofab.ufoLib",
+		fileCreator="com.github.fonttools.ufoLib",
 		structure=None,
 		validate=True,
 	):


Test patch:
Repository: fonttools/fonttools

Issue:
DeprecationWarning: invalid escape sequence 
the rest is nicely escaped with `r'...'`

https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/ttLib/tables/ttProgram.py#L198-L199


DeprecationWarning: invalid escape sequence 
the rest is nicely escaped with `r'...'`

https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/ttLib/tables/ttProgram.py#L198-L199




Discussion:

Resolving patch:
diff --git a/Lib/fontTools/ttLib/tables/ttProgram.py b/Lib/fontTools/ttLib/tables/ttProgram.py
--- a/Lib/fontTools/ttLib/tables/ttProgram.py
+++ b/Lib/fontTools/ttLib/tables/ttProgram.py
@@ -159,7 +159,7 @@ def bitRepr(value, bits):
 	return s
 
 
-_mnemonicPat = re.compile("[A-Z][A-Z0-9]*$")
+_mnemonicPat = re.compile(r"[A-Z][A-Z0-9]*$")
 
 def _makeDict(instructionList):
 	opcodeDict = {}
@@ -195,8 +195,8 @@ def __str__(self):
 
 _pushCountPat = re.compile(r"[A-Z][A-Z0-9]*\s*\[.*?\]\s*/\* ([0-9]+).*?\*/")
 
-_indentRE = re.compile("^FDEF|IF|ELSE\[ \]\t.+")
-_unindentRE = re.compile("^ELSE|ENDF|EIF\[ \]\t.+")
+_indentRE = re.compile(r"^FDEF|IF|ELSE\[ \]\t.+")
+_unindentRE = re.compile(r"^ELSE|ENDF|EIF\[ \]\t.+")
 
 def _skipWhite(data, pos):
 	m = _whiteRE.match(data, pos)


Test patch:
Repository: fonttools/fonttools

Issue:
ufoLib.glifLib [TypeError: Expected a path string or fs object, found tuple]
I was saving a normalized ufo using `fontParts.fontshell.font` RFont, where I got the following error:

```Traceback (most recent call last):
  File "user/Desktop/test2.py", line 21, in <module>
    f.save()
  File "user/Github/fontParts/Lib/fontParts/base/font.py", line 203, in save
    formatVersion=formatVersion)
  File "user/Github/fontParts/Lib/fontParts/fontshell/font.py", line 49, in _save
    self.naked().save(path=path, formatVersion=formatVersion)
  File "/usr/local/lib/python3.7/site-packages/defcon/objects/font.py", line 862, in save
    self.layers.save(writer, saveAs=saveAs, progressBar=progressBar)
  File "/usr/local/lib/python3.7/site-packages/defcon/objects/layerSet.py", line 321, in save
    glyphSet = writer.getGlyphSet(layerName=None, defaultLayer=True, validateRead=self.ufoLibReadValidate, validateWrite=self.ufoLibWriteValidate)
  File "/usr/local/lib/python3.7/site-packages/fontTools/ufoLib/__init__.py", line 1383, in getGlyphSet
    return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
  File "/usr/local/lib/python3.7/site-packages/fontTools/ufoLib/__init__.py", line 1400, in _getGlyphSetFormatVersion1
    validateWrite=validateWrite,
  File "/usr/local/lib/python3.7/site-packages/fontTools/ufoLib/glifLib.py", line 148, in __init__
    % type(path).__name__
TypeError: Expected a path string or fs object, found tuple
```


Discussion:

Resolving patch:
diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py
--- a/Lib/fontTools/ufoLib/__init__.py
+++ b/Lib/fontTools/ufoLib/__init__.py
@@ -1391,7 +1391,7 @@ def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc
 	def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
 		from fontTools.ufoLib.glifLib import GlyphSet
 
-		glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True),
+		glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
 		return GlyphSet(
 			glyphSubFS,
 			glyphNameToFileNameFunc=glyphNameToFileNameFunc,


Test patch:
Repository: fonttools/fonttools

Issue:
ascender and ascent
The [opentype spec ](https://docs.microsoft.com/en-gb/typography/opentype/spec/hhea) calls the first two substantive entries in the `hhea` table "`ascender`" and "`descender`". fonttools calls them "`ascent`" and "`descent`".

This was surprising! Maybe it's too late to change then but can we at least have an alias?


Discussion:
The spec changed at some point, and fonttools didn't change with it. 

A pr for aliases would be good. 

Resolving patch:
diff --git a/Lib/fontTools/ttLib/tables/_h_h_e_a.py b/Lib/fontTools/ttLib/tables/_h_h_e_a.py
--- a/Lib/fontTools/ttLib/tables/_h_h_e_a.py
+++ b/Lib/fontTools/ttLib/tables/_h_h_e_a.py
@@ -35,6 +35,19 @@ class table__h_h_e_a(DefaultTable.DefaultTable):
 
 	dependencies = ['hmtx', 'glyf', 'CFF ']
 
+	# OpenType spec renamed these, add aliases for compatibility
+	@property
+	def ascender(self): return self.ascent
+
+	@ascender.setter
+	def ascender(self,value): self.ascent = value
+
+	@property
+	def descender(self): return self.descent
+
+	@descender.setter
+	def descender(self,value): self.descent = value
+
 	def decompile(self, data, ttFont):
 		sstruct.unpack(hheaFormat, data, self)
 


Test patch:
diff --git a/Tests/ttLib/tables/_h_h_e_a_test.py b/Tests/ttLib/tables/_h_h_e_a_test.py
--- a/Tests/ttLib/tables/_h_h_e_a_test.py
+++ b/Tests/ttLib/tables/_h_h_e_a_test.py
@@ -126,6 +126,13 @@ def test_toXML_version_as_float(self):
             len([r for r in captor.records
                  if "Table version value is a float" in r.msg]) == 1)
 
+    def test_aliases(self):
+        hhea = self.font['hhea']
+        self.assertEqual(hhea.ascent, hhea.ascender)
+        self.assertEqual(hhea.descent, hhea.descender)
+        hhea.ascender = 800
+        self.assertEqual(hhea.ascent, 800)
+        hhea.ascent = 750
 
 class HheaDecompileOrFromXMLTest(unittest.TestCase):
Repository: fonttools/fonttools

Issue:
[ufoLib/glifLib] .glif parser does not accept XML comments
I just noticed that if a `.glif` file contains an XML comment, the data is rejected by `ufoLib.glifLib`. The following snippet causes an error:

```python
from fontTools.ufoLib.glifLib import readGlyphFromString

s = """<?xml version='1.0' encoding='UTF-8'?>
<glyph name="A" format="2">
  <advance width="1290"/>
  <unicode hex="0041"/>
  <!-- a comment -->
</glyph>
"""

class Glyph:
    pass

g = Glyph()
readGlyphFromString(s, g)
```
Exception:
`fontTools.ufoLib.errors.GlifLibError: Unknown element in GLIF: <!-- a comment -->`

I bet this is unintentional.

(Note that I don't really mind this behavior, I just wanted it to be recorded.)


Discussion:
I think we should let them through.
I believe this only happens when using lxml. The built-in etree doesn't parse the comments.
With lxml one can check if an element is a comment by doing `element.tag is etree.Comment`.
or we can use custom `XMLParser(remove_comments=True)` as documented in https://lxml.de/parsing.html -- but this is lxml specific I think

Resolving patch:
diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py
--- a/Lib/fontTools/ufoLib/glifLib.py
+++ b/Lib/fontTools/ufoLib/glifLib.py
@@ -852,7 +852,11 @@ def validateLayerInfoVersion3Data(infoData):
 # -----------------
 
 def _glifTreeFromFile(aFile):
-	root = etree.parse(aFile).getroot()
+	if etree._have_lxml:
+		tree = etree.parse(aFile, parser=etree.XMLParser(remove_comments=True))
+	else:
+		tree = etree.parse(aFile)
+	root = tree.getroot()
 	if root.tag != "glyph":
 		raise GlifLibError("The GLIF is not properly formatted.")
 	if root.text and root.text.strip() != '':
@@ -862,7 +866,10 @@ def _glifTreeFromFile(aFile):
 
 def _glifTreeFromString(aString):
 	data = tobytes(aString, encoding="utf-8")
-	root = etree.fromstring(data)
+	if etree._have_lxml:
+		root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True))
+	else:
+		root = etree.fromstring(data)
 	if root.tag != "glyph":
 		raise GlifLibError("The GLIF is not properly formatted.")
 	if root.text and root.text.strip() != '':


Test patch:
diff --git a/Tests/ufoLib/glifLib_test.py b/Tests/ufoLib/glifLib_test.py
--- a/Tests/ufoLib/glifLib_test.py
+++ b/Tests/ufoLib/glifLib_test.py
@@ -161,3 +161,21 @@ def testRoundTrip(self):
 	def testXmlDeclaration(self):
 		s = writeGlyphToString("a", _Glyph())
 		self.assertTrue(s.startswith(XML_DECLARATION % "UTF-8"))
+
+
+def test_parse_xml_remove_comments():
+	s = b"""<?xml version='1.0' encoding='UTF-8'?>
+	<!-- a comment -->
+	<glyph name="A" format="2">
+		<advance width="1290"/>
+		<unicode hex="0041"/>
+		<!-- another comment -->
+	</glyph>
+	"""
+
+	g = _Glyph()
+	readGlyphFromString(s, g)
+
+	assert g.name == "A"
+	assert g.width == 1290
+	assert g.unicodes == [0x0041]
Repository: fonttools/fonttools

Issue:
[feaLib] MultipleSubstStatement docstring mistake
Here `glyph` seems to actually expect a single `glyph-containing object` instead of a list?

https://github.com/fonttools/fonttools/blob/73631c9642235e55bbf31eab1fc2e9f156b62ec2/Lib/fontTools/feaLib/ast.py#L1119-L1156


Discussion:

Resolving patch:
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -1119,11 +1119,14 @@ def asFea(self, indent=""):
 class MultipleSubstStatement(Statement):
     """A multiple substitution statement.
 
-    ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
-    `glyph-containing objects`_.
-
-    If ``forceChain`` is True, this is expressed as a chaining rule
-    (e.g. ``sub f' i' by f_i``) even when no context is given."""
+    Args:
+        prefix: a list of `glyph-containing objects`_.
+        glyph: a single glyph-containing object.
+        suffix: a list of glyph-containing objects.
+        replacement: a list of glyph-containing objects.
+        forceChain: If true, the statement is expressed as a chaining rule
+            (e.g. ``sub f' i' by f_i``) even when no context is given.
+    """
     def __init__(
         self, prefix, glyph, suffix, replacement, forceChain=False, location=None
     ):


Test patch: