Repository: fonttools/fonttools

Issue:
[varLib] merging OTL fails if master TTFonts are loaded with lazy=True
If I populate my designspace source descriptors with TTFont() objects, similar to this:

```python
for source in ds.sources:
    source.font = TTFont(<ttfsource>, lazy=True)
```
I get an AssertionError deep down in `mergeThings()`:

```
    ttFont = varLib.build(self.doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT'])
../fonttools_justvanrossum/Lib/fontTools/varLib/__init__.py:880: in build
    _merge_OTL(vf, model, master_fonts, axisTags)
../fonttools_justvanrossum/Lib/fontTools/varLib/__init__.py:601: in _merge_OTL
    merger.mergeTables(font, master_fonts, ['GSUB', 'GDEF', 'GPOS'])
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:123: in mergeTables
    for m in master_ttfs])
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
```
```
E      AssertionError: (({'name': <'name' table at 12b0066d0>, 'fvar': <'fvar' table at 12b006610>, 'GPOS': <'GPOS' table at 11cc64850>}, [{'name': <'name' table at 12af841d0>, 'GPOS': <'GPOS' table at 12b00ffd0>}, {'GPOS': <'GPOS' table at 12b00fcd0>}, {'GPOS': <'GPOS' table at 12b019590>}, {'GPOS': <'GPOS' table at 12b019850>}]), 'dict', '.tables', 'TTFont', '.font', 'FeatureList', '.FeatureList', 'GPOS', '.table', 'table_G_P_O_S_')
```

This is because the `FeatureList` object has `font` and `reader` attributes that the merger ties to merge, but can't. When the font is not loaded in "lazy mode", those attrs don't exist and all is well.

I wonder if it can have anything to do with how `deepcopy()` works on the TTFont instance:
https://github.com/fonttools/fonttools/blob/a37dab38243d8c1e88ab83376a80942012bbab48/Lib/fontTools/varLib/__init__.py#L852

I don't have  a ready reproducer yet, will see if I can make one.
[varLib] merging OTL fails if master TTFonts are loaded with lazy=True
If I populate my designspace source descriptors with TTFont() objects, similar to this:

```python
for source in ds.sources:
    source.font = TTFont(<ttfsource>, lazy=True)
```
I get an AssertionError deep down in `mergeThings()`:

```
    ttFont = varLib.build(self.doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT'])
../fonttools_justvanrossum/Lib/fontTools/varLib/__init__.py:880: in build
    _merge_OTL(vf, model, master_fonts, axisTags)
../fonttools_justvanrossum/Lib/fontTools/varLib/__init__.py:601: in _merge_OTL
    merger.mergeTables(font, master_fonts, ['GSUB', 'GDEF', 'GPOS'])
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:123: in mergeTables
    for m in master_ttfs])
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:108: in mergeThings
    self.mergeObjects(out, lst)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:80: in mergeObjects
    mergerFunc(self, value, values)
../fonttools_justvanrossum/Lib/fontTools/varLib/merger.py:1013: in mergeThings
    super(VariationMerger, self).mergeThings(out, lst)
```
```
E      AssertionError: (({'name': <'name' table at 12b0066d0>, 'fvar': <'fvar' table at 12b006610>, 'GPOS': <'GPOS' table at 11cc64850>}, [{'name': <'name' table at 12af841d0>, 'GPOS': <'GPOS' table at 12b00ffd0>}, {'GPOS': <'GPOS' table at 12b00fcd0>}, {'GPOS': <'GPOS' table at 12b019590>}, {'GPOS': <'GPOS' table at 12b019850>}]), 'dict', '.tables', 'TTFont', '.font', 'FeatureList', '.FeatureList', 'GPOS', '.table', 'table_G_P_O_S_')
```

This is because the `FeatureList` object has `font` and `reader` attributes that the merger ties to merge, but can't. When the font is not loaded in "lazy mode", those attrs don't exist and all is well.

I wonder if it can have anything to do with how `deepcopy()` works on the TTFont instance:
https://github.com/fonttools/fonttools/blob/a37dab38243d8c1e88ab83376a80942012bbab48/Lib/fontTools/varLib/__init__.py#L852

I don't have  a ready reproducer yet, will see if I can make one.


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
@@ -60,6 +60,11 @@ def mergersFor(celf, thing, _default={}):
 		return _default
 
 	def mergeObjects(self, out, lst, exclude=()):
+		if hasattr(out, "ensureDecompiled"):
+			out.ensureDecompiled()
+		for item in lst:
+			if hasattr(item, "ensureDecompiled"):
+				item.ensureDecompiled()
 		keys = sorted(vars(out).keys())
 		assert all(keys == sorted(vars(v).keys()) for v in lst), \
 			(keys, [sorted(vars(v).keys()) for v in lst])


Test patch:
diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py
--- a/Tests/varLib/varLib_test.py
+++ b/Tests/varLib/varLib_test.py
@@ -478,6 +478,27 @@ def test_varlib_build_sparse_masters(self):
         tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
         self.expect_ttx(varfont, expected_ttx_path, tables)
 
+    def test_varlib_build_lazy_masters(self):
+        # See https://github.com/fonttools/fonttools/issues/1808
+        ds_path = self.get_test_input("SparseMasters.designspace")
+        expected_ttx_path = self.get_test_output("SparseMasters.ttx")
+
+        def _open_font(master_path, master_finder=lambda s: s):
+            font = TTFont()
+            font.importXML(master_path)
+            buf = BytesIO()
+            font.save(buf, reorderTables=False)
+            buf.seek(0)
+            font = TTFont(buf, lazy=True)  # reopen in lazy mode, to reproduce #1808
+            return font
+
+        ds = DesignSpaceDocument.fromfile(ds_path)
+        ds.loadSourceFonts(_open_font)
+        varfont, _, _ = build(ds)
+        varfont = reload_font(varfont)
+        tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
+        self.expect_ttx(varfont, expected_ttx_path, tables)
+
     def test_varlib_build_sparse_masters_MVAR(self):
         import fontTools.varLib.mvar