From a56b1af2f6ba0115480dd2898e202eb73e688fe8 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 9 Jun 2019 02:59:53 +0200 Subject: [PATCH 1/5] [subset] Gracefully handle partial MATH table (#1635) Both MathGlyphInfo and MathVariants can be None, so check for that first before trying to access their methods. --- Lib/fontTools/subset/__init__.py | 9 +- Tests/subset/data/expect_math_partial.ttx | 168 ++++++++++ Tests/subset/data/test_math_partial.ttx | 391 ++++++++++++++++++++++ Tests/subset/subset_test.py | 7 + 4 files changed, 572 insertions(+), 3 deletions(-) create mode 100644 Tests/subset/data/expect_math_partial.ttx create mode 100644 Tests/subset/data/test_math_partial.ttx diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 8cf82c122..6863bb6fc 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -1987,7 +1987,8 @@ def closure_glyphs(self, s): @_add_method(ttLib.getTableClass('MATH')) def closure_glyphs(self, s): - self.table.MathVariants.closure_glyphs(s) + if self.table.MathVariants: + self.table.MathVariants.closure_glyphs(s) @_add_method(otTables.MathItalicsCorrectionInfo) def subset_glyphs(self, s): @@ -2039,8 +2040,10 @@ def subset_glyphs(self, s): @_add_method(ttLib.getTableClass('MATH')) def subset_glyphs(self, s): s.glyphs = s.glyphs_mathed - self.table.MathGlyphInfo.subset_glyphs(s) - self.table.MathVariants.subset_glyphs(s) + if self.table.MathGlyphInfo: + self.table.MathGlyphInfo.subset_glyphs(s) + if self.table.MathVariants: + self.table.MathVariants.subset_glyphs(s) return True @_add_method(ttLib.getTableModule('glyf').Glyph) diff --git a/Tests/subset/data/expect_math_partial.ttx b/Tests/subset/data/expect_math_partial.ttx new file mode 100644 index 000000000..34136e96b --- /dev/null +++ b/Tests/subset/data/expect_math_partial.ttx @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/test_math_partial.ttx b/Tests/subset/data/test_math_partial.ttx new file mode 100644 index 000000000..c0a70da0a --- /dev/null +++ b/Tests/subset/data/test_math_partial.ttx @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + XITS Math + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 19 vlineto + -52 6 -14 21 -28 65 rrcurveto + -246 563 -20 0 -206 -488 rlineto + -59 -140 -9 -21 -58 -6 rrcurveto + -19 199 19 vlineto + -48 -22 10 31 hvcurveto + 0 12 4 17 5 13 rrcurveto + 46 114 262 0 41 -94 rlineto + 12 -28 7 -27 0 -15 0 -9 -6 -11 -8 -4 -12 -7 -7 -2 -36 0 rrcurveto + -19 vlineto + return + + + -231 0 115 275 rlineto + return + + + + + + -351 endchar + + + 121 0 20 196 41 397 20 hstem + 707 hmoveto + -107 callsubr + -5 257 rmoveto + -106 callsubr + endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py index 956197a32..e04027a7d 100644 --- a/Tests/subset/subset_test.py +++ b/Tests/subset/subset_test.py @@ -237,6 +237,13 @@ class SubsetTest(unittest.TestCase): subsetfont = TTFont(subsetpath) self.expect_ttx(subsetfont, self.getpath("expect_keep_math.ttx"), ["GlyphOrder", "CFF ", "MATH", "hmtx"]) + def test_subset_math_partial(self): + _, fontpath = self.compile_font(self.getpath("test_math_partial.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--text=A", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_math_partial.ttx"), ["MATH"]) + def test_subset_opbd_remove(self): # In the test font, only the glyphs 'A' and 'zero' have an entry in # the Optical Bounds table. When subsetting, we do not request any From 6ea99e4569a35e365322006cb7a1c50573d766c8 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 11 Jun 2019 13:14:35 +0100 Subject: [PATCH 2/5] feaLib/builder_test: use CapturingLogHandler instead of assertLogs --- Tests/feaLib/builder_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index eb800782d..3d852afd9 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -513,10 +513,9 @@ class BuilderTest(unittest.TestCase): addOpenTypeFeatures(font, tree) assert "GSUB" in font - @unittest.skipIf(sys.version_info[0:2] < (3, 4), - "assertLogs() was introduced in 3.4") def test_unsupported_subtable_break(self): - with self.assertLogs(level='WARNING') as logs: + logger = logging.getLogger("fontTools.feaLib.builder") + with CapturingLogHandler(logger, level='WARNING') as captor: self.build( "feature test {" " pos a 10;" @@ -524,9 +523,10 @@ class BuilderTest(unittest.TestCase): " pos b 10;" "} test;" ) - self.assertEqual(logs.output, - ['WARNING:fontTools.feaLib.builder::1:32: ' - 'unsupported "subtable" statement for lookup type']) + + captor.assertRegex( + ':1:32: unsupported "subtable" statement for lookup type' + ) def test_skip_featureNames_if_no_name_table(self): features = ( From 9af92fdb41779d9d0d8330831364546f8c87f210 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 11 Jun 2019 12:44:46 +0100 Subject: [PATCH 3/5] woff2_test: fix up flaky tests some tests were failing when shuffling the order of the tests with pytest-randomly. That's because calling TTFont.getTableData method on 'loca' table before having compiled 'glyf' returns an empty b"" string. --- Tests/ttLib/woff2_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/ttLib/woff2_test.py b/Tests/ttLib/woff2_test.py index 55c4b778e..c2702954d 100644 --- a/Tests/ttLib/woff2_test.py +++ b/Tests/ttLib/woff2_test.py @@ -145,6 +145,7 @@ class WOFF2ReaderTTFTest(WOFF2ReaderTest): def test_reconstruct_loca(self): woff2Reader = WOFF2Reader(self.file) reconstructedData = woff2Reader['loca'] + self.font.getTableData("glyf") # 'glyf' needs to be compiled before 'loca' self.assertEqual(self.font.getTableData('loca'), reconstructedData) self.assertTrue(hasattr(woff2Reader.tables['glyf'], 'data')) @@ -360,7 +361,7 @@ class WOFF2WriterTest(unittest.TestCase): def setUpClass(cls): cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2") cls.font.importXML(OTX) - cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder'] + cls.tags = sorted(t for t in cls.font.keys() if t != 'GlyphOrder') cls.numTables = len(cls.tags) cls.file = BytesIO(CFF_WOFF2.getvalue()) cls.file.seek(0, 2) @@ -518,7 +519,7 @@ class WOFF2WriterTTFTest(WOFF2WriterTest): def setUpClass(cls): cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2") cls.font.importXML(TTX) - cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder'] + cls.tags = sorted(t for t in cls.font.keys() if t != 'GlyphOrder') cls.numTables = len(cls.tags) cls.file = BytesIO(TT_WOFF2.getvalue()) cls.file.seek(0, 2) From ab9472d3ab4c84a0b58f250c4ec14ef35d003114 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 11 Jun 2019 13:22:20 +0100 Subject: [PATCH 4/5] tox.ini: use pytest-randomly to randomize test execution https://github.com/pytest-dev/pytest-randomly --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 2e8d9ee0f..6a01501fb 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = py{27,37}-cov, htmlcov deps = cov: coverage>=4.3 pytest + pytest-randomly -rrequirements.txt extras = ufo From 1fc1d2f529778074f3b2288d8e582c94b94a41f0 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 11 Jun 2019 13:39:59 +0100 Subject: [PATCH 5/5] [loggingTools] remove unused backport of LastResortLogger we are not in the business of logging. --- Lib/fontTools/misc/loggingTools.py | 61 ------------------------------ Tests/misc/loggingTools_test.py | 33 ---------------- 2 files changed, 94 deletions(-) diff --git a/Lib/fontTools/misc/loggingTools.py b/Lib/fontTools/misc/loggingTools.py index aacc08582..b00fff0d3 100644 --- a/Lib/fontTools/misc/loggingTools.py +++ b/Lib/fontTools/misc/loggingTools.py @@ -530,67 +530,6 @@ def deprecateFunction(msg, category=UserWarning): return decorator -class LastResortLogger(logging.Logger): - """ Adds support for 'lastResort' handler introduced in Python 3.2. - It allows to print messages to sys.stderr even when no explicit handler - was configured. - To enable it, you can do: - - import logging - logging.lastResort = StderrHandler(logging.WARNING) - logging.setLoggerClass(LastResortLogger) - """ - - def callHandlers(self, record): - # this is the same as Python 3.5's logging.Logger.callHandlers - c = self - found = 0 - while c: - for hdlr in c.handlers: - found = found + 1 - if record.levelno >= hdlr.level: - hdlr.handle(record) - if not c.propagate: - c = None # break out - else: - c = c.parent - if found == 0: - if logging.lastResort: - if record.levelno >= logging.lastResort.level: - logging.lastResort.handle(record) - elif ( - logging.raiseExceptions - and not self.manager.emittedNoHandlerWarning - ): - sys.stderr.write( - "No handlers could be found for logger" - ' "%s"\n' % self.name - ) - self.manager.emittedNoHandlerWarning = True - - -class StderrHandler(logging.StreamHandler): - """ This class is like a StreamHandler using sys.stderr, but always uses - whateve sys.stderr is currently set to rather than the value of - sys.stderr at handler construction time. - """ - - def __init__(self, level=logging.NOTSET): - """ - Initialize the handler. - """ - logging.Handler.__init__(self, level) - - @property - def stream(self): - # the try/execept avoids failures during interpreter shutdown, when - # globals are set to None - try: - return sys.stderr - except AttributeError: - return __import__("sys").stderr - - if __name__ == "__main__": import doctest sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) diff --git a/Tests/misc/loggingTools_test.py b/Tests/misc/loggingTools_test.py index 18b71b192..fd64b8b3a 100644 --- a/Tests/misc/loggingTools_test.py +++ b/Tests/misc/loggingTools_test.py @@ -6,15 +6,11 @@ from fontTools.misc.loggingTools import ( configLogger, ChannelsFilter, LogMixin, - StderrHandler, - LastResortLogger, - _resetExistingLoggers, ) import logging import textwrap import time import re -import sys import pytest @@ -179,32 +175,3 @@ def test_LogMixin(): assert isinstance(b.log, logging.Logger) assert a.log.name == "loggingTools_test.A" assert b.log.name == "loggingTools_test.B" - - -@pytest.mark.skipif(sys.version_info[:2] > (2, 7), reason="only for python2.7") -@pytest.mark.parametrize( - "reset", [True, False], ids=["reset", "no-reset"] -) -def test_LastResortLogger(reset, capsys, caplog): - current = logging.getLoggerClass() - msg = "The quick brown fox jumps over the lazy dog" - try: - if reset: - _resetExistingLoggers() - else: - caplog.set_level(logging.ERROR, logger="myCustomLogger") - logging.lastResort = StderrHandler(logging.WARNING) - logging.setLoggerClass(LastResortLogger) - logger = logging.getLogger("myCustomLogger") - logger.error(msg) - finally: - del logging.lastResort - logging.setLoggerClass(current) - - captured = capsys.readouterr() - if reset: - assert msg in captured.err - msg not in caplog.text - else: - msg in caplog.text - msg not in captured.err