aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/misc/testTools.py
blob: be6116132d93a6a5f692f5b8465be346aad7ca5c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""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