diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 67faf5d..6ffd699 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -7,7 +7,7 @@ from ufoLib.validators import * from ufoLib.filenames import userNameToFileName from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning -from ufoLib.plistlib import readPlist, writePlist +from ufoLib import plistlib """ A library for importing .ufo files and their descendants. Refer to http://unifiedfontobject.com for the UFO specification. @@ -107,7 +107,7 @@ def _getPlist(self, fileName, default=None): raise UFOLibError("%s is missing in %s. This file is required" % (fileName, self._path)) try: with open(path, "rb") as f: - return readPlist(f) + return plistlib.load(f) except: raise UFOLibError("The file %s could not be read." % fileName) @@ -1343,9 +1343,7 @@ def writePlistAtomically(obj, path): If so, the file is not rewritten so that the modification date is preserved. """ - f = BytesIO() - writePlist(obj, f) - data = f.getvalue() + data = plistlib.dumps(obj) writeDataFileAtomically(data, path) def writeFileAtomically(text, path, encoding="utf-8"): @@ -1427,7 +1425,7 @@ def convertUFOFormatVersion1ToFormatVersion2(inPath, outPath=None, validateRead= infoData = {} else: with open(infoPath, "rb") as f: - infoData = readPlist(f) + infoData = plistlib.load(f) infoData = _convertFontInfoDataVersion1ToVersion2(infoData) # if the paths are the same, only need to change the # fontinfo and meta info files. diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index 45cd77e..991332d 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -17,8 +17,7 @@ from warnings import warn from collections import OrderedDict from fontTools.misc.py23 import basestring, unicode, tobytes -from ufoLib.plistlib import PlistWriter, readPlist, writePlist -from ufoLib.plistFromETree import readPlistFromTree +from ufoLib import plistlib from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen from ufoLib.filenames import userNameToFileName from ufoLib.validators import isDictEnough, genericTypeValidator, colorValidator,\ @@ -181,7 +180,7 @@ def writeContents(self): """ contentsPath = os.path.join(self.dirName, "contents.plist") with open(contentsPath, "wb") as f: - writePlist(self.contents, f) + plistlib.dump(self.contents, f) # layer info @@ -234,7 +233,7 @@ def writeLayerInfo(self, info, validateWrite=None): # write file path = os.path.join(self.dirName, LAYERINFO_FILENAME) with open(path, "wb") as f: - writePlist(infoData, f) + plistlib.dump(infoData, f) # read caching @@ -481,7 +480,7 @@ def getImageReferences(self, glyphNames=None): def _readPlist(self, path): try: with open(path, "rb") as f: - data = readPlist(f) + data = plistlib.load(f) return data except Exception as e: if isinstance(e, IOError) and e.errno == 2: @@ -549,10 +548,7 @@ def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate) -# we use a custom XML declaration for backward compatibility with older -# ufoLib versions which would write it using double quotes. -# https://github.com/unified-font-object/ufoLib/issues/158 -XML_DECLARATION = b"""\n""" +_XML_DECLARATION = plistlib.XML_DECLARATION + b"\n" def _writeGlyphToBytes( @@ -598,7 +594,7 @@ def _writeGlyphToBytes( if getattr(glyphObject, "lib", None): _writeLib(glyphObject, root, validate) # return the text - data = XML_DECLARATION + etree.tostring( + data = _XML_DECLARATION + etree.tostring( root, encoding="utf-8", xml_declaration=False, pretty_print=True ) return data @@ -771,19 +767,18 @@ def _writeAnchors(glyphObject, element, identifiers, validate): def _writeLib(glyphObject, element, validate): lib = getattr(glyphObject, "lib", None) + if not lib: + # don't write empty lib + return if validate: valid, message = glyphLibValidator(lib) if not valid: raise GlifLibError(message) if not isinstance(lib, dict): lib = dict(lib) - f = BytesIO() - plistWriter = PlistWriter(f, writeHeader=False) # TODO: fix indent - plistWriter.writeValue(lib) - text = f.getvalue() - text = etree.fromstring(text) - if len(text): - etree.SubElement(element, "lib").append(text) + # plist inside GLIF begins with 2 levels of indentation + e = plistlib.totree(lib, indent_level=2) + etree.SubElement(element, "lib").append(e) # ----------------------- # layerinfo.plist Support @@ -1036,7 +1031,7 @@ def _readNote(glyphObject, note): def _readLib(glyphObject, lib, validate): assert len(lib) == 1 child = lib[0] - plist = readPlistFromTree(child) + plist = plistlib.fromtree(child) if validate: valid, message = glyphLibValidator(plist) if not valid: diff --git a/Lib/ufoLib/plistFromETree.py b/Lib/ufoLib/plistFromETree.py deleted file mode 100644 index 63f4f08..0000000 --- a/Lib/ufoLib/plistFromETree.py +++ /dev/null @@ -1,30 +0,0 @@ -from ufoLib.plistlib import PlistParser - -__all__ = ["readPlistFromTree"] - - -def readPlistFromTree(tree): - """ - Given an ElementTree Element *tree*, parse Plist data and return the root - object. - """ - parser = PlistTreeParser() - return parser.parseTree(tree) - - -class PlistTreeParser(PlistParser): - - def parseTree(self, tree): - self.parseElement(tree) - return self.root - - def parseElement(self, element): - self.handleBeginElement(element.tag, element.attrib) - # if there are children, recurse - for child in element: - self.parseElement(child) - # otherwise, parse the leaf's data - if not len(element): - # always pass str, not None - self.handleData(element.text or "") - self.handleEndElement(element.tag) diff --git a/Lib/ufoLib/plistlib.py b/Lib/ufoLib/plistlib.py index 2a84c42..41b6bf8 100644 --- a/Lib/ufoLib/plistlib.py +++ b/Lib/ufoLib/plistlib.py @@ -1,54 +1,432 @@ -""" -A py23 shim to plistlib. Reimplements plistlib under py2 naming. -""" -from __future__ import absolute_import -import sys +from __future__ import absolute_import, unicode_literals +import re +from io import BytesIO +from datetime import datetime +from base64 import b64encode, b64decode +from numbers import Integral + try: - from plistlib import ( - load as readPlist, dump as writePlist, - loads as readPlistFromString, dumps as writePlistToString) - from plistlib import _PlistParser, _PlistWriter - # Public API changed in Python 3.4 - if sys.version_info >= (3, 4): - class PlistWriter(_PlistWriter): + from functools import singledispatch +except ImportError: + from singledispatch import singledispatch +from lxml import etree +from fontTools.misc.py23 import ( + unicode, + basestring, + tounicode, + tobytes, + SimpleNamespace, + range, +) - def __init__(self, *args, **kwargs): - if "indentLevel" in kwargs: - kwargs["indent_level"] = kwargs["indentLevel"] - del kwargs["indentLevel"] - super().__init__(*args, **kwargs) - def writeValue(self, *args, **kwargs): - super().write_value(*args, **kwargs) +# we use a custom XML declaration for backward compatibility with older +# ufoLib versions which would write it using double quotes. +# https://github.com/unified-font-object/ufoLib/issues/158 +XML_DECLARATION = b"""""" - def writeData(self, *args, **kwargs): - super().write_data(*args, **kwargs) +PLIST_DOCTYPE = ( + b'' +) - def writeDict(self, *args, **kwargs): - super().write_dict(*args, **kwargs) +# Date should conform to a subset of ISO 8601: +# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' +_date_parser = re.compile( + r"(?P\d\d\d\d)" + r"(?:-(?P\d\d)" + r"(?:-(?P\d\d)" + r"(?:T(?P\d\d)" + r"(?::(?P\d\d)" + r"(?::(?P\d\d))" + r"?)?)?)?)?Z", + getattr(re, "ASCII", 0), # py3-only +) - def writeArray(self, *args, **kwargs): - super().write_array(*args, **kwargs) - class PlistParser(_PlistParser): +def _date_from_string(s): + order = ("year", "month", "day", "hour", "minute", "second") + gd = _date_parser.match(s).groupdict() + lst = [] + for key in order: + val = gd[key] + if val is None: + break + lst.append(int(val)) + return datetime(*lst) - def __init__(self): - super().__init__(use_builtin_types=True, dict_type=dict) - def parseElement(self, *args, **kwargs): - super().parse_element(*args, **kwargs) +def _date_to_string(d): + return "%04d-%02d-%02dT%02d:%02d:%02dZ" % ( + d.year, + d.month, + d.day, + d.hour, + d.minute, + d.second, + ) - def handleBeginElement(self, *args, **kwargs): - super().handle_begin_element(*args, **kwargs) - def handleData(self, *args, **kwargs): - super().handle_data(*args, **kwargs) +class PlistTarget(object): + """ Event handler using the ElementTree Target API that can be + passed to a XMLParser to produce property list objects from XML. + It is based on the CPython plistlib module's _PlistParser class, + but does not use the expat parser. - def handleEndElement(self, *args, **kwargs): - super().handle_end_element(*args, **kwargs) - else: - PlistWriter = _PlistWriter - PlistParser = _PlistParser -except ImportError: - from plistlib import readPlist, writePlist, readPlistFromString, writePlistToString - from plistlib import PlistParser, PlistWriter + >>> from lxml import etree + >>> parser = etree.XMLParser(target=PlistTarget()) + >>> result = etree.XML( + ... "" + ... " something" + ... " blah" + ... "", + ... parser=parser) + >>> result == {"something": "blah"} + True + + Links: + https://github.com/python/cpython/blob/master/Lib/plistlib.py + http://lxml.de/parsing.html#the-target-parser-interface + """ + + def __init__(self, dict_type=dict): + self.stack = [] + self.current_key = None + self.root = None + self._dict_type = dict_type + + def start(self, tag, attrib): + self._data = [] + handler = _TARGET_START_HANDLERS.get(tag) + if handler is not None: + handler(self) + + def end(self, tag): + handler = _TARGET_END_HANDLERS.get(tag) + if handler is not None: + handler(self) + + def data(self, data): + self._data.append(data) + + def close(self): + return self.root + + # helpers + + def add_object(self, value): + if self.current_key is not None: + if not isinstance(self.stack[-1], type({})): + raise ValueError("unexpected element: %r" % self.stack[-1]) + self.stack[-1][self.current_key] = value + self.current_key = None + elif not self.stack: + # this is the root object + self.root = value + else: + if not isinstance(self.stack[-1], type([])): + raise ValueError("unexpected element: %r" % self.stack[-1]) + self.stack[-1].append(value) + + def get_data(self): + data = "".join(self._data) + self._data = [] + return data + + +# event handlers + + +def start_dict(self): + d = self._dict_type() + self.add_object(d) + self.stack.append(d) + + +def end_dict(self): + if self.current_key: + raise ValueError("missing value for key '%s'" % self.current_key) + self.stack.pop() + + +def end_key(self): + if self.current_key or not isinstance(self.stack[-1], type({})): + raise ValueError("unexpected key") + self.current_key = self.get_data() + + +def start_array(self): + a = [] + self.add_object(a) + self.stack.append(a) + + +def end_array(self): + self.stack.pop() + + +def end_true(self): + self.add_object(True) + + +def end_false(self): + self.add_object(False) + + +def end_integer(self): + self.add_object(int(self.get_data())) + + +def end_real(self): + self.add_object(float(self.get_data())) + + +def end_string(self): + self.add_object(self.get_data()) + + +def end_data(self): + self.add_object(b64decode(self.get_data())) + + +def end_date(self): + self.add_object(_date_from_string(self.get_data())) + + +_TARGET_START_HANDLERS = {"dict": start_dict, "array": start_array} + +_TARGET_END_HANDLERS = { + "dict": end_dict, + "array": end_array, + "key": end_key, + "true": end_true, + "false": end_false, + "integer": end_integer, + "real": end_real, + "string": end_string, + "data": end_data, + "date": end_date, +} + +# single-dispatch generic function and overloaded implementations based +# on the type of argument, to build an element tree from a plist data + + +@singledispatch +def _make_element(value, ctx): + raise TypeError("unsupported type: %s" % type(value)) + + +@_make_element.register(unicode) +def _unicode_element(value, ctx): + el = etree.Element("string") + el.text = value + return el + + +@_make_element.register(bool) +def _bool_element(value, ctx): + if value: + return etree.Element("true") + else: + return etree.Element("false") + + +@_make_element.register(Integral) +def _integer_element(value, ctx): + if -1 << 63 <= value < 1 << 64: + el = etree.Element("integer") + el.text = "%d" % value + return el + else: + raise OverflowError(value) + + +@_make_element.register(float) +def _float_element(value, ctx): + el = etree.Element("real") + el.text = repr(value) + return el + + +@_make_element.register(dict) +def _dict_element(d, ctx): + el = etree.Element("dict") + items = d.items() + if ctx.sort_keys: + items = sorted(items) + ctx.indent_level += 1 + for key, value in items: + if not isinstance(key, basestring): + if ctx.skipkeys: + continue + raise TypeError("keys must be strings") + k = etree.SubElement(el, "key") + k.text = tounicode(key, "utf-8") + el.append(_make_element(value, ctx)) + ctx.indent_level -= 1 + return el + + +@_make_element.register(list) +@_make_element.register(tuple) +def _array_element(array, ctx): + el = etree.Element("array") + if len(array) == 0: + return el + ctx.indent_level += 1 + for value in array: + el.append(_make_element(value, ctx)) + ctx.indent_level -= 1 + return el + + +@_make_element.register(datetime) +def _date_element(date, ctx): + el = etree.Element("date") + el.text = _date_to_string(date) + return el + + +@_make_element.register(bytes) +@_make_element.register(bytearray) +def _data_element(data, ctx): + data = b64encode(data) + if data and ctx.pretty_print: + # split into multiple lines right-justified to max 76 chars + indent = b"\n" + b" " * ctx.indent_level + max_length = max(16, 76 - len(indent)) + chunks = [] + for i in range(0, len(data), max_length): + chunks.append(indent) + chunks.append(data[i : i + max_length]) + chunks.append(indent) + data = b"".join(chunks) + el = etree.Element("data") + el.text = data + return el + + +# Public functions to create element tree from plist-compatible python +# data structures and viceversa, for use when (de)serializing GLIF xml. + + +def totree( + value, sort_keys=True, skipkeys=False, pretty_print=True, indent_level=1 +): + context = SimpleNamespace( + sort_keys=sort_keys, + skipkeys=skipkeys, + pretty_print=pretty_print, + indent_level=indent_level, + ) + return _make_element(value, context) + + +def fromtree(tree, dict_type=dict): + target = PlistTarget(dict_type=dict_type) + for action, element in etree.iterwalk(tree, events=("start", "end")): + if action == "start": + target.start(element.tag, element.attrib) + elif action == "end": + # if there are no children, parse the leaf's data + if not len(element): + # always pass str, not None + target.data(element.text or "") + target.end(element.tag) + return target.close() + + +# python3 plistlib API + + +def load(fp, dict_type=dict): + if not hasattr(fp, "read"): + raise AttributeError( + "'%s' object has no attribute 'read'" % type(fp).__name__ + ) + target = PlistTarget(dict_type=dict_type) + parser = etree.XMLParser(target=target) + return etree.parse(fp, parser=parser) + + +def loads(value, dict_type=dict): + fp = BytesIO(value) + return load(fp, dict_type=dict_type) + + +def dump(value, fp, sort_keys=True, skipkeys=False, pretty_print=True): + if not hasattr(fp, "write"): + raise AttributeError( + "'%s' object has no attribute 'write'" % type(fp).__name__ + ) + root = etree.Element("plist", version="1.0") + el = totree( + value, + sort_keys=sort_keys, + skipkeys=skipkeys, + pretty_print=pretty_print, + ) + root.append(el) + tree = etree.ElementTree(root) + if pretty_print: + header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b"")) + else: + header = XML_DECLARATION + PLIST_DOCTYPE + fp.write(header) + tree.write( + fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False + ) + + +def dumps(value, sort_keys=True, skipkeys=False, pretty_print=True): + fp = BytesIO() + dump( + value, + fp, + sort_keys=sort_keys, + skipkeys=skipkeys, + pretty_print=pretty_print, + ) + return fp.getvalue() + + +# The following functions were part of the old py2-like ufoLib.plistlib API. +# They are kept only for backward compatiblity. +from .utils import deprecated + + +@deprecated("Use 'load' instead") +def readPlist(path_or_file): + did_open = False + if isinstance(path_or_file, basestring): + path_or_file = open(path_or_file, "rb") + did_open = True + try: + return load(path_or_file) + finally: + if did_open: + path_or_file.close() + + +@deprecated("Use 'dump' instead") +def writePlist(value, path_or_file): + did_open = False + if isinstance(path_or_file, basestring): + path_or_file = open(path_or_file, "wb") + did_open = True + try: + dump(value, path_or_file) + finally: + if did_open: + path_or_file.close() + + +@deprecated("Use 'loads' instead") +def readPlistFromString(data): + return loads(tobytes(data, encoding="utf-8")) + + +@deprecated("Use 'dumps' instead") +def writePlistToString(value): + return dumps(value) diff --git a/Lib/ufoLib/test/test_UFO1.py b/Lib/ufoLib/test/test_UFO1.py index dd3c186..48cc9d8 100644 --- a/Lib/ufoLib/test/test_UFO1.py +++ b/Lib/ufoLib/test/test_UFO1.py @@ -5,7 +5,7 @@ import tempfile from io import open from ufoLib import UFOReader, UFOWriter, UFOLibError -from ufoLib.plistlib import readPlist, writePlist +from ufoLib import plistlib from ufoLib.test.testSupport import fontInfoVersion1, fontInfoVersion2 @@ -23,7 +23,7 @@ def setUp(self): } path = os.path.join(self.dstDir, "metainfo.plist") with open(path, "wb") as f: - writePlist(metaInfo, f) + plistlib.dump(metaInfo, f) def tearDown(self): shutil.rmtree(self.dstDir) @@ -31,7 +31,7 @@ def tearDown(self): def _writeInfoToPlist(self, info): path = os.path.join(self.dstDir, "fontinfo.plist") with open(path, "wb") as f: - writePlist(info, f) + plistlib.dump(info, f) def testRead(self): originalData = dict(fontInfoVersion1) @@ -103,7 +103,7 @@ def makeInfoObject(self): def readPlist(self): path = os.path.join(self.dstDir, "fontinfo.plist") with open(path, "rb") as f: - plist = readPlist(f) + plist = plistlib.load(f) return plist def testWrite(self): diff --git a/Lib/ufoLib/test/test_UFO2.py b/Lib/ufoLib/test/test_UFO2.py index f08156b..c076d62 100644 --- a/Lib/ufoLib/test/test_UFO2.py +++ b/Lib/ufoLib/test/test_UFO2.py @@ -5,7 +5,7 @@ import tempfile from io import open from ufoLib import UFOReader, UFOWriter, UFOLibError -from ufoLib.plistlib import readPlist, writePlist +from ufoLib import plistlib from ufoLib.test.testSupport import fontInfoVersion2 @@ -23,7 +23,7 @@ def setUp(self): } path = os.path.join(self.dstDir, "metainfo.plist") with open(path, "wb") as f: - writePlist(metaInfo, f) + plistlib.dump(metaInfo, f) def tearDown(self): shutil.rmtree(self.dstDir) @@ -31,7 +31,7 @@ def tearDown(self): def _writeInfoToPlist(self, info): path = os.path.join(self.dstDir, "fontinfo.plist") with open(path, "wb") as f: - writePlist(info, f) + plistlib.dump(info, f) def testRead(self): originalData = dict(fontInfoVersion2) @@ -790,7 +790,7 @@ def makeInfoObject(self): def readPlist(self): path = os.path.join(self.dstDir, "fontinfo.plist") with open(path, "rb") as f: - plist = readPlist(f) + plist = plistlib.load(f) return plist def testWrite(self): diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index 78b68f2..8d98a38 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -7,7 +7,7 @@ from io import open from ufoLib import UFOReader, UFOWriter, UFOLibError from ufoLib.glifLib import GlifLibError -from ufoLib.plistlib import readPlist, writePlist +from ufoLib import plistlib from ufoLib.test.testSupport import fontInfoVersion3 @@ -29,7 +29,7 @@ def setUp(self): } path = os.path.join(self.dstDir, "metainfo.plist") with open(path, "wb") as f: - writePlist(metaInfo, f) + plistlib.dump(metaInfo, f) def tearDown(self): shutil.rmtree(self.dstDir) @@ -37,7 +37,7 @@ def tearDown(self): def _writeInfoToPlist(self, info): path = os.path.join(self.dstDir, "fontinfo.plist") with open(path, "wb") as f: - writePlist(info, f) + plistlib.dump(info, f) def testRead(self): originalData = dict(fontInfoVersion3) @@ -1717,7 +1717,7 @@ def makeInfoObject(self): def readPlist(self): path = os.path.join(self.dstDir, "fontinfo.plist") with open(path, "rb") as f: - plist = readPlist(f) + plist = plistlib.load(f) return plist def testWrite(self): @@ -3381,7 +3381,7 @@ def makeUFO(self, metaInfo=None, layerContents=None): metaInfo = dict(creator="test", formatVersion=3) path = os.path.join(self.ufoPath, "metainfo.plist") with open(path, "wb") as f: - writePlist(metaInfo, f) + plistlib.dump(metaInfo, f) # layers if layerContents is None: layerContents = [ @@ -3392,7 +3392,7 @@ def makeUFO(self, metaInfo=None, layerContents=None): if layerContents: path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) else: layerContents = [("", "glyphs")] for name, directory in layerContents: @@ -3401,7 +3401,7 @@ def makeUFO(self, metaInfo=None, layerContents=None): contents = dict(a="a.glif") path = os.path.join(glyphsPath, "contents.plist") with open(path, "wb") as f: - writePlist(contents, f) + plistlib.dump(contents, f) path = os.path.join(glyphsPath, "a.glif") with open(path, "w") as f: f.write(" ") @@ -3462,7 +3462,7 @@ def testInvalidLayerContentsFormat(self): "layer 2" : "glyphs.layer 2", } with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) self.assertRaises(UFOLibError, reader.getGlyphSet) @@ -3478,7 +3478,7 @@ def testInvalidLayerContentsNameFormat(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) self.assertRaises(UFOLibError, reader.getGlyphSet) @@ -3494,7 +3494,7 @@ def testInvalidLayerContentsDirectoryFormat(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) self.assertRaises(UFOLibError, reader.getGlyphSet) @@ -3510,7 +3510,7 @@ def testLayerContentsHasMissingDirectory(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) self.assertRaises(UFOLibError, reader.getGlyphSet) @@ -3526,7 +3526,7 @@ def testLayerContentsHasMissingDirectory(self): # ("layer 1", "glyphs.layer 2") # ] # with open(path, "wb") as f: - # writePlist(layerContents, f) + # plistlib.dump(layerContents, f) # reader = UFOReader(self.ufoPath, validate=True) # with self.assertRaises(UFOLibError): # reader.getGlyphSet() @@ -3542,7 +3542,7 @@ def testMissingDefaultLayer(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) self.assertRaises(UFOLibError, reader.getGlyphSet) @@ -3558,7 +3558,7 @@ def testDuplicateLayerName(self): ("layer 1", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) self.assertRaises(UFOLibError, reader.getGlyphSet) @@ -3574,7 +3574,7 @@ def testDuplicateLayerDirectory(self): ("layer 2", "glyphs.layer 1") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) self.assertRaises(UFOLibError, reader.getGlyphSet) @@ -3591,7 +3591,7 @@ def testDefaultLayerNoName(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) reader.getGlyphSet() @@ -3609,7 +3609,7 @@ def testDefaultLayerName(self): ] expected = layerContents[0][0] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) result = reader.getDefaultLayerName() self.assertEqual(expected, result) @@ -3623,7 +3623,7 @@ def testDefaultLayerName(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) reader.getGlyphSet(expected) @@ -3640,7 +3640,7 @@ def testLayerOrder(self): ] expected = [name for (name, directory) in layerContents] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) result = reader.getLayerNames() self.assertEqual(expected, result) @@ -3654,7 +3654,7 @@ def testLayerOrder(self): ] expected = [name for (name, directory) in layerContents] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) result = reader.getLayerNames() self.assertEqual(expected, result) @@ -3668,7 +3668,7 @@ def testLayerOrder(self): ] expected = [name for (name, directory) in layerContents] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) reader = UFOReader(self.ufoPath, validate=True) result = reader.getLayerNames() self.assertEqual(expected, result) @@ -3693,7 +3693,7 @@ def makeUFO(self, metaInfo=None, layerContents=None): metaInfo = dict(creator="test", formatVersion=3) path = os.path.join(self.ufoPath, "metainfo.plist") with open(path, "wb") as f: - writePlist(metaInfo, f) + plistlib.dump(metaInfo, f) # layers if layerContents is None: layerContents = [ @@ -3704,7 +3704,7 @@ def makeUFO(self, metaInfo=None, layerContents=None): if layerContents: path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) else: layerContents = [("", "glyphs")] for name, directory in layerContents: @@ -3713,7 +3713,7 @@ def makeUFO(self, metaInfo=None, layerContents=None): contents = dict(a="a.glif") path = os.path.join(glyphsPath, "contents.plist") with open(path, "wb") as f: - writePlist(contents, f) + plistlib.dump(contents, f) path = os.path.join(glyphsPath, "a.glif") with open(path, "w") as f: f.write(" ") @@ -3750,7 +3750,7 @@ def testInvalidLayerContentsFormat(self): "layer 2" : "glyphs.layer 2", } with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) self.assertRaises(UFOLibError, UFOWriter, self.ufoPath) # __init__: layer contents invalid name format @@ -3765,7 +3765,7 @@ def testInvalidLayerContentsNameFormat(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) self.assertRaises(UFOLibError, UFOWriter, self.ufoPath) # __init__: layer contents invalid directory format @@ -3780,7 +3780,7 @@ def testInvalidLayerContentsDirectoryFormat(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) self.assertRaises(UFOLibError, UFOWriter, self.ufoPath) # __init__: directory listed in contents not on disk @@ -3795,7 +3795,7 @@ def testLayerContentsHasMissingDirectory(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) self.assertRaises(UFOLibError, UFOWriter, self.ufoPath) # __init__: no default layer on disk @@ -3809,7 +3809,7 @@ def testMissingDefaultLayer(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) self.assertRaises(UFOLibError, UFOWriter, self.ufoPath) # __init__: duplicate layer name @@ -3824,7 +3824,7 @@ def testDuplicateLayerName(self): ("layer 1", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) self.assertRaises(UFOLibError, UFOWriter, self.ufoPath) # __init__: directory referenced by two layer names @@ -3839,7 +3839,7 @@ def testDuplicateLayerDirectory(self): ("layer 2", "glyphs.layer 1") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) self.assertRaises(UFOLibError, UFOWriter, self.ufoPath) # __init__: default without a name @@ -3855,7 +3855,7 @@ def testDefaultLayerNoName(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) writer = UFOWriter(self.ufoPath) # __init__: default with a name @@ -3870,7 +3870,7 @@ def testDefaultLayerName(self): ("layer 2", "glyphs.layer 2") ] with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) writer = UFOWriter(self.ufoPath) # __init__: up convert 1 > 3 @@ -3884,7 +3884,7 @@ def testUpConvert1To3(self): writer.writeLayerContents(["public.default"]) path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [["public.default", "glyphs"]] self.assertEqual(expected, result) @@ -3899,7 +3899,7 @@ def testUpConvert2To3(self): writer.writeLayerContents(["public.default"]) path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [["public.default", "glyphs"]] self.assertEqual(expected, result) @@ -3922,10 +3922,10 @@ def testGetGlyphSets(self): # hack contents.plist path = os.path.join(self.ufoPath, "glyphs.layer 1", "contents.plist") with open(path, "wb") as f: - writePlist(dict(b="a.glif"), f) + plistlib.dump(dict(b="a.glif"), f) path = os.path.join(self.ufoPath, "glyphs.layer 2", "contents.plist") with open(path, "wb") as f: - writePlist(dict(c="a.glif"), f) + plistlib.dump(dict(c="a.glif"), f) # now test writer = UFOWriter(self.ufoPath) # default @@ -3955,7 +3955,7 @@ def testNewFontOneLayer(self): # layer contents path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [["public.default", "glyphs"]] self.assertEqual(expected, result) @@ -3979,7 +3979,7 @@ def testNewFontThreeLayers(self): # layer contents path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [["layer 1", "glyphs.layer 1"], ["public.default", "glyphs"], ["layer 2", "glyphs.layer 2"]] self.assertEqual(expected, result) @@ -4006,7 +4006,7 @@ def testAddLayerToExistingFont(self): # layer contents path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [['public.default', 'glyphs'], ['layer 1', 'glyphs.layer 1'], ['layer 2', 'glyphs.layer 2'], ["layer 3", "glyphs.layer 3"]] self.assertEqual(expected, result) @@ -4033,7 +4033,7 @@ def testRenameLayer(self): # layer contents path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [['public.default', 'glyphs'], ['layer 3', 'glyphs.layer 3'], ['layer 2', 'glyphs.layer 2']] self.assertEqual(expected, result) @@ -4058,7 +4058,7 @@ def testRenameLayerDefault(self): # layer contents path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [['layer xxx', 'glyphs.layer xxx'], ['layer 1', 'glyphs'], ['layer 2', 'glyphs.layer 2']] self.assertEqual(expected, result) @@ -4096,7 +4096,7 @@ def testRemoveLayer(self): # layer contents path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [["public.default", "glyphs"], ["layer 2", "glyphs.layer 2"]] self.assertEqual(expected, result) @@ -4119,7 +4119,7 @@ def testRemoveDefaultLayer(self): # layer contents path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) expected = [["layer 1", "glyphs.layer 1"], ["layer 2", "glyphs.layer 2"]] self.assertEqual(expected, result) @@ -4311,19 +4311,19 @@ def makeUFO(self, formatVersion=3, layerInfo=None): metaInfo = dict(creator="test", formatVersion=formatVersion) path = os.path.join(self.ufoPath, "metainfo.plist") with open(path, "wb") as f: - writePlist(metaInfo, f) + plistlib.dump(metaInfo, f) # layercontents.plist layerContents = [("public.default", "glyphs")] path = os.path.join(self.ufoPath, "layercontents.plist") with open(path, "wb") as f: - writePlist(layerContents, f) + plistlib.dump(layerContents, f) # glyphs glyphsPath = os.path.join(self.ufoPath, "glyphs") os.mkdir(glyphsPath) contents = dict(a="a.glif") path = os.path.join(glyphsPath, "contents.plist") with open(path, "wb") as f: - writePlist(contents, f) + plistlib.dump(contents, f) path = os.path.join(glyphsPath, "a.glif") with open(path, "w") as f: f.write(" ") @@ -4335,7 +4335,7 @@ def makeUFO(self, formatVersion=3, layerInfo=None): ) path = os.path.join(glyphsPath, "layerinfo.plist") with open(path, "wb") as f: - writePlist(layerInfo, f) + plistlib.dump(layerInfo, f) def clearUFO(self): if os.path.exists(self.ufoPath): @@ -4382,7 +4382,7 @@ def testInvalidFormatLayerInfo(self): path = os.path.join(self.ufoPath, "glyphs", "layerinfo.plist") info = [("color", "0,0,0,0")] with open(path, "wb") as f: - writePlist(info, f) + plistlib.dump(info, f) # read reader = UFOReader(self.ufoPath, validate=True) glyphSet = reader.getGlyphSet() @@ -4562,7 +4562,7 @@ def testValidWrite(self): glyphSet.writeLayerInfo(info) path = os.path.join(self.ufoPath, "glyphs", "layerinfo.plist") with open(path, "rb") as f: - result = readPlist(f) + result = plistlib.load(f) self.assertEqual(expected, result) def testColor(self): diff --git a/Lib/ufoLib/test/test_UFOConversion.py b/Lib/ufoLib/test/test_UFOConversion.py index c9e9543..0255dda 100644 --- a/Lib/ufoLib/test/test_UFOConversion.py +++ b/Lib/ufoLib/test/test_UFOConversion.py @@ -5,7 +5,7 @@ import tempfile from io import open from ufoLib import convertUFOFormatVersion1ToFormatVersion2, UFOReader, UFOWriter -from ufoLib.plistlib import readPlist, writePlist +from ufoLib import plistlib from ufoLib.test.testSupport import expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion @@ -70,27 +70,27 @@ def compareFileStructures(self, path1, path2, expectedInfoData, testFeatures): self.assertEqual(os.path.exists(featuresPath1), True) # look for aggrement with open(metainfoPath1, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) with open(metainfoPath2, "rb") as f: - data2 = readPlist(f) + data2 = plistlib.load(f) self.assertEqual(data1, data2) with open(fontinfoPath1, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) self.assertEqual(sorted(data1.items()), sorted(expectedInfoData.items())) with open(kerningPath1, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) with open(kerningPath2, "rb") as f: - data2 = readPlist(f) + data2 = plistlib.load(f) self.assertEqual(data1, data2) with open(groupsPath1, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) with open(groupsPath2, "rb") as f: - data2 = readPlist(f) + data2 = plistlib.load(f) self.assertEqual(data1, data2) with open(libPath1, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) with open(libPath2, "rb") as f: - data2 = readPlist(f) + data2 = plistlib.load(f) if "UFO1" in libPath1: for key in removeFromFormatVersion1Lib: if key in data1: @@ -101,19 +101,19 @@ def compareFileStructures(self, path1, path2, expectedInfoData, testFeatures): del data2[key] self.assertEqual(data1, data2) with open(glyphsPath1_contents, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) with open(glyphsPath2_contents, "rb") as f: - data2 = readPlist(f) + data2 = plistlib.load(f) self.assertEqual(data1, data2) with open(glyphsPath1_A, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) with open(glyphsPath2_A, "rb") as f: - data2 = readPlist(f) + data2 = plistlib.load(f) self.assertEqual(data1, data2) with open(glyphsPath1_B, "rb") as f: - data1 = readPlist(f) + data1 = plistlib.load(f) with open(glyphsPath2_B, "rb") as f: - data2 = readPlist(f) + data2 = plistlib.load(f) self.assertEqual(data1, data2) def test1To2(self): @@ -175,7 +175,7 @@ def makeUFO(self, formatVersion): metaInfo = dict(creator="test", formatVersion=formatVersion) path = os.path.join(self.ufoPath, "metainfo.plist") with open(path, "wb") as f: - writePlist(metaInfo, f) + plistlib.dump(metaInfo, f) # kerning kerning = { "A" : { @@ -199,7 +199,7 @@ def makeUFO(self, formatVersion): } path = os.path.join(self.ufoPath, "kerning.plist") with open(path, "wb") as f: - writePlist(kerning, f) + plistlib.dump(kerning, f) # groups groups = { "BGroup" : ["B"], @@ -209,14 +209,14 @@ def makeUFO(self, formatVersion): } path = os.path.join(self.ufoPath, "groups.plist") with open(path, "wb") as f: - writePlist(groups, f) + plistlib.dump(groups, f) # font info fontInfo = { "familyName" : "Test" } path = os.path.join(self.ufoPath, "fontinfo.plist") with open(path, "wb") as f: - writePlist(fontInfo, f) + plistlib.dump(fontInfo, f) def clearUFO(self): if os.path.exists(self.ufoPath): @@ -343,12 +343,12 @@ def testWrite(self): # test groups path = os.path.join(self.dstDir, "groups.plist") with open(path, "rb") as f: - writtenGroups = readPlist(f) + writtenGroups = plistlib.load(f) self.assertEqual(writtenGroups, self.expectedWrittenGroups) # test kerning path = os.path.join(self.dstDir, "kerning.plist") with open(path, "rb") as f: - writtenKerning = readPlist(f) + writtenKerning = plistlib.load(f) self.assertEqual(writtenKerning, self.expectedWrittenKerning) self.tearDownUFO() diff --git a/Lib/ufoLib/test/test_glifLib.py b/Lib/ufoLib/test/test_glifLib.py index 62e2d55..c744510 100644 --- a/Lib/ufoLib/test/test_glifLib.py +++ b/Lib/ufoLib/test/test_glifLib.py @@ -6,7 +6,7 @@ from ufoLib.test.testSupport import getDemoFontGlyphSetPath from ufoLib.glifLib import ( GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString, - XML_DECLARATION, + _XML_DECLARATION, ) GLYPHSETDIR = getDemoFontGlyphSetPath() @@ -160,7 +160,7 @@ def testRoundTrip(self): def testXmlDeclaration(self): s = writeGlyphToString("a", _Glyph()) - self.assertTrue(s.startswith(XML_DECLARATION.decode("utf-8"))) + self.assertTrue(s.startswith(_XML_DECLARATION.decode("utf-8"))) if __name__ == "__main__": diff --git a/Lib/ufoLib/test/test_plistlib.py b/Lib/ufoLib/test/test_plistlib.py new file mode 100644 index 0000000..1fcd960 --- /dev/null +++ b/Lib/ufoLib/test/test_plistlib.py @@ -0,0 +1,439 @@ +from __future__ import absolute_import, unicode_literals +from ufoLib import plistlib +import os +import datetime +import codecs +import collections +from io import BytesIO +from numbers import Integral +from lxml import etree +import pytest + + +# The testdata is generated using https://github.com/python/cpython/... +# Mac/Tools/plistlib_generate_testdata.py +# which uses PyObjC to control the Cocoa classes for generating plists +datadir = os.path.join(os.path.dirname(__file__), "testdata") +with open(os.path.join(datadir, "test.plist"), "rb") as fp: + TESTDATA = fp.read() + + +@pytest.fixture +def pl(): + data = dict( + aString="Doodah", + aList=["A", "B", 12, 32.5, [1, 2, 3]], + aFloat=0.5, + anInt=728, + aBigInt=2 ** 63 - 44, + aBigInt2=2 ** 63 + 44, + aNegativeInt=-5, + aNegativeBigInt=-80000000000, + aDict=dict( + anotherString="", + aUnicodeValue="M\xe4ssig, Ma\xdf", + aTrueValue=True, + aFalseValue=False, + deeperDict=dict(a=17, b=32.5, c=[1, 2, "text"]), + ), + someData=b"", + someMoreData=b"\0\1\2\3" * 10, + nestedData=[b"\0\1\2\3" * 10], + aDate=datetime.datetime(2004, 10, 26, 10, 33, 33), + anEmptyDict=dict(), + anEmptyList=list(), + ) + data["\xc5benraa"] = "That was a unicode key." + return data + + +def test_io(tmpdir, pl): + testpath = tmpdir / "test.plist" + with testpath.open("wb") as fp: + plistlib.dump(pl, fp) + + with testpath.open("rb") as fp: + pl2 = plistlib.load(fp) + + assert pl == pl2 + + with pytest.raises(AttributeError): + plistlib.dump(pl, "filename") + + with pytest.raises(AttributeError): + plistlib.load("filename") + + +def test_invalid_type(): + pl = [object()] + + with pytest.raises(TypeError): + plistlib.dumps(pl) + + +@pytest.mark.parametrize( + "pl", + [ + 0, + 2 ** 8 - 1, + 2 ** 8, + 2 ** 16 - 1, + 2 ** 16, + 2 ** 32 - 1, + 2 ** 32, + 2 ** 63 - 1, + 2 ** 64 - 1, + 1, + -2 ** 63, + ], +) +def test_int(pl): + data = plistlib.dumps(pl) + pl2 = plistlib.loads(data) + assert isinstance(pl2, Integral) + assert pl == pl2 + data2 = plistlib.dumps(pl2) + assert data == data2 + + +@pytest.mark.parametrize( + "pl", [2 ** 64 + 1, 2 ** 127 - 1, -2 ** 64, -2 ** 127] +) +def test_int_overflow(pl): + with pytest.raises(OverflowError): + plistlib.dumps(pl) + + +def test_bytearray(pl): + pl = b"" + data = plistlib.dumps(bytearray(pl)) + pl2 = plistlib.loads(data) + assert isinstance(pl2, bytes) + assert pl2 == pl + data2 = plistlib.dumps(pl2) + assert data == data2 + + +def test_bytes(pl): + pl = b"" + data = plistlib.dumps(pl) + pl2 = plistlib.loads(data) + assert isinstance(pl2, bytes) + assert pl2 == pl + data2 = plistlib.dumps(pl2) + assert data == data2 + + +def test_indentation_array(): + data = [[[[[[[[{"test": b"aaaaaa"}]]]]]]]] + assert plistlib.loads(plistlib.dumps(data)) == data + + +def test_indentation_dict(): + data = { + "1": {"2": {"3": {"4": {"5": {"6": {"7": {"8": {"9": b"aaaaaa"}}}}}}}} + } + assert plistlib.loads(plistlib.dumps(data)) == data + + +def test_indentation_dict_mix(): + data = {"1": {"2": [{"3": [[[[[{"test": b"aaaaaa"}]]]]]}]}} + assert plistlib.loads(plistlib.dumps(data)) == data + + +@pytest.mark.xfail(reason="we use two spaces, Apple uses tabs") +def test_apple_formatting(): + # we also split base64 data into multiple lines differently: + # both right-justify data to 76 chars, but Apple's treats tabs + # as 8 spaces, whereas we use 2 spaces + pl = plistlib.loads(TESTDATA) + data = plistlib.dumps(pl) + assert data == TESTDATA + + +def test_apple_formatting_fromliteral(pl): + pl2 = plistlib.loads(TESTDATA) + assert pl == pl2 + + +def test_apple_roundtrips(): + pl = plistlib.loads(TESTDATA) + data = plistlib.dumps(pl) + pl2 = plistlib.loads(data) + data2 = plistlib.dumps(pl2) + assert data == data2 + + +def test_bytesio(pl): + b = BytesIO() + plistlib.dump(pl, b) + pl2 = plistlib.load(BytesIO(b.getvalue())) + assert pl == pl2 + pl2 = plistlib.load(BytesIO(b.getvalue())) + assert pl == pl2 + + +@pytest.mark.parametrize("sort_keys", [False, True]) +def test_keysort_bytesio(sort_keys): + pl = collections.OrderedDict() + pl["b"] = 1 + pl["a"] = 2 + pl["c"] = 3 + + b = BytesIO() + + plistlib.dump(pl, b, sort_keys=sort_keys) + pl2 = plistlib.load( + BytesIO(b.getvalue()), dict_type=collections.OrderedDict + ) + + assert dict(pl) == dict(pl2) + if sort_keys: + assert list(pl2.keys()) == ["a", "b", "c"] + else: + assert list(pl2.keys()) == ["b", "a", "c"] + + +@pytest.mark.parametrize("sort_keys", [False, True]) +def test_keysort(sort_keys): + pl = collections.OrderedDict() + pl["b"] = 1 + pl["a"] = 2 + pl["c"] = 3 + + data = plistlib.dumps(pl, sort_keys=sort_keys) + pl2 = plistlib.loads(data, dict_type=collections.OrderedDict) + + assert dict(pl) == dict(pl2) + if sort_keys: + assert list(pl2.keys()) == ["a", "b", "c"] + else: + assert list(pl2.keys()) == ["b", "a", "c"] + + +def test_keys_no_string(): + pl = {42: "aNumber"} + + with pytest.raises(TypeError): + plistlib.dumps(pl) + + b = BytesIO() + with pytest.raises(TypeError): + plistlib.dump(pl, b) + + +def test_skipkeys(): + pl = {42: "aNumber", "snake": "aWord"} + + data = plistlib.dumps(pl, skipkeys=True, sort_keys=False) + + pl2 = plistlib.loads(data) + assert pl2 == {"snake": "aWord"} + + fp = BytesIO() + plistlib.dump(pl, fp, skipkeys=True, sort_keys=False) + data = fp.getvalue() + pl2 = plistlib.loads(fp.getvalue()) + assert pl2 == {"snake": "aWord"} + + +def test_tuple_members(): + pl = {"first": (1, 2), "second": (1, 2), "third": (3, 4)} + + data = plistlib.dumps(pl) + pl2 = plistlib.loads(data) + assert pl2 == {"first": [1, 2], "second": [1, 2], "third": [3, 4]} + assert pl2["first"] is not pl2["second"] + + +def test_list_members(): + pl = {"first": [1, 2], "second": [1, 2], "third": [3, 4]} + + data = plistlib.dumps(pl) + pl2 = plistlib.loads(data) + assert pl2 == {"first": [1, 2], "second": [1, 2], "third": [3, 4]} + assert pl2["first"] is not pl2["second"] + + +def test_dict_members(): + pl = {"first": {"a": 1}, "second": {"a": 1}, "third": {"b": 2}} + + data = plistlib.dumps(pl) + pl2 = plistlib.loads(data) + assert pl2 == {"first": {"a": 1}, "second": {"a": 1}, "third": {"b": 2}} + assert pl2["first"] is not pl2["second"] + + +def test_controlcharacters(): + for i in range(128): + c = chr(i) + testString = "string containing %s" % c + if i >= 32 or c in "\r\n\t": + # \r, \n and \t are the only legal control chars in XML + data = plistlib.dumps(testString) + # the stdlib's plistlib writer always replaces \r with \n + # inside string values; we don't (the ctrl character is + # escaped by lxml, so it roundtrips) + # if c != "\r": + assert plistlib.loads(data) == testString + else: + with pytest.raises(ValueError): + plistlib.dumps(testString) + + +def test_non_bmp_characters(): + pl = {"python": "\U0001f40d"} + data = plistlib.dumps(pl) + assert plistlib.loads(data) == pl + + +def test_nondictroot(): + test1 = "abc" + test2 = [1, 2, 3, "abc"] + result1 = plistlib.loads(plistlib.dumps(test1)) + result2 = plistlib.loads(plistlib.dumps(test2)) + assert test1 == result1 + assert test2 == result2 + + +def test_invalidarray(): + for i in [ + "key inside an array", + "key inside an array23", + "key inside an array3", + ]: + with pytest.raises(ValueError): + plistlib.loads( + ("%s" % i).encode("utf-8") + ) + + +def test_invaliddict(): + for i in [ + "kcompound key", + "single key", + "missing key", + "k1v15.3" + "k1k2double key", + ]: + with pytest.raises(ValueError): + plistlib.loads(("%s" % i).encode()) + with pytest.raises(ValueError): + plistlib.loads( + ("%s" % i).encode() + ) + + +def test_invalidinteger(): + with pytest.raises(ValueError): + plistlib.loads(b"not integer") + + +def test_invalidreal(): + with pytest.raises(ValueError): + plistlib.loads(b"not real") + + +@pytest.mark.parametrize( + "xml_encoding, encoding, bom", + [ + (b"utf-8", "utf-8", codecs.BOM_UTF8), + (b"utf-16", "utf-16-le", codecs.BOM_UTF16_LE), + (b"utf-16", "utf-16-be", codecs.BOM_UTF16_BE), + (b"utf-32", "utf-32-le", codecs.BOM_UTF32_LE), + (b"utf-32", "utf-32-be", codecs.BOM_UTF32_BE), + ], +) +def test_xml_encodings(pl, xml_encoding, encoding, bom): + data = TESTDATA.replace(b"UTF-8", xml_encoding) + data = bom + data.decode("utf-8").encode(encoding) + pl2 = plistlib.loads(data) + assert pl == pl2 + + +def test_fromtree(pl): + tree = etree.fromstring(TESTDATA) + pl2 = plistlib.fromtree(tree) + assert pl == pl2 + + +def _strip(txt): + return ( + "".join(l.strip() for l in txt.splitlines()) + if txt is not None + else "" + ) + + +def test_totree(pl): + tree = etree.fromstring(TESTDATA)[0] # ignore root 'plist' element + tree2 = plistlib.totree(pl) + assert tree.tag == tree2.tag == "dict" + for (_, e1), (_, e2) in zip(etree.iterwalk(tree), etree.iterwalk(tree2)): + assert e1.tag == e2.tag + assert e1.attrib == e2.attrib + assert len(e1) == len(e2) + # ignore whitespace + assert _strip(e1.text) == _strip(e2.text) + + +def test_no_pretty_print(): + data = plistlib.dumps({"data": b"hello"}, pretty_print=False) + assert data == ( + plistlib.XML_DECLARATION + + plistlib.PLIST_DOCTYPE + + b'' + b"" + b"data" + b"aGVsbG8=" + b"" + b"" + ) + + +def test_readPlist_from_path(pl): + path = os.path.join(datadir, "test.plist") + pl2 = plistlib.readPlist(path) + assert pl2 == pl + + +def test_readPlist_from_file(pl): + f = open(os.path.join(datadir, "test.plist"), "rb") + pl2 = plistlib.readPlist(f) + assert pl2 == pl + assert not f.closed + f.close() + + +def test_readPlistFromString(pl): + pl2 = plistlib.readPlistFromString(TESTDATA) + assert pl2 == pl + + +def test_writePlist_to_path(tmpdir, pl): + testpath = tmpdir / "test.plist" + plistlib.writePlist(pl, str(testpath)) + with testpath.open("rb") as fp: + pl2 = plistlib.load(fp) + assert pl2 == pl + + +def test_writePlist_to_file(tmpdir, pl): + testpath = tmpdir / "test.plist" + with testpath.open("wb") as fp: + plistlib.writePlist(pl, fp) + with testpath.open("rb") as fp: + pl2 = plistlib.load(fp) + assert pl2 == pl + + +def test_writePlistToString(pl): + data = plistlib.writePlistToString(pl) + pl2 = plistlib.loads(data) + assert pl2 == pl + + +if __name__ == "__main__": + import sys + + sys.exit(pytest.main(sys.argv)) diff --git a/Lib/ufoLib/test/testdata/test.plist b/Lib/ufoLib/test/testdata/test.plist new file mode 100644 index 0000000..864605f --- /dev/null +++ b/Lib/ufoLib/test/testdata/test.plist @@ -0,0 +1,87 @@ + + + + + aBigInt + 9223372036854775764 + aBigInt2 + 9223372036854775852 + aDate + 2004-10-26T10:33:33Z + aDict + + aFalseValue + + aTrueValue + + aUnicodeValue + Mässig, Maß + anotherString + <hello & 'hi' there!> + deeperDict + + a + 17 + b + 32.5 + c + + 1 + 2 + text + + + + aFloat + 0.5 + aList + + A + B + 12 + 32.5 + + 1 + 2 + 3 + + + aNegativeBigInt + -80000000000 + aNegativeInt + -5 + aString + Doodah + anEmptyDict + + anEmptyList + + anInt + 728 + nestedData + + + PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r + PgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5 + IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi + aW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz + IG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID + PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw== + + + someData + + PGJpbmFyeSBndW5rPg== + + someMoreData + + PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8 + bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxs + b3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxv + dHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90 + cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw== + + Åbenraa + That was a unicode key. + + diff --git a/Lib/ufoLib/utils.py b/Lib/ufoLib/utils.py new file mode 100644 index 0000000..d960e42 --- /dev/null +++ b/Lib/ufoLib/utils.py @@ -0,0 +1,39 @@ +"""The module contains miscellaneous helpers. +It's not considered part of the public ufoLib API. +""" +import warnings +import functools + + +def deprecated(msg=""): + """Decorator factory to mark functions as deprecated with given message. + + >>> @deprecated("Enough!") + ... def some_function(): + ... "I just print 'hello world'." + ... print("hello world") + >>> some_function() + hello world + >>> some_function.__doc__ + "I just print 'hello world'." + """ + + def deprecated_decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + "{} function is a deprecated. {}".format(func.__name__, msg), + category=DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return deprecated_decorator + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/requirements.txt b/requirements.txt index 8b5e96a..4435f96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ fonttools==3.28.0 lxml==4.2.3 +singledispatch==3.4.0.3; python_version < '3.4' diff --git a/setup.py b/setup.py index 1b28d3e..5f06803 100755 --- a/setup.py +++ b/setup.py @@ -165,8 +165,9 @@ def run(self): 'pytest>=3.0.2', ], install_requires=[ - "fonttools>=3.1.2", - "lxml>=4.0", + "fonttools >= 3.1.2, < 4", + "lxml >= 4.0, < 5", + "singledispatch >= 3.4.0.3, < 4; python_version < '3.4'", ], cmdclass={ "release": release,