aboutsummaryrefslogtreecommitdiff
path: root/Tests/feaLib/builder_test.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tests/feaLib/builder_test.py')
-rw-r--r--Tests/feaLib/builder_test.py531
1 files changed, 334 insertions, 197 deletions
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 0a55239c..5c298e85 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -1,11 +1,15 @@
from fontTools.misc.loggingTools import CapturingLogHandler
-from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \
- addOpenTypeFeaturesFromString
+from fontTools.feaLib.builder import (
+ Builder,
+ addOpenTypeFeatures,
+ addOpenTypeFeaturesFromString,
+)
from fontTools.feaLib.error import FeatureLibError
-from fontTools.ttLib import TTFont
+from fontTools.ttLib import TTFont, newTable
from fontTools.feaLib.parser import Parser
from fontTools.feaLib import ast
from fontTools.feaLib.lexer import Lexer
+from fontTools.fontBuilder import addFvar
import difflib
from io import StringIO
import os
@@ -65,7 +69,7 @@ class BuilderTest(unittest.TestCase):
spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g
spec10
bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509
- bug512 bug514 bug568 bug633 bug1307 bug1459
+ bug512 bug514 bug568 bug633 bug1307 bug1459 bug2276
name size size2 multiple_feature_blocks omitted_GlyphClassDef
ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
@@ -75,8 +79,14 @@ class BuilderTest(unittest.TestCase):
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
+ variable_scalar_valuerecord variable_scalar_anchor variable_conditionset
""".split()
+ VARFONT_AXES = [
+ ("wght", 200, 200, 1000, "Weight"),
+ ("wdth", 100, 100, 200, "Width"),
+ ]
+
def __init__(self, methodName):
unittest.TestCase.__init__(self, methodName)
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
@@ -101,25 +111,36 @@ class BuilderTest(unittest.TestCase):
if not self.tempdir:
self.tempdir = tempfile.mkdtemp()
self.num_tempfiles += 1
- return os.path.join(self.tempdir,
- "tmp%d%s" % (self.num_tempfiles, suffix))
+ return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
def read_ttx(self, path):
lines = []
with open(path, "r", encoding="utf-8") as ttx:
for line in ttx.readlines():
- # Elide ttFont attributes because ttLibVersion may change,
- # and use os-native line separators so we can run difflib.
+ # Elide ttFont attributes because ttLibVersion may change.
if line.startswith("<ttFont "):
- lines.append("<ttFont>" + os.linesep)
+ lines.append("<ttFont>\n")
else:
- lines.append(line.rstrip() + os.linesep)
+ lines.append(line.rstrip() + "\n")
return lines
def expect_ttx(self, font, expected_ttx, replace=None):
path = self.temp_path(suffix=".ttx")
- font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB',
- 'GPOS', 'OS/2', 'STAT', 'hhea', 'vhea'])
+ font.saveXML(
+ path,
+ tables=[
+ "head",
+ "name",
+ "BASE",
+ "GDEF",
+ "GSUB",
+ "GPOS",
+ "OS/2",
+ "STAT",
+ "hhea",
+ "vhea",
+ ],
+ )
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
if replace:
@@ -128,7 +149,8 @@ class BuilderTest(unittest.TestCase):
expected[i] = expected[i].replace(k, v)
if actual != expected:
for line in difflib.unified_diff(
- expected, actual, fromfile=expected_ttx, tofile=path):
+ expected, actual, fromfile=expected_ttx, tofile=path
+ ):
sys.stderr.write(line)
self.fail("TTX output is different from expected")
@@ -139,13 +161,17 @@ class BuilderTest(unittest.TestCase):
def check_feature_file(self, name):
font = makeTTFont()
+ if name.startswith("variable_"):
+ font["name"] = newTable("name")
+ addFvar(font, self.VARFONT_AXES, [])
+ del font["name"]
feapath = self.getpath("%s.fea" % name)
addOpenTypeFeatures(font, feapath)
self.expect_ttx(font, self.getpath("%s.ttx" % name))
# Check that:
# 1) tables do compile (only G* tables as long as we have a mock font)
# 2) dumping after save-reload yields the same TTX dump as before
- for tag in ('GDEF', 'GSUB', 'GPOS'):
+ for tag in ("GDEF", "GSUB", "GPOS"):
if tag in font:
data = font[tag].compile(font)
font[tag].decompile(data, font)
@@ -154,11 +180,11 @@ class BuilderTest(unittest.TestCase):
debugttx = self.getpath("%s-debug.ttx" % name)
if os.path.exists(debugttx):
addOpenTypeFeatures(font, feapath, debug=True)
- self.expect_ttx(font, debugttx, replace = {"__PATH__": feapath})
+ self.expect_ttx(font, debugttx, replace={"__PATH__": feapath})
def check_fea2fea_file(self, name, base=None, parser=Parser):
font = makeTTFont()
- fname = (name + ".fea") if '.' not in name else name
+ fname = (name + ".fea") if "." not in name else name
p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder())
doc = p.parse()
actual = self.normal_fea(doc.asFea().split("\n"))
@@ -168,12 +194,16 @@ class BuilderTest(unittest.TestCase):
if expected != actual:
fname = name.rsplit(".", 1)[0] + ".fea"
for line in difflib.unified_diff(
- expected, actual,
- fromfile=fname + " (expected)",
- tofile=fname + " (actual)"):
- sys.stderr.write(line+"\n")
- self.fail("Fea2Fea output is different from expected. "
- "Generated:\n{}\n".format("\n".join(actual)))
+ expected,
+ actual,
+ fromfile=fname + " (expected)",
+ tofile=fname + " (actual)",
+ ):
+ sys.stderr.write(line + "\n")
+ self.fail(
+ "Fea2Fea output is different from expected. "
+ "Generated:\n{}\n".format("\n".join(actual))
+ )
def normal_fea(self, lines):
output = []
@@ -198,13 +228,14 @@ class BuilderTest(unittest.TestCase):
def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
- "Already defined alternates for glyph \"A\"",
+ 'Already defined alternates for glyph "A"',
self.build,
"feature test {"
" sub A from [A.alt1 A.alt2];"
" sub B from [B.alt1 B.alt2 B.alt3];"
" sub A from [A.alt1 A.alt2];"
- "} test;")
+ "} test;",
+ )
def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
logger = logging.getLogger("fontTools.feaLib.builder")
@@ -214,19 +245,23 @@ class BuilderTest(unittest.TestCase):
" sub A by A.sc;"
" sub B by B.sc;"
" sub A by A.sc;"
- "} test;")
- captor.assertRegex('Removing duplicate single substitution from glyph "A" to "A.sc"')
+ "} test;"
+ )
+ captor.assertRegex(
+ 'Removing duplicate single substitution from glyph "A" to "A.sc"'
+ )
def test_multipleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
- "Already defined substitution for glyph \"f_f_i\"",
+ 'Already defined substitution for glyph "f_f_i"',
self.build,
"feature test {"
" sub f_f_i by f f i;"
" sub c_t by c t;"
" sub f_f_i by f_f i;"
- "} test;")
+ "} test;",
+ )
def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
logger = logging.getLogger("fontTools.feaLib.builder")
@@ -236,8 +271,11 @@ class BuilderTest(unittest.TestCase):
" sub f_f_i by f f i;"
" sub c_t by c t;"
" sub f_f_i by f f i;"
- "} test;")
- captor.assertRegex(r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)")
+ "} test;"
+ )
+ captor.assertRegex(
+ r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)"
+ )
def test_pairPos_redefinition_warning(self):
# https://github.com/fonttools/fonttools/issues/1147
@@ -251,17 +289,18 @@ class BuilderTest(unittest.TestCase):
" pos yacute semicolon -70;"
" enum pos @Y_LC semicolon -80;"
" pos @Y_LC @SMALL_PUNC -100;"
- "} kern;")
+ "} kern;"
+ )
captor.assertRegex("Already defined position for pair yacute semicolon")
# the first definition prevails: yacute semicolon -70
st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0]
self.assertEqual(st.Coverage.glyphs[2], "yacute")
- self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph,
- "semicolon")
- self.assertEqual(vars(st.PairSet[2].PairValueRecord[0].Value1),
- {"XAdvance": -70})
+ self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, "semicolon")
+ self.assertEqual(
+ vars(st.PairSet[2].PairValueRecord[0].Value1), {"XAdvance": -70}
+ )
def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
@@ -271,127 +310,153 @@ class BuilderTest(unittest.TestCase):
"feature test {"
" sub [a-z] by [A.sc-Z.sc];"
" sub e by e.fina;"
- "} test;")
+ "} test;",
+ )
def test_singlePos_redefinition(self):
self.assertRaisesRegex(
FeatureLibError,
- "Already defined different position for glyph \"A\"",
- self.build, "feature test { pos A 123; pos A 456; } test;")
+ 'Already defined different position for glyph "A"',
+ self.build,
+ "feature test { pos A 123; pos A 456; } test;",
+ )
def test_feature_outside_aalt(self):
self.assertRaisesRegex(
FeatureLibError,
'Feature references are only allowed inside "feature aalt"',
- self.build, "feature test { feature test; } test;")
+ self.build,
+ "feature test { feature test; } test;",
+ )
def test_feature_undefinedReference(self):
self.assertRaisesRegex(
- FeatureLibError, 'Feature none has not been defined',
- self.build, "feature aalt { feature none; } aalt;")
+ FeatureLibError,
+ "Feature none has not been defined",
+ self.build,
+ "feature aalt { feature none; } aalt;",
+ )
def test_GlyphClassDef_conflictingClasses(self):
self.assertRaisesRegex(
- FeatureLibError, "Glyph X was assigned to a different class",
+ FeatureLibError,
+ "Glyph X was assigned to a different class",
self.build,
"table GDEF {"
" GlyphClassDef [a b], [X], , ;"
" GlyphClassDef [a b X], , , ;"
- "} GDEF;")
+ "} GDEF;",
+ )
def test_languagesystem(self):
builder = Builder(makeTTFont(), (None, None))
- builder.add_language_system(None, 'latn', 'FRA')
- builder.add_language_system(None, 'cyrl', 'RUS')
- builder.start_feature(location=None, name='test')
- self.assertEqual(builder.language_systems,
- {('latn', 'FRA'), ('cyrl', 'RUS')})
+ builder.add_language_system(None, "latn", "FRA")
+ builder.add_language_system(None, "cyrl", "RUS")
+ builder.start_feature(location=None, name="test")
+ self.assertEqual(builder.language_systems, {("latn", "FRA"), ("cyrl", "RUS")})
def test_languagesystem_duplicate(self):
self.assertRaisesRegex(
FeatureLibError,
'"languagesystem cyrl RUS" has already been specified',
- self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;")
+ self.build,
+ "languagesystem cyrl RUS; languagesystem cyrl RUS;",
+ )
def test_languagesystem_none_specified(self):
builder = Builder(makeTTFont(), (None, None))
- builder.start_feature(location=None, name='test')
- self.assertEqual(builder.language_systems, {('DFLT', 'dflt')})
+ builder.start_feature(location=None, name="test")
+ self.assertEqual(builder.language_systems, {("DFLT", "dflt")})
def test_languagesystem_DFLT_dflt_not_first(self):
self.assertRaisesRegex(
FeatureLibError,
- "If \"languagesystem DFLT dflt\" is present, "
+ 'If "languagesystem DFLT dflt" is present, '
"it must be the first of the languagesystem statements",
- self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
+ self.build,
+ "languagesystem latn TRK; languagesystem DFLT dflt;",
+ )
def test_languagesystem_DFLT_not_preceding(self):
self.assertRaisesRegex(
FeatureLibError,
- "languagesystems using the \"DFLT\" script tag must "
+ 'languagesystems using the "DFLT" script tag must '
"precede all other languagesystems",
self.build,
"languagesystem DFLT dflt; "
"languagesystem latn dflt; "
- "languagesystem DFLT fooo; "
+ "languagesystem DFLT fooo; ",
)
def test_script(self):
builder = Builder(makeTTFont(), (None, None))
- builder.start_feature(location=None, name='test')
- builder.set_script(location=None, script='cyrl')
- self.assertEqual(builder.language_systems, {('cyrl', 'dflt')})
+ builder.start_feature(location=None, name="test")
+ builder.set_script(location=None, script="cyrl")
+ self.assertEqual(builder.language_systems, {("cyrl", "dflt")})
def test_script_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Script statements are not allowed within \"feature aalt\"",
- self.build, "feature aalt { script latn; } aalt;")
+ 'Script statements are not allowed within "feature aalt"',
+ self.build,
+ "feature aalt { script latn; } aalt;",
+ )
def test_script_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Script statements are not allowed within \"feature size\"",
- self.build, "feature size { script latn; } size;")
+ 'Script statements are not allowed within "feature size"',
+ self.build,
+ "feature size { script latn; } size;",
+ )
def test_script_in_standalone_lookup(self):
self.assertRaisesRegex(
FeatureLibError,
"Script statements are not allowed within standalone lookup blocks",
- self.build, "lookup test { script latn; } test;")
+ self.build,
+ "lookup test { script latn; } test;",
+ )
def test_language(self):
builder = Builder(makeTTFont(), (None, None))
- builder.add_language_system(None, 'latn', 'FRA ')
- builder.start_feature(location=None, name='test')
- builder.set_script(location=None, script='cyrl')
- builder.set_language(location=None, language='RUS ',
- include_default=False, required=False)
- self.assertEqual(builder.language_systems, {('cyrl', 'RUS ')})
- builder.set_language(location=None, language='BGR ',
- include_default=True, required=False)
- self.assertEqual(builder.language_systems,
- {('cyrl', 'BGR ')})
- builder.start_feature(location=None, name='test2')
- self.assertEqual(builder.language_systems, {('latn', 'FRA ')})
+ builder.add_language_system(None, "latn", "FRA ")
+ builder.start_feature(location=None, name="test")
+ builder.set_script(location=None, script="cyrl")
+ builder.set_language(
+ location=None, language="RUS ", include_default=False, required=False
+ )
+ self.assertEqual(builder.language_systems, {("cyrl", "RUS ")})
+ builder.set_language(
+ location=None, language="BGR ", include_default=True, required=False
+ )
+ self.assertEqual(builder.language_systems, {("cyrl", "BGR ")})
+ builder.start_feature(location=None, name="test2")
+ self.assertEqual(builder.language_systems, {("latn", "FRA ")})
def test_language_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Language statements are not allowed within \"feature aalt\"",
- self.build, "feature aalt { language FRA; } aalt;")
+ 'Language statements are not allowed within "feature aalt"',
+ self.build,
+ "feature aalt { language FRA; } aalt;",
+ )
def test_language_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Language statements are not allowed within \"feature size\"",
- self.build, "feature size { language FRA; } size;")
+ 'Language statements are not allowed within "feature size"',
+ self.build,
+ "feature size { language FRA; } size;",
+ )
def test_language_in_standalone_lookup(self):
self.assertRaisesRegex(
FeatureLibError,
"Language statements are not allowed within standalone lookup blocks",
- self.build, "lookup test { language FRA; } test;")
+ self.build,
+ "lookup test { language FRA; } test;",
+ )
def test_language_required_duplicate(self):
self.assertRaisesRegex(
@@ -409,13 +474,16 @@ class BuilderTest(unittest.TestCase):
" script latn;"
" language FRA required;"
" substitute [a-z] by [A.sc-Z.sc];"
- "} test;")
+ "} test;",
+ )
def test_lookup_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
- "Lookup \"foo\" has already been defined",
- self.build, "lookup foo {} foo; lookup foo {} foo;")
+ 'Lookup "foo" has already been defined',
+ self.build,
+ "lookup foo {} foo; lookup foo {} foo;",
+ )
def test_lookup_multiple_flags(self):
self.assertRaisesRegex(
@@ -428,7 +496,8 @@ class BuilderTest(unittest.TestCase):
" sub f i by f_i;"
" lookupflag 2;"
" sub f f i by f_f_i;"
- "} foo;")
+ "} foo;",
+ )
def test_lookup_multiple_types(self):
self.assertRaisesRegex(
@@ -439,13 +508,16 @@ class BuilderTest(unittest.TestCase):
"lookup foo {"
" sub f f i by f_f_i;"
" sub A from [A.alt1 A.alt2];"
- "} foo;")
+ "} foo;",
+ )
def test_lookup_inside_feature_aalt(self):
self.assertRaisesRegex(
FeatureLibError,
"Lookup blocks cannot be placed inside 'aalt' features",
- self.build, "feature aalt {lookup L {} L;} aalt;")
+ self.build,
+ "feature aalt {lookup L {} L;} aalt;",
+ )
def test_chain_subst_refrences_GPOS_looup(self):
self.assertRaisesRegex(
@@ -455,7 +527,7 @@ class BuilderTest(unittest.TestCase):
"lookup dummy { pos a 50; } dummy;"
"feature test {"
" sub a' lookup dummy b;"
- "} test;"
+ "} test;",
)
def test_chain_pos_refrences_GSUB_looup(self):
@@ -466,203 +538,215 @@ class BuilderTest(unittest.TestCase):
"lookup dummy { sub a by A; } dummy;"
"feature test {"
" pos a' lookup dummy b;"
- "} test;"
+ "} test;",
)
def test_STAT_elidedfallbackname_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackName is already set.',
+ "ElidedFallbackName is already set.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
- ' ElidedFallbackNameID 256;'
- '} STAT;')
+ " ElidedFallbackNameID 256;"
+ "} STAT;",
+ )
def test_STAT_elidedfallbackname_set_twice(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackName is already set.',
+ "ElidedFallbackName is already set.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' ElidedFallbackName { name "Italic"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_elidedfallbacknameID_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackNameID is already set.',
+ "ElidedFallbackNameID is already set.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
- ' ElidedFallbackNameID 256;'
+ "} name;"
+ "table STAT {"
+ " ElidedFallbackNameID 256;"
' ElidedFallbackName { name "Roman"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_elidedfallbacknameID_not_in_name_table(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackNameID 256 points to a nameID that does not '
+ "ElidedFallbackNameID 256 points to a nameID that does not "
'exist in the "name" table',
self.build,
- 'table name {'
+ "table name {"
' nameid 257 "Roman"; '
- '} name;'
- 'table STAT {'
- ' ElidedFallbackNameID 256;'
+ "} name;"
+ "table STAT {"
+ " ElidedFallbackNameID 256;"
' DesignAxis opsz 1 { name "Optical Size"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "name"',
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { badtag "Optical Size"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_duplicate_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'DesignAxis already defined for tag "opsz".',
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis opsz 1 { name "Optical Size"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_design_axis_duplicate_order(self):
self.assertRaisesRegex(
FeatureLibError,
"DesignAxis already defined for axis number 0.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 0 { name "Width"; };'
- ' AxisValue {'
- ' location opsz 8;'
- ' location wdth 400;'
+ " AxisValue {"
+ " location opsz 8;"
+ " location wdth 400;"
' name "Caption";'
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_undefined_tag(self):
self.assertRaisesRegex(
FeatureLibError,
- 'DesignAxis not defined for wdth.',
+ "DesignAxis not defined for wdth.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
- ' AxisValue { '
- ' location wdth 125; '
+ " AxisValue { "
+ " location wdth 125; "
' name "Wide"; '
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_axis_value_format4(self):
self.assertRaisesRegex(
FeatureLibError,
- 'Axis tag wdth already defined.',
+ "Axis tag wdth already defined.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 1 { name "Width"; };'
' DesignAxis wght 2 { name "Weight"; };'
- ' AxisValue { '
- ' location opsz 8; '
- ' location wdth 125; '
- ' location wdth 125; '
- ' location wght 500; '
+ " AxisValue { "
+ " location opsz 8; "
+ " location wdth 125; "
+ " location wdth 125; "
+ " location wght 500; "
' name "Caption Medium Wide"; '
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_duplicate_axis_value_record(self):
# Test for Duplicate AxisValueRecords even when the definition order
# is different.
self.assertRaisesRegex(
FeatureLibError,
- 'An AxisValueRecord with these values is already defined.',
+ "An AxisValueRecord with these values is already defined.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 1 { name "Width"; };'
- ' AxisValue {'
- ' location opsz 8;'
- ' location wdth 400;'
+ " AxisValue {"
+ " location opsz 8;"
+ " location wdth 400;"
' name "Caption";'
- ' };'
- ' AxisValue {'
- ' location wdth 400;'
- ' location opsz 8;'
+ " };"
+ " AxisValue {"
+ " location wdth 400;"
+ " location opsz 8;"
' name "Caption";'
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_axis_value_missing_location(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "Axis location"',
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; '
- '};'
+ "};"
' DesignAxis opsz 0 { name "Optical Size"; };'
- ' AxisValue { '
+ " AxisValue { "
' name "Wide"; '
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_invalid_location_tag(self):
self.assertRaisesRegex(
FeatureLibError,
- 'Tags cannot be longer than 4 characters',
+ "Tags cannot be longer than 4 characters",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; '
' name 3 1 0x0411 "ローマン"; }; '
' DesignAxis width 0 { name "Width"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_extensions(self):
class ast_BaseClass(ast.MarkClass):
@@ -680,7 +764,9 @@ class BuilderTest(unittest.TestCase):
for bcd in self.base.markClass.definitions:
if res != "":
res += "\n{}".format(indent)
- res += "pos base {} {}".format(bcd.glyphs.asFea(), bcd.anchor.asFea())
+ res += "pos base {} {}".format(
+ bcd.glyphs.asFea(), bcd.anchor.asFea()
+ )
for m in self.marks:
res += " mark @{}".format(m.name)
res += ";"
@@ -693,6 +779,7 @@ class BuilderTest(unittest.TestCase):
class testAst(object):
MarkBasePosStatement = ast_MarkBasePosStatement
+
def __getattr__(self, name):
return getattr(ast, name)
@@ -703,8 +790,9 @@ class BuilderTest(unittest.TestCase):
if enumerated:
raise FeatureLibError(
'"enumerate" is not allowed with '
- 'mark-to-base attachment positioning',
- location)
+ "mark-to-base attachment positioning",
+ location,
+ )
base = self.parse_glyphclass_(accept_glyphname=True)
if self.next_token_ == "<":
marks = self.parse_anchor_marks_()
@@ -715,11 +803,10 @@ class BuilderTest(unittest.TestCase):
m = self.expect_markClass_reference_()
marks.append(m)
self.expect_symbol_(";")
- return self.ast.MarkBasePosStatement(base, marks,
- location=location)
+ return self.ast.MarkBasePosStatement(base, marks, location=location)
def parseBaseClass(self):
- if not hasattr(self.doc_, 'baseClasses'):
+ if not hasattr(self.doc_, "baseClasses"):
self.doc_.baseClasses = {}
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
@@ -731,37 +818,39 @@ class BuilderTest(unittest.TestCase):
baseClass = ast_BaseClass(name)
self.doc_.baseClasses[name] = baseClass
self.glyphclasses_.define(name, baseClass)
- bcdef = ast_BaseClassDefinition(baseClass, anchor, glyphs,
- location=location)
+ bcdef = ast_BaseClassDefinition(
+ baseClass, anchor, glyphs, location=location
+ )
baseClass.addDefinition(bcdef)
return bcdef
- extensions = {
- 'baseClass' : lambda s : s.parseBaseClass()
- }
+ extensions = {"baseClass": lambda s: s.parseBaseClass()}
ast = testAst()
self.check_fea2fea_file(
- "baseClass.feax", base="baseClass.fea", parser=testParser)
+ "baseClass.feax", base="baseClass.fea", parser=testParser
+ )
def test_markClass_same_glyph_redefined(self):
self.assertRaisesRegex(
FeatureLibError,
"Glyph acute already defined",
self.build,
- "markClass [acute] <anchor 350 0> @TOP_MARKS;"*2)
+ "markClass [acute] <anchor 350 0> @TOP_MARKS;" * 2,
+ )
def test_markClass_same_glyph_multiple_classes(self):
self.assertRaisesRegex(
FeatureLibError,
- 'Glyph uni0327 cannot be in both @ogonek and @cedilla',
+ "Glyph uni0327 cannot be in both @ogonek and @cedilla",
self.build,
"feature mark {"
" markClass [uni0327 uni0328] <anchor 0 0> @ogonek;"
" pos base [a] <anchor 399 0> mark @ogonek;"
" markClass [uni0327] <anchor 0 0> @cedilla;"
" pos base [a] <anchor 244 0> mark @cedilla;"
- "} mark;")
+ "} mark;",
+ )
def test_build_specific_tables(self):
features = "feature liga {sub f i by f_i;} liga;"
@@ -783,7 +872,7 @@ class BuilderTest(unittest.TestCase):
def test_unsupported_subtable_break(self):
logger = logging.getLogger("fontTools.otlLib.builder")
- with CapturingLogHandler(logger, level='WARNING') as captor:
+ with CapturingLogHandler(logger, level="WARNING") as captor:
self.build(
"feature test {"
" pos a 10;"
@@ -814,10 +903,8 @@ class BuilderTest(unittest.TestCase):
FeatureLibError,
"Already defined different position for glyph",
self.build,
- "lookup foo {"
- " pos A -45; "
- " pos A 45; "
- "} foo;")
+ "lookup foo {" " pos A -45; " " pos A 45; " "} foo;",
+ )
def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self):
logger = logging.getLogger("fontTools.otlLib.builder")
@@ -826,8 +913,56 @@ class BuilderTest(unittest.TestCase):
"feature test {"
" enum pos A [V Y] -80;"
" pos A V -75;"
- "} test;")
- captor.assertRegex('Already defined position for pair A V at')
+ "} test;"
+ )
+ captor.assertRegex("Already defined position for pair A V at")
+
+ def test_ignore_empty_lookup_block(self):
+ # https://github.com/fonttools/fonttools/pull/2277
+ font = self.build(
+ "lookup EMPTY { ; } EMPTY;" "feature ss01 { lookup EMPTY; } ss01;"
+ )
+ assert "GPOS" not in font
+ assert "GSUB" not in font
+
+ def test_disable_empty_classes(self):
+ for test in [
+ "sub a by c []",
+ "sub f f [] by f",
+ "ignore sub a []'",
+ "ignore sub [] a'",
+ "sub a []' by b",
+ "sub [] a' by b",
+ "rsub [] by a",
+ "pos [] 120",
+ "pos a [] 120",
+ "enum pos a [] 120",
+ "pos cursive [] <anchor NULL> <anchor NULL>",
+ "pos base [] <anchor NULL> mark @TOPMARKS",
+ "pos ligature [] <anchor NULL> mark @TOPMARKS",
+ "pos mark [] <anchor NULL> mark @TOPMARKS",
+ "ignore pos a []'",
+ "ignore pos [] a'",
+ ]:
+ self.assertRaisesRegex(
+ FeatureLibError,
+ "Empty ",
+ self.build,
+ f"markClass a <anchor 150 -10> @TOPMARKS; lookup foo {{ {test}; }} foo;",
+ )
+ self.assertRaisesRegex(
+ FeatureLibError,
+ "Empty glyph class in mark class definition",
+ self.build,
+ "markClass [] <anchor 150 -10> @TOPMARKS;"
+ )
+ self.assertRaisesRegex(
+ FeatureLibError,
+ 'Expected a glyph class with 1 elements after "by", but found a glyph class with 0 elements',
+ self.build,
+ "feature test { sub a by []; test};"
+ )
+
def generate_feature_file_test(name):
@@ -835,8 +970,7 @@ def generate_feature_file_test(name):
for name in BuilderTest.TEST_FEATURE_FILES:
- setattr(BuilderTest, "test_FeatureFile_%s" % name,
- generate_feature_file_test(name))
+ setattr(BuilderTest, "test_FeatureFile_%s" % name, generate_feature_file_test(name))
def generate_fea2fea_file_test(name):
@@ -844,8 +978,11 @@ def generate_fea2fea_file_test(name):
for name in BuilderTest.TEST_FEATURE_FILES:
- setattr(BuilderTest, "test_Fea2feaFile_{}".format(name),
- generate_fea2fea_file_test(name))
+ setattr(
+ BuilderTest,
+ "test_Fea2feaFile_{}".format(name),
+ generate_fea2fea_file_test(name),
+ )
if __name__ == "__main__":