Spaces:
Paused
Paused
| """Helpers for writing unit tests.""" | |
| from collections.abc import Iterable | |
| from io import BytesIO | |
| import os | |
| import re | |
| import shutil | |
| import sys | |
| import tempfile | |
| from unittest import TestCase as _TestCase | |
| from fontTools.config import Config | |
| from fontTools.misc.textTools import tobytes | |
| from fontTools.misc.xmlWriter import XMLWriter | |
| def parseXML(xmlSnippet): | |
| """Parses a snippet of XML. | |
| Input can be either a single string (unicode or UTF-8 bytes), or a | |
| a sequence of strings. | |
| The result is in the same format that would be returned by | |
| XMLReader, but the parser imposes no constraints on the root | |
| element so it can be called on small snippets of TTX files. | |
| """ | |
| # To support snippets with multiple elements, we add a fake root. | |
| reader = TestXMLReader_() | |
| xml = b"<root>" | |
| if isinstance(xmlSnippet, bytes): | |
| xml += xmlSnippet | |
| elif isinstance(xmlSnippet, str): | |
| xml += tobytes(xmlSnippet, "utf-8") | |
| elif isinstance(xmlSnippet, Iterable): | |
| xml += b"".join(tobytes(s, "utf-8") for s in xmlSnippet) | |
| else: | |
| raise TypeError( | |
| "expected string or sequence of strings; found %r" | |
| % type(xmlSnippet).__name__ | |
| ) | |
| xml += b"</root>" | |
| reader.parser.Parse(xml, 0) | |
| return reader.root[2] | |
| def parseXmlInto(font, parseInto, xmlSnippet): | |
| parsed_xml = [e for e in parseXML(xmlSnippet.strip()) if not isinstance(e, str)] | |
| for name, attrs, content in parsed_xml: | |
| parseInto.fromXML(name, attrs, content, font) | |
| parseInto.populateDefaults() | |
| return parseInto | |
| class FakeFont: | |
| def __init__(self, glyphs): | |
| self.glyphOrder_ = glyphs | |
| self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)} | |
| self.lazy = False | |
| self.tables = {} | |
| self.cfg = Config() | |
| def __getitem__(self, tag): | |
| return self.tables[tag] | |
| def __setitem__(self, tag, table): | |
| self.tables[tag] = table | |
| def get(self, tag, default=None): | |
| return self.tables.get(tag, default) | |
| def getGlyphID(self, name): | |
| return self.reverseGlyphOrderDict_[name] | |
| def getGlyphIDMany(self, lst): | |
| return [self.getGlyphID(gid) for gid in lst] | |
| def getGlyphName(self, glyphID): | |
| if glyphID < len(self.glyphOrder_): | |
| return self.glyphOrder_[glyphID] | |
| else: | |
| return "glyph%.5d" % glyphID | |
| def getGlyphNameMany(self, lst): | |
| return [self.getGlyphName(gid) for gid in lst] | |
| def getGlyphOrder(self): | |
| return self.glyphOrder_ | |
| def getReverseGlyphMap(self): | |
| return self.reverseGlyphOrderDict_ | |
| def getGlyphNames(self): | |
| return sorted(self.getGlyphOrder()) | |
| class TestXMLReader_(object): | |
| def __init__(self): | |
| from xml.parsers.expat import ParserCreate | |
| self.parser = ParserCreate() | |
| self.parser.StartElementHandler = self.startElement_ | |
| self.parser.EndElementHandler = self.endElement_ | |
| self.parser.CharacterDataHandler = self.addCharacterData_ | |
| self.root = None | |
| self.stack = [] | |
| def startElement_(self, name, attrs): | |
| element = (name, attrs, []) | |
| if self.stack: | |
| self.stack[-1][2].append(element) | |
| else: | |
| self.root = element | |
| self.stack.append(element) | |
| def endElement_(self, name): | |
| self.stack.pop() | |
| def addCharacterData_(self, data): | |
| self.stack[-1][2].append(data) | |
| def makeXMLWriter(newlinestr="\n"): | |
| # don't write OS-specific new lines | |
| writer = XMLWriter(BytesIO(), newlinestr=newlinestr) | |
| # erase XML declaration | |
| writer.file.seek(0) | |
| writer.file.truncate() | |
| return writer | |
| def getXML(func, ttFont=None): | |
| """Call the passed toXML function and return the written content as a | |
| list of lines (unicode strings). | |
| Result is stripped of XML declaration and OS-specific newline characters. | |
| """ | |
| writer = makeXMLWriter() | |
| func(writer, ttFont) | |
| xml = writer.file.getvalue().decode("utf-8") | |
| # toXML methods must always end with a writer.newline() | |
| assert xml.endswith("\n") | |
| return xml.splitlines() | |
| def stripVariableItemsFromTTX( | |
| string: str, | |
| ttLibVersion: bool = True, | |
| checkSumAdjustment: bool = True, | |
| modified: bool = True, | |
| created: bool = True, | |
| sfntVersion: bool = False, # opt-in only | |
| ) -> str: | |
| """Strip stuff like ttLibVersion, checksums, timestamps, etc. from TTX dumps.""" | |
| # ttlib changes with the fontTools version | |
| if ttLibVersion: | |
| string = re.sub(' ttLibVersion="[^"]+"', "", string) | |
| # sometimes (e.g. some subsetter tests) we don't care whether it's OTF or TTF | |
| if sfntVersion: | |
| string = re.sub(' sfntVersion="[^"]+"', "", string) | |
| # head table checksum and creation and mod date changes with each save. | |
| if checkSumAdjustment: | |
| string = re.sub('<checkSumAdjustment value="[^"]+"/>', "", string) | |
| if modified: | |
| string = re.sub('<modified value="[^"]+"/>', "", string) | |
| if created: | |
| string = re.sub('<created value="[^"]+"/>', "", string) | |
| return string | |
| class MockFont(object): | |
| """A font-like object that automatically adds any looked up glyphname | |
| to its glyphOrder.""" | |
| def __init__(self): | |
| self._glyphOrder = [".notdef"] | |
| class AllocatingDict(dict): | |
| def __missing__(reverseDict, key): | |
| self._glyphOrder.append(key) | |
| gid = len(reverseDict) | |
| reverseDict[key] = gid | |
| return gid | |
| self._reverseGlyphOrder = AllocatingDict({".notdef": 0}) | |
| self.lazy = False | |
| def getGlyphID(self, glyph): | |
| gid = self._reverseGlyphOrder[glyph] | |
| return gid | |
| def getReverseGlyphMap(self): | |
| return self._reverseGlyphOrder | |
| def getGlyphName(self, gid): | |
| return self._glyphOrder[gid] | |
| def getGlyphOrder(self): | |
| return self._glyphOrder | |
| class TestCase(_TestCase): | |
| def __init__(self, methodName): | |
| _TestCase.__init__(self, methodName) | |
| # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, | |
| # and fires deprecation warnings if a program uses the old name. | |
| if not hasattr(self, "assertRaisesRegex"): | |
| self.assertRaisesRegex = self.assertRaisesRegexp | |
| class DataFilesHandler(TestCase): | |
| def setUp(self): | |
| self.tempdir = None | |
| self.num_tempfiles = 0 | |
| def tearDown(self): | |
| if self.tempdir: | |
| shutil.rmtree(self.tempdir) | |
| def getpath(self, testfile): | |
| folder = os.path.dirname(sys.modules[self.__module__].__file__) | |
| return os.path.join(folder, "data", testfile) | |
| def temp_dir(self): | |
| if not self.tempdir: | |
| self.tempdir = tempfile.mkdtemp() | |
| def temp_font(self, font_path, file_name): | |
| self.temp_dir() | |
| temppath = os.path.join(self.tempdir, file_name) | |
| shutil.copy2(font_path, temppath) | |
| return temppath | |