From 45910b6131dbb8e679fffceb5b24433960693f8a Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 2 May 2016 23:06:25 -0400 Subject: [PATCH 01/71] Initial work on reading single file UFO. This is a work in progress update of UFOReader and UFOWriter that supports UFO in its package and zipped forms. Reading works. Writing is not yet implemented. I'm building a base file system (that lives on top of fs for now and maybe in the long term) that the reader and writer then subclass. This base class implements the file system interaction so that the reader and writer can be blissfully ignorant about file systems. Additionally, I ran into a problem with the local plistlib.py creating an import error, so I've temporarily renamed it plistlibShim.py so that I can continue working. Did I mention that this is a work in progress? It's a work in progress. --- Lib/ufoLib/__init__.py | 130 ++------- Lib/ufoLib/errors.py | 1 + Lib/ufoLib/filesystem.py | 282 ++++++++++++++++++++ Lib/ufoLib/glifLib.py | 233 ++++++++-------- Lib/ufoLib/plistFromTree.py | 2 +- Lib/ufoLib/{plistlib.py => plistlibShim.py} | 0 6 files changed, 423 insertions(+), 225 deletions(-) create mode 100644 Lib/ufoLib/errors.py create mode 100644 Lib/ufoLib/filesystem.py rename Lib/ufoLib/{plistlib.py => plistlibShim.py} (100%) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index f2dfc9d..108299f 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -3,11 +3,14 @@ from io import StringIO, BytesIO, open import codecs from copy import deepcopy +from ufoLib.filesystem import FileSystem from ufoLib.glifLib import GlyphSet from ufoLib.validators import * from ufoLib.filenames import userNameToFileName from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning -from ufoLib.plistlib import readPlist, writePlist +from ufoLib.plistlibShim import readPlist, writePlist +from ufoLib.errors import UFOLibError + """ A library for importing .ufo files and their descendants. Refer to http://unifiedfontobject.com for the UFO specification. @@ -61,7 +64,7 @@ ] -class UFOLibError(Exception): pass + # ---------- @@ -118,14 +121,12 @@ def _getPlist(self, fileName, default=None): # UFO Reader # ---------- -class UFOReader(object): +class UFOReader(FileSystem): """Read the various components of the .ufo.""" def __init__(self, path): - if not os.path.exists(path): - raise UFOLibError("The specified UFO doesn't exist.") - self._path = path + super(UFOReader, self).__init__(path) self.readMetaInfo() self._upConvertedKerningData = None @@ -182,73 +183,13 @@ def _upConvertKerning(self): self._upConvertedKerningData["groups"] = groups self._upConvertedKerningData["groupRenameMaps"] = conversionMaps - # support methods - - _checkForFile = staticmethod(os.path.exists) - - _getPlist = _getPlist - - def readBytesFromPath(self, path, encoding=None): - """ - Returns the bytes in the file at the given path. - The path must be relative to the UFO path. - Returns None if the file does not exist. - An encoding may be passed if needed. - """ - fullPath = os.path.join(self._path, path) - if not self._checkForFile(fullPath): - return None - if os.path.isdir(fullPath): - raise UFOLibError("%s is a directory." % path) - if encoding: - f = open(fullPath, encoding=encoding) - else: - f = open(fullPath, "rb", encoding=encoding) - data = f.read() - f.close() - return data - - def getReadFileForPath(self, path, encoding=None): - """ - Returns a file (or file-like) object for the - file at the given path. The path must be relative - to the UFO path. Returns None if the file does not exist. - An encoding may be passed if needed. - - Note: The caller is responsible for closing the open file. - """ - fullPath = os.path.join(self._path, path) - if not self._checkForFile(fullPath): - return None - if os.path.isdir(fullPath): - raise UFOLibError("%s is a directory." % path) - if encoding: - f = open(fullPath, "rb", encoding=encoding) - else: - f = open(fullPath, "r") - return f - - def getFileModificationTime(self, path): - """ - Returns the modification time (as reported by os.path.getmtime) - for the file at the given path. The path must be relative to - the UFO path. Returns None if the file does not exist. - """ - fullPath = os.path.join(self._path, path) - if not self._checkForFile(fullPath): - return None - return os.path.getmtime(fullPath) - # metainfo.plist def readMetaInfo(self): """ Read metainfo.plist. Only used for internal operations. """ - # should there be a blind try/except with a UFOLibError - # raised in except here (and elsewhere)? It would be nice to - # provide external callers with a single exception to catch. - data = self._getPlist(METAINFO_FILENAME) + data = self.readPlist(METAINFO_FILENAME) if not isinstance(data, dict): raise UFOLibError("metainfo.plist is not properly formatted.") formatVersion = data["formatVersion"] @@ -259,7 +200,7 @@ def readMetaInfo(self): # groups.plist def _readGroups(self): - return self._getPlist(GROUPS_FILENAME, {}) + return self.readPlist(GROUPS_FILENAME, {}) def readGroups(self): """ @@ -301,7 +242,7 @@ def getKerningGroupConversionRenameMaps(self): # fontinfo.plist def _readInfo(self): - data = self._getPlist(FONTINFO_FILENAME, {}) + data = self.readPlist(FONTINFO_FILENAME, {}) if not isinstance(data, dict): raise UFOLibError("fontinfo.plist is not properly formatted.") return data @@ -353,7 +294,7 @@ def readInfo(self, info): # kerning.plist def _readKerning(self): - data = self._getPlist(KERNING_FILENAME, {}) + data = self.readPlist(KERNING_FILENAME, {}) return data def readKerning(self): @@ -384,7 +325,7 @@ def readLib(self): """ Read lib.plist. Returns a dict. """ - data = self._getPlist(LIB_FILENAME, {}) + data = self.readPlist(LIB_FILENAME, {}) valid, message = fontLibValidator(data) if not valid: raise UFOLibError(message) @@ -396,10 +337,9 @@ def readFeatures(self): """ Read features.fea. Returns a string. """ - path = os.path.join(self._path, FEATURES_FILENAME) - if not self._checkForFile(path): + if not self.exists(FEATURES_FILENAME): return "" - with open(path, "r") as f: + with self.open(FEATURES_FILENAME, "r") as f: text = f.read() return text @@ -413,7 +353,7 @@ def _readLayerContents(self): if self._formatVersion < 3: return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] # read the file on disk - contents = self._getPlist(LAYERCONTENTS_FILENAME) + contents = self.readPlist(LAYERCONTENTS_FILENAME) valid, error = layerContentsValidator(contents, self._path) if not valid: raise UFOLibError(error) @@ -456,8 +396,7 @@ def getGlyphSet(self, layerName=None): break if directory is None: raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName) - glyphsPath = os.path.join(self._path, directory) - return GlyphSet(glyphsPath, ufoFormatVersion=self._formatVersion) + return GlyphSet(directory, fileSystem=self, ufoFormatVersion=self._formatVersion) def getCharacterMapping(self, layerName=None): """ @@ -477,36 +416,18 @@ def getCharacterMapping(self, layerName=None): # /data - def getDataDirectoryListing(self, maxDepth=100): + def getDataDirectoryListing(self): """ Returns a list of all files in the data directory. The returned paths will be relative to the UFO. This will not list directory names, only file names. Thus, empty directories will be skipped. - - The maxDepth argument sets the maximum number - of sub-directories that are allowed. """ - path = os.path.join(self._path, DATA_DIRNAME) - if not self._checkForFile(path): + if not self.exists(DATA_DIRNAME): return [] - listing = self._getDirectoryListing(path, maxDepth=maxDepth) - listing = [os.path.relpath(path, "data") for path in listing] + listing = self.listDirectory(path, recurse=True) return listing - def _getDirectoryListing(self, path, depth=0, maxDepth=100): - if depth > maxDepth: - raise UFOLibError("Maximum recusion depth reached.") - result = [] - for fileName in os.listdir(path): - p = os.path.join(path, fileName) - if os.path.isdir(p): - result += self._getDirectoryListing(p, depth=depth+1, maxDepth=maxDepth) - else: - p = os.path.relpath(p, self._path) - result.append(p) - return result - def getImageDirectoryListing(self): """ Returns a list of all image file names in @@ -515,15 +436,13 @@ def getImageDirectoryListing(self): """ if self._formatVersion < 3: return [] - path = os.path.join(self._path, IMAGES_DIRNAME) - if not os.path.exists(path): + if not self.exists(IMAGES_DIRNAME): return [] - if not os.path.isdir(path): + if not self.isDirectory(IMAGES_DIRNAME): raise UFOLibError("The UFO contains an \"images\" file instead of a directory.") result = [] - for fileName in os.listdir(path): - p = os.path.join(path, fileName) - if os.path.isdir(p): + for fileName in self.listDirectory(path): + if self.isDirectory(fileName): # silently skip this as version control # systems often have hidden directories continue @@ -538,7 +457,8 @@ def readImage(self, fileName): """ if self._formatVersion < 3: raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion) - data = self.readBytesFromPath(os.path.join(IMAGES_DIRNAME, fileName)) + path = self.joinPath(IMAGES_DIRNAME, fileName) + data = self.readBytesFromPath(path) if data is None: raise UFOLibError("No image file named %s." % fileName) valid, error = pngValidator(data=data) diff --git a/Lib/ufoLib/errors.py b/Lib/ufoLib/errors.py new file mode 100644 index 0000000..984d189 --- /dev/null +++ b/Lib/ufoLib/errors.py @@ -0,0 +1 @@ +class UFOLibError(Exception): pass \ No newline at end of file diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py new file mode 100644 index 0000000..1e42aaa --- /dev/null +++ b/Lib/ufoLib/filesystem.py @@ -0,0 +1,282 @@ +import os +from io import StringIO +from fs.osfs import OSFS +from fs.zipfs import ZipFS, ZipOpenError +from ufoLib.plistlibShim import readPlist, writePlist +from ufoLib.errors import UFOLibError + +try: + basestring +except NameError: + basestring = str + + +class FileSystem(object): + + def __init__(self, path): + self._path = "" + if isinstance(path, basestring): + self._path = path + if not os.path.exists(path): + raise UFOLibError("The specified UFO doesn't exist.") + if os.path.isdir(path): + path = OSFS(path) + else: + try: + path = ZipFS(path, mode="r", allow_zip_64=True) + except ZipOpenError: + raise UFOLibError("The specified UFO is not in a proper format.") + self._fs = path + + def close(self): + self._fs.close() + + # ----------------- + # Path Manipulation + # ----------------- + + def joinPath(self, *parts): + return os.path.join(*parts) + + def splitPath(self, path): + return os.path.split(path) + + def directoryName(self, path): + return self.splitPath(path)[0] + + def relativePath(self, path, start): + return os.path.relpath(path, start) + + # --------- + # Existence + # --------- + + def exists(self, path): + return self._fs.exists(path) + + def isDirectory(self, path): + return self._fs.isdir(path) + + def listDirectory(self, path, recurse=False): + return self._listDirectory(path, recurse=recurse) + + def _listDirectory(self, path, recurse=False, depth=0, maxDepth=100): + if depth > maxDepth: + raise UFOLibError("Maximum recusion depth reached.") + result = [] + for fileName in self._fs.listdir(path): + p = self.joinPath(path, fileName) + if os.path.isdir(p) and recurse: + result += self._listDirectory(p, recurse=True, depth=depth+1, maxDepth=maxDepth) + else: + result.append(p) + return result + + # ----------- + # File Opener + # ----------- + + def open(self, path, mode="r", encoding=None): + """ + Returns a file (or file-like) object for the + file at the given path. The path must be relative + to the UFO path. Returns None if the file does + not exist and the mode is "r" or "rb. An encoding + may be passed if needed. + + Note: The caller is responsible for closing the open file. + """ + if encoding: + if mode == "r": + mode = "rb" + elif mode == "w": + mode = "wb" + if mode in ("r", "rb") and not self.exists(path): + return None + if self.exists(path) and self.isDirectory(path): + raise UFOLibError("%s is a directory." % path) + if mode in ("w", "wb"): + self._buildDirectoryTree(path) + f = self._fs.open(path, mode, encoding=encoding) + return f + + def _buildDirectoryTree(self, path): + directory, fileName = self.splitPath(path) + directoryTree = [] + while directory: + directory, d = self.splitPath(directory) + directoryTree.append(d) + directoryTree.reverse() + built = "" + for d in directoryTree: + d = self.joinPath(built, d) + p = self.joinPath(self._path, d) + if not self.exists(p): + self._fs.mkdir(p) + built = d + + # ------------------ + # Modification Times + # ------------------ + + def getFileModificationTime(self, path): + """ + Returns the modification time (as reported by os.path.getmtime) + for the file at the given path. The path must be relative to + the UFO path. Returns None if the file does not exist. + """ + if not self.exists(path): + return None + info = self._fs.getinfo(path) + return info["modified_time"] + + # -------------- + # Raw Read/Write + # -------------- + + def readBytesFromPath(self, path, encoding=None): + """ + Returns the bytes in the file at the given path. + The path must be relative to the UFO path. + Returns None if the file does not exist. + An encoding may be passed if needed. + """ + f = self.open(path, mode="r", encoding=encoding) + if f is None: + return None + data = f.read() + f.close() + return data + + def writeBytesToPath(self, path, data, encoding=None): + """ + Write bytes to path. If needed, the directory tree + for the given path will be built. The path must be + relative to the UFO. An encoding may be passed if needed. + """ + if encoding: + data = StringIO(data).encode(encoding) + self._writeFileAtomically(data, fullPath) + + def _writeFileAtomically(self, data, path): + """ + Write data into a file at path. Do this sort of atomically + making it harder to cause corrupt files. This also checks to see + if data matches the data that is already in the file at path. + If so, the file is not rewritten so that the modification date + is preserved. + """ + assert isinstance(data, bytes) + if self.exists(path): + f = self.open(path, "rb") + oldData = f.read() + f.close() + if data == oldData: + return + if data: + f = self.open(path, "wb") + f.write(data) + f.close() + + # ------------ + # File Removal + # ------------ + + def remove(self, path): + """ + Remove the file (or directory) at path. The path + must be relative to the UFO. This is only allowed + for files in the data and image directories. + """ + d = path + parts = [] + while d: + d, p = self.splitPath(d) + if p: + parts.append(p) + if parts[-1] not in ("images", "data"): + raise UFOLibError("Removing \"%s\" is not legal." % path) + self._removeFileForPath(path, raiseErrorIfMissing=True) + + def _removeFileForPath(self, path, raiseErrorIfMissing=False): + if not self.exists(path): + if raiseErrorIfMissing: + raise UFOLibError("The file %s does not exist." % path) + else: + if self.isDirectory(path): + self._fs.removedir(path) + else: + self._fs.remove(path) + directory = self.directoryName(path) + self._removeEmptyDirectoriesForPath(directory) + + def _removeEmptyDirectoriesForPath(self, directory): + if not self.exists(directory): + return + if not len(self._fs.listdir(directory)): + self._fs.removedir(directory) + else: + return + directory = self.directoryName(directory) + if directory: + self._removeEmptyDirectoriesForPath(directory) + + # -------------- + # Property Lists + # -------------- + + def readPlist(self, path, default=None): + """ + Read a property list relative to the + UFO path. If the file is missing and + default is None a UFOLibError will be + raised. Otherwise default is returned. + The errors that could be raised during + the reading of a plist are unpredictable + and/or too large to list, so, a blind + try: except: is done. If an exception + occurs, a UFOLibError will be raised. + """ + if not self.exists(path): + if default is not None: + return default + else: + raise UFOLibError("%s is missing. This file is required" % path) + try: + with self.open(path, "rb") as f: + return readPlist(f) + except: + raise UFOLibError("The file %s could not be read." % fileName) + + +if __name__ == "__main__": + from ufoLib import UFOReader + path = os.path.dirname(__file__) + path = os.path.dirname(path) + path = os.path.dirname(path) + path = os.path.join(path, "TestData", "TestFont1 (UFO2).ufo") + + import zipfile + + def _packDirectory(z, d, root=""): + for i in os.listdir(d): + p = os.path.join(d, i) + l = os.path.join(root, i) + if os.path.isdir(p): + _packDirectory(z, p, root=l) + else: + l = os.path.join(root, i) + z.write(p, l) + + p = path + ".zip" + if os.path.exists(p): + os.remove(p) + z = zipfile.ZipFile(p, "w") + _packDirectory(z, path) + z.close() + + path += ".zip" + + reader = UFOReader(path) + glyphSet = reader.getGlyphSet() + print glyphSet.getGLIF("A") diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index 701e7bf..186a37c 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -15,8 +15,9 @@ from io import BytesIO, StringIO, open from warnings import warn from fontTools.misc.py23 import tobytes, tostr +from ufoLib.filesystem import FileSystem from ufoLib.xmlTreeBuilder import buildTree, stripCharacterData -from ufoLib.plistlib import PlistWriter, readPlist, writePlist +from ufoLib.plistlibShim import PlistWriter, readPlist, writePlist from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen from ufoLib.filenames import userNameToFileName from ufoLib.validators import isDictEnough, genericTypeValidator, colorValidator,\ @@ -96,7 +97,7 @@ class GlyphSet(object): glyphClass = Glyph - def __init__(self, dirName, glyphNameToFileNameFunc=None, ufoFormatVersion=3): + def __init__(self, dirName, fileSystem=None, glyphNameToFileNameFunc=None, ufoFormatVersion=3): """ 'dirName' should be a path to an existing directory. @@ -109,6 +110,10 @@ def __init__(self, dirName, glyphNameToFileNameFunc=None, ufoFormatVersion=3): self.dirName = dirName if ufoFormatVersion not in supportedUFOFormatVersions: raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion) + if fileSystem is None: + fileSystem = FileSystem(dirName) + dirName = "" + self._fs = fileSystem self.ufoFormatVersion = ufoFormatVersion if glyphNameToFileNameFunc is None: glyphNameToFileNameFunc = glyphNameToFileName @@ -121,12 +126,12 @@ def rebuildContents(self): """ Rebuild the contents dict by loading contents.plist. """ - contentsPath = os.path.join(self.dirName, "contents.plist") - if not os.path.exists(contentsPath): + contentsPath = self._fs.joinPath(self.dirName, "contents.plist") + if not self._fs.exists(contentsPath): # missing, consider the glyphset empty. contents = {} else: - contents = self._readPlist(contentsPath) + contents = self._fs.readPlist(contentsPath) # validate the contents invalidFormat = False if not isinstance(contents, dict): @@ -137,7 +142,7 @@ def rebuildContents(self): invalidFormat = True if not isinstance(fileName, basestring): invalidFormat = True - elif not os.path.exists(os.path.join(self.dirName, fileName)): + elif not self._fs.exists(self._fs.joinPath(self.dirName, fileName)): raise GlifLibError("contents.plist references a file that does not exist: %s" % fileName) if invalidFormat: raise GlifLibError("contents.plist is not properly formatted") @@ -160,22 +165,22 @@ def getReverseContents(self): self._reverseContents = d return self._reverseContents - def writeContents(self): - """ - Write the contents.plist file out to disk. Call this method when - you're done writing glyphs. - """ - contentsPath = os.path.join(self.dirName, "contents.plist") - with open(contentsPath, "wb") as f: - writePlist(self.contents, f) + # def writeContents(self): + # """ + # Write the contents.plist file out to disk. Call this method when + # you're done writing glyphs. + # """ + # contentsPath = os.path.join(self.dirName, "contents.plist") + # with open(contentsPath, "wb") as f: + # writePlist(self.contents, f) # layer info def readLayerInfo(self, info): - path = os.path.join(self.dirName, LAYERINFO_FILENAME) - if not os.path.exists(path): + path = self._fs.joinPath(self.dirName, LAYERINFO_FILENAME) + if not self._fs.exists(path): return - infoDict = self._readPlist(path) + infoDict = self._fs.readPlist(path) if not isinstance(infoDict, dict): raise GlifLibError("layerinfo.plist is not properly formatted.") infoDict = validateLayerInfoVersion3Data(infoDict) @@ -186,28 +191,28 @@ def readLayerInfo(self, info): except AttributeError: raise GlifLibError("The supplied layer info object does not support setting a necessary attribute (%s)." % attr) - def writeLayerInfo(self, info): - if self.ufoFormatVersion < 3: - raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion) - # gather data - infoData = {} - for attr in list(layerInfoVersion3ValueData.keys()): - if hasattr(info, attr): - try: - value = getattr(info, attr) - except AttributeError: - raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr) - if value is None: - continue - infoData[attr] = value - # validate - infoData = validateLayerInfoVersion3Data(infoData) - # write file - path = os.path.join(self.dirName, LAYERINFO_FILENAME) - with open(path, "wb") as f: - writePlist(infoData, f) - - # read caching + # def writeLayerInfo(self, info): + # if self.ufoFormatVersion < 3: + # raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion) + # # gather data + # infoData = {} + # for attr in list(layerInfoVersion3ValueData.keys()): + # if hasattr(info, attr): + # try: + # value = getattr(info, attr) + # except AttributeError: + # raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr) + # if value is None: + # continue + # infoData[attr] = value + # # validate + # infoData = validateLayerInfoVersion3Data(infoData) + # # write file + # path = os.path.join(self.dirName, LAYERINFO_FILENAME) + # with open(path, "wb") as f: + # writePlist(infoData, f) + + # # read caching def getGLIF(self, glyphName): """ @@ -229,18 +234,18 @@ def getGLIF(self, glyphName): fileName = self.contents.get(glyphName) path = None if fileName is not None: - path = os.path.join(self.dirName, fileName) + path = self._fs.joinPath(self.dirName, fileName) if glyphName not in self._glifCache: needRead = True - elif fileName is not None and os.path.getmtime(path) != self._glifCache[glyphName][1]: + elif fileName is not None and self._fs.getFileModificationTime(path) != self._glifCache[glyphName][1]: needRead = True if needRead: fileName = self.contents[glyphName] - if not os.path.exists(path): + if not self._fs.exists(path): raise KeyError(glyphName) - with open(path, "rb") as f: + with self._fs.open(path, "rb") as f: text = f.read() - self._glifCache[glyphName] = (text, os.path.getmtime(path)) + self._glifCache[glyphName] = (text, self._fs.getFileModificationTime(path)) return self._glifCache[glyphName][0] def getGLIFModificationTime(self, glyphName): @@ -297,71 +302,71 @@ def readGlyph(self, glyphName, glyphObject=None, pointPen=None): formatVersions = (1, 2) _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions) - def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None): - """ - Write a .glif file for 'glyphName' to the glyph set. The - 'glyphObject' argument can be any kind of object (even None); - the writeGlyph() method will attempt to get the following - attributes from it: - "width" the advance with of the glyph - "height" the advance height of the glyph - "unicodes" a list of unicode values for this glyph - "note" a string - "lib" a dictionary containing custom data - "image" a dictionary containing image data - "guidelines" a list of guideline data dictionaries - "anchors" a list of anchor data dictionaries - - All attributes are optional: if 'glyphObject' doesn't - have the attribute, it will simply be skipped. - - To write outline data to the .glif file, writeGlyph() needs - a function (any callable object actually) that will take one - argument: an object that conforms to the PointPen protocol. - The function will be called by writeGlyph(); it has to call the - proper PointPen methods to transfer the outline to the .glif file. - - The GLIF format version will be chosen based on the ufoFormatVersion - passed during the creation of this object. If a particular format - version is desired, it can be passed with the formatVersion argument. - """ - if formatVersion is None: - if self.ufoFormatVersion >= 3: - formatVersion = 2 - else: - formatVersion = 1 - else: - if formatVersion not in supportedGLIFFormatVersions: - raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) - if formatVersion == 2 and self.ufoFormatVersion < 3: - raise GlifLibError("Unsupported GLIF format version (%d) for UFO format version %d." % (formatVersion, self.ufoFormatVersion)) - self._purgeCachedGLIF(glyphName) - data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion) - fileName = self.contents.get(glyphName) - if fileName is None: - fileName = self.glyphNameToFileName(glyphName, self) - self.contents[glyphName] = fileName - if self._reverseContents is not None: - self._reverseContents[fileName.lower()] = glyphName - path = os.path.join(self.dirName, fileName) - if os.path.exists(path): - with open(path, "rb") as f: - oldData = f.read() - if data == oldData: - return - with open(path, "wb") as f: - f.write(tobytes(data, encoding="utf-8")) - - def deleteGlyph(self, glyphName): - """Permanently delete the glyph from the glyph set on disk. Will - raise KeyError if the glyph is not present in the glyph set. - """ - self._purgeCachedGLIF(glyphName) - fileName = self.contents[glyphName] - os.remove(os.path.join(self.dirName, fileName)) - if self._reverseContents is not None: - del self._reverseContents[self.contents[glyphName].lower()] - del self.contents[glyphName] + # def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None): + # """ + # Write a .glif file for 'glyphName' to the glyph set. The + # 'glyphObject' argument can be any kind of object (even None); + # the writeGlyph() method will attempt to get the following + # attributes from it: + # "width" the advance with of the glyph + # "height" the advance height of the glyph + # "unicodes" a list of unicode values for this glyph + # "note" a string + # "lib" a dictionary containing custom data + # "image" a dictionary containing image data + # "guidelines" a list of guideline data dictionaries + # "anchors" a list of anchor data dictionaries + + # All attributes are optional: if 'glyphObject' doesn't + # have the attribute, it will simply be skipped. + + # To write outline data to the .glif file, writeGlyph() needs + # a function (any callable object actually) that will take one + # argument: an object that conforms to the PointPen protocol. + # The function will be called by writeGlyph(); it has to call the + # proper PointPen methods to transfer the outline to the .glif file. + + # The GLIF format version will be chosen based on the ufoFormatVersion + # passed during the creation of this object. If a particular format + # version is desired, it can be passed with the formatVersion argument. + # """ + # if formatVersion is None: + # if self.ufoFormatVersion >= 3: + # formatVersion = 2 + # else: + # formatVersion = 1 + # else: + # if formatVersion not in supportedGLIFFormatVersions: + # raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) + # if formatVersion == 2 and self.ufoFormatVersion < 3: + # raise GlifLibError("Unsupported GLIF format version (%d) for UFO format version %d." % (formatVersion, self.ufoFormatVersion)) + # self._purgeCachedGLIF(glyphName) + # data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion) + # fileName = self.contents.get(glyphName) + # if fileName is None: + # fileName = self.glyphNameToFileName(glyphName, self) + # self.contents[glyphName] = fileName + # if self._reverseContents is not None: + # self._reverseContents[fileName.lower()] = glyphName + # path = os.path.join(self.dirName, fileName) + # if os.path.exists(path): + # with open(path, "rb") as f: + # oldData = f.read() + # if data == oldData: + # return + # with open(path, "wb") as f: + # f.write(tobytes(data, encoding="utf-8")) + + # def deleteGlyph(self, glyphName): + # """Permanently delete the glyph from the glyph set on disk. Will + # raise KeyError if the glyph is not present in the glyph set. + # """ + # self._purgeCachedGLIF(glyphName) + # fileName = self.contents[glyphName] + # os.remove(os.path.join(self.dirName, fileName)) + # if self._reverseContents is not None: + # del self._reverseContents[self.contents[glyphName].lower()] + # del self.contents[glyphName] # dict-like support @@ -428,16 +433,6 @@ def getImageReferences(self, glyphNames=None): images[glyphName] = _fetchImageFileName(text) return images - # internal methods - - def _readPlist(self, path): - try: - with open(path, "rb") as f: - data = readPlist(f) - return data - except: - raise GlifLibError("The file %s could not be read." % path) - # ----------------------- # Glyph Name to File Name diff --git a/Lib/ufoLib/plistFromTree.py b/Lib/ufoLib/plistFromTree.py index 1a74395..31ce615 100644 --- a/Lib/ufoLib/plistFromTree.py +++ b/Lib/ufoLib/plistFromTree.py @@ -1,4 +1,4 @@ -from ufoLib.plistlib import PlistParser +from ufoLib.plistlibShim import PlistParser """ Small helper module to parse Plist-formatted data from trees as created by xmlTreeBuilder. diff --git a/Lib/ufoLib/plistlib.py b/Lib/ufoLib/plistlibShim.py similarity index 100% rename from Lib/ufoLib/plistlib.py rename to Lib/ufoLib/plistlibShim.py From 622b7c9352dc853b8779a9c70b4d936a4583ce86 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Tue, 3 May 2016 09:59:26 -0400 Subject: [PATCH 02/71] ZIPs will contain a single root directory that packages will not. Abstract this. --- Lib/ufoLib/filesystem.py | 124 +++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 1e42aaa..cfbcc33 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -1,5 +1,6 @@ import os from io import StringIO +import zipfile from fs.osfs import OSFS from fs.zipfs import ZipFS, ZipOpenError from ufoLib.plistlibShim import readPlist, writePlist @@ -13,7 +14,8 @@ class FileSystem(object): - def __init__(self, path): + def __init__(self, path, mode="r"): + self._root = None self._path = "" if isinstance(path, basestring): self._path = path @@ -21,16 +23,79 @@ def __init__(self, path): raise UFOLibError("The specified UFO doesn't exist.") if os.path.isdir(path): path = OSFS(path) + elif zipfile.is_zipfile(path): + path = ZipFS(path, mode=mode, allow_zip_64=True) + roots = path.listdir("") + if len(roots) > 1: + raise UFOLibError("The UFO contains more than one root.") + self._root = roots[0] else: - try: - path = ZipFS(path, mode="r", allow_zip_64=True) - except ZipOpenError: - raise UFOLibError("The specified UFO is not in a proper format.") + raise UFOLibError("The specified UFO is not in a proper format.") self._fs = path def close(self): + self._fsClose() + + # -------- + # fs Calls + # -------- + + """ + All actual low-level file system interaction + MUST be done through these methods. + + This is necessary because ZIPs will have a + top level root directory that packages will + not have. + """ + + def _fsRootPath(self, path): + if self._root is None: + return path + return self.joinPath(self._root, path) + + def _fsUnRootPath(self, path): + if self._root is None: + return path + return self.relativePath(path, self._root) + + def _fsClose(self): self._fs.close() + def _fsOpen(self, path, mode="r", encoding=None): + path = self._fsRootPath(path) + f = self._fs.open(path, mode, encoding=encoding) + return f + + def _fsRemove(self, path): + path = self._fsRootPath(path) + self._fs.remove(path) + + def _fsMakeDirectory(self, path): + path = self._fsRootPath(path) + self._fs.mkdir(path) + + def _fsRemoveDirectory(self, path): + path = self._fsRootPath(path) + self._fs.removedir(path) + + def _fsExists(self, path): + path = self._fsRootPath(path) + return self._fs.exists(path) + + def _fsIsDirectory(self, path): + path = self._fsRootPath(path) + return self._fs.isdir(path) + + def _fsListDirectory(self, path): + path = self._fsRootPath(path) + return self._fs.listdir(path) + + def _fsGetFileModificationTime(self, path): + path = self._fsRootPath(path) + info = self._fs.getinfo(path) + return info["modified_time"] + # ----------------- # Path Manipulation # ----------------- @@ -52,10 +117,10 @@ def relativePath(self, path, start): # --------- def exists(self, path): - return self._fs.exists(path) + return self._fsExists(path) def isDirectory(self, path): - return self._fs.isdir(path) + return self._fsIsDirectory(path) def listDirectory(self, path, recurse=False): return self._listDirectory(path, recurse=recurse) @@ -64,14 +129,18 @@ def _listDirectory(self, path, recurse=False, depth=0, maxDepth=100): if depth > maxDepth: raise UFOLibError("Maximum recusion depth reached.") result = [] - for fileName in self._fs.listdir(path): + for fileName in self._fsListDirectory(path): p = self.joinPath(path, fileName) - if os.path.isdir(p) and recurse: + if self.isDirectory(p) and recurse: result += self._listDirectory(p, recurse=True, depth=depth+1, maxDepth=maxDepth) else: result.append(p) return result + def makeDirectory(self, path): + if not self.exists(path): + self._fsMakeDirectory(path) + # ----------- # File Opener # ----------- @@ -97,7 +166,7 @@ def open(self, path, mode="r", encoding=None): raise UFOLibError("%s is a directory." % path) if mode in ("w", "wb"): self._buildDirectoryTree(path) - f = self._fs.open(path, mode, encoding=encoding) + f = self._fsOpen(path, mode, encoding=encoding) return f def _buildDirectoryTree(self, path): @@ -111,8 +180,7 @@ def _buildDirectoryTree(self, path): for d in directoryTree: d = self.joinPath(built, d) p = self.joinPath(self._path, d) - if not self.exists(p): - self._fs.mkdir(p) + self.makeDirectory(p) built = d # ------------------ @@ -127,8 +195,7 @@ def getFileModificationTime(self, path): """ if not self.exists(path): return None - info = self._fs.getinfo(path) - return info["modified_time"] + return self._fsGetFileModificationTime(path) # -------------- # Raw Read/Write @@ -204,17 +271,17 @@ def _removeFileForPath(self, path, raiseErrorIfMissing=False): raise UFOLibError("The file %s does not exist." % path) else: if self.isDirectory(path): - self._fs.removedir(path) + self._fsRemoveDirectory(path) else: - self._fs.remove(path) + self._fsRemove(path) directory = self.directoryName(path) self._removeEmptyDirectoriesForPath(directory) def _removeEmptyDirectoriesForPath(self, directory): if not self.exists(directory): return - if not len(self._fs.listdir(directory)): - self._fs.removedir(directory) + if not len(self._fsListDirectory(directory)): + self._fsRemoveDirectory(directory) else: return directory = self.directoryName(directory) @@ -256,26 +323,7 @@ def readPlist(self, path, default=None): path = os.path.dirname(path) path = os.path.join(path, "TestData", "TestFont1 (UFO2).ufo") - import zipfile - - def _packDirectory(z, d, root=""): - for i in os.listdir(d): - p = os.path.join(d, i) - l = os.path.join(root, i) - if os.path.isdir(p): - _packDirectory(z, p, root=l) - else: - l = os.path.join(root, i) - z.write(p, l) - - p = path + ".zip" - if os.path.exists(p): - os.remove(p) - z = zipfile.ZipFile(p, "w") - _packDirectory(z, path) - z.close() - - path += ".zip" + # path += ".zip" reader = UFOReader(path) glyphSet = reader.getGlyphSet() From 59d723b9f82f0e1c5083ffe7590d325a47a07006 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Tue, 3 May 2016 10:17:48 -0400 Subject: [PATCH 03/71] GlyphSet tweaks. --- Lib/ufoLib/glifLib.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index 186a37c..b28266d 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -107,13 +107,13 @@ def __init__(self, dirName, fileSystem=None, glyphNameToFileNameFunc=None, ufoFo extension). The glyphNameToFileName function is called whenever a file name is created for a given glyph name. """ - self.dirName = dirName if ufoFormatVersion not in supportedUFOFormatVersions: raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion) if fileSystem is None: fileSystem = FileSystem(dirName) dirName = "" - self._fs = fileSystem + self.dirName = dirName + self.fileSystem = fileSystem self.ufoFormatVersion = ufoFormatVersion if glyphNameToFileNameFunc is None: glyphNameToFileNameFunc = glyphNameToFileName @@ -126,12 +126,12 @@ def rebuildContents(self): """ Rebuild the contents dict by loading contents.plist. """ - contentsPath = self._fs.joinPath(self.dirName, "contents.plist") - if not self._fs.exists(contentsPath): + contentsPath = self.fileSystem.joinPath(self.dirName, "contents.plist") + if not self.fileSystem.exists(contentsPath): # missing, consider the glyphset empty. contents = {} else: - contents = self._fs.readPlist(contentsPath) + contents = self.fileSystem.readPlist(contentsPath) # validate the contents invalidFormat = False if not isinstance(contents, dict): @@ -142,7 +142,7 @@ def rebuildContents(self): invalidFormat = True if not isinstance(fileName, basestring): invalidFormat = True - elif not self._fs.exists(self._fs.joinPath(self.dirName, fileName)): + elif not self.fileSystem.exists(self.fileSystem.joinPath(self.dirName, fileName)): raise GlifLibError("contents.plist references a file that does not exist: %s" % fileName) if invalidFormat: raise GlifLibError("contents.plist is not properly formatted") @@ -177,10 +177,10 @@ def getReverseContents(self): # layer info def readLayerInfo(self, info): - path = self._fs.joinPath(self.dirName, LAYERINFO_FILENAME) - if not self._fs.exists(path): + path = self.fileSystem.joinPath(self.dirName, LAYERINFO_FILENAME) + if not self.fileSystem.exists(path): return - infoDict = self._fs.readPlist(path) + infoDict = self.fileSystem.readPlist(path) if not isinstance(infoDict, dict): raise GlifLibError("layerinfo.plist is not properly formatted.") infoDict = validateLayerInfoVersion3Data(infoDict) @@ -234,18 +234,18 @@ def getGLIF(self, glyphName): fileName = self.contents.get(glyphName) path = None if fileName is not None: - path = self._fs.joinPath(self.dirName, fileName) + path = self.fileSystem.joinPath(self.dirName, fileName) if glyphName not in self._glifCache: needRead = True - elif fileName is not None and self._fs.getFileModificationTime(path) != self._glifCache[glyphName][1]: + elif fileName is not None and self.fileSystem.getFileModificationTime(path) != self._glifCache[glyphName][1]: needRead = True if needRead: fileName = self.contents[glyphName] - if not self._fs.exists(path): + if not self.fileSystem.exists(path): raise KeyError(glyphName) - with self._fs.open(path, "rb") as f: + with self.fileSystem.open(path, "rb") as f: text = f.read() - self._glifCache[glyphName] = (text, self._fs.getFileModificationTime(path)) + self._glifCache[glyphName] = (text, self.fileSystem.getFileModificationTime(path)) return self._glifCache[glyphName][0] def getGLIFModificationTime(self, glyphName): From 08f8f114cbad7b9c8910af8ff4a696ecb8c06c13 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 4 May 2016 10:53:43 -0400 Subject: [PATCH 04/71] Implement writing to package or ZIP. This has only been lightly tested. It slightly breaks defcon, but I'll deal with that later. --- Lib/ufoLib/__init__.py | 257 +++++++++------------------------------ Lib/ufoLib/filesystem.py | 135 ++++++++++++++++---- Lib/ufoLib/glifLib.py | 186 ++++++++++++++-------------- 3 files changed, 265 insertions(+), 313 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 108299f..602d61e 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -352,7 +352,6 @@ def _readLayerContents(self): """ if self._formatVersion < 3: return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] - # read the file on disk contents = self.readPlist(LAYERCONTENTS_FILENAME) valid, error = layerContentsValidator(contents, self._path) if not valid: @@ -471,30 +470,43 @@ def readImage(self, fileName): # ---------- -class UFOWriter(object): +class UFOWriter(FileSystem): """Write the various components of the .ufo.""" - def __init__(self, path, formatVersion=3, fileCreator="org.robofab.ufoLib"): + def __init__(self, path, formatVersion=3, structure=None, fileCreator="org.robofab.ufoLib"): + # XXX + # for testing only + if isinstance(path, basestring) and structure is None: + ext = os.path.splitext(path)[-1].lower() + if ext == ".ufo": + structure = "package" + else: + structure = "zip" + # /XXX + if formatVersion not in supportedUFOFormatVersions: raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) - # establish some basic stuff - self._path = path - self._formatVersion = formatVersion - self._fileCreator = fileCreator - self._downConversionKerningData = None - # if the file already exists, get the format version. - # this will be needed for up and down conversion. + havePreviousFile = False + if isinstance(path, basestring) and os.path.exists(path): + havePreviousFile = True + super(UFOWriter, self).__init__(path, mode="w", structure=structure) previousFormatVersion = None - if os.path.exists(path): - metaInfo = self._getPlist(METAINFO_FILENAME) + if havePreviousFile: + metaInfo = self.readPlist(METAINFO_FILENAME) previousFormatVersion = metaInfo.get("formatVersion") try: previousFormatVersion = int(previousFormatVersion) except: + self.close() raise UFOLibError("The existing metainfo.plist is not properly formatted.") if previousFormatVersion not in supportedUFOFormatVersions: + self.close() raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) + # establish some basic stuff + self._formatVersion = formatVersion + self._fileCreator = fileCreator + self._downConversionKerningData = None # catch down conversion if previousFormatVersion is not None and previousFormatVersion > formatVersion: raise UFOLibError("The UFO located at this path is a higher version (%d) than the version (%d) that is trying to be written. This is not supported." % (previousFormatVersion, formatVersion)) @@ -529,149 +541,22 @@ def _get_fileCreator(self): fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.") - # support methods - - _getPlist = _getPlist - - def _writePlist(self, fileName, data): - """ - Write a property list. The errors that - could be raised during the writing of - a plist are unpredictable and/or too - large to list, so, a blind try: except: - is done. If an exception occurs, a - UFOLibError will be raised. - """ - self._makeDirectory() - path = os.path.join(self._path, fileName) - try: - data = writePlistAtomically(data, path) - except: - raise UFOLibError("The data for the file %s could not be written because it is not properly formatted." % fileName) - - def _deleteFile(self, fileName): - path = os.path.join(self._path, fileName) - if os.path.exists(path): - os.remove(path) - - def _makeDirectory(self, subDirectory=None): - path = self._path - if subDirectory: - path = os.path.join(self._path, subDirectory) - if not os.path.exists(path): - os.makedirs(path) - return path - - def _buildDirectoryTree(self, path): - directory, fileName = os.path.split(path) - directoryTree = [] - while directory: - directory, d = os.path.split(directory) - directoryTree.append(d) - directoryTree.reverse() - built = "" - for d in directoryTree: - d = os.path.join(built, d) - p = os.path.join(self._path, d) - if not os.path.exists(p): - os.mkdir(p) - built = d - - def _removeFileForPath(self, path, raiseErrorIfMissing=False): - originalPath = path - path = os.path.join(self._path, path) - if not os.path.exists(path): - if raiseErrorIfMissing: - raise UFOLibError("The file %s does not exist." % path) - else: - if os.path.isdir(path): - shutil.rmtree(path) - else: - os.remove(path) - # remove any directories that are now empty - self._removeEmptyDirectoriesForPath(os.path.dirname(originalPath)) - - def _removeEmptyDirectoriesForPath(self, directory): - absoluteDirectory = os.path.join(self._path, directory) - if not os.path.exists(absoluteDirectory): - return - if not len(os.listdir(absoluteDirectory)): - shutil.rmtree(absoluteDirectory) - else: - return - directory = os.path.dirname(directory) - if directory: - self._removeEmptyDirectoriesForPath(directory) - - # file system interaction - - def writeBytesToPath(self, path, data, encoding=None): - """ - Write bytes to path. If needed, the directory tree - for the given path will be built. The path must be - relative to the UFO. An encoding may be passed if needed. - """ - fullPath = os.path.join(self._path, path) - if os.path.exists(fullPath) and os.path.isdir(fullPath): - raise UFOLibError("A directory exists at %s." % path) - self._buildDirectoryTree(path) - if encoding: - data = StringIO(data).encode(encoding) - writeDataFileAtomically(data, fullPath) - - def getFileObjectForPath(self, path, encoding=None): - """ - Creates a write mode file object at path. If needed, - the directory tree for the given path will be built. - The path must be relative to the UFO. An encoding may - be passed if needed. - - Note: The caller is responsible for closing the open file. - """ - fullPath = os.path.join(self._path, path) - if os.path.exists(fullPath) and os.path.isdir(fullPath): - raise UFOLibError("A directory exists at %s." % path) - self._buildDirectoryTree(path) - return open(fullPath, "w", encoding=encoding) - - def removeFileForPath(self, path): - """ - Remove the file (or directory) at path. The path - must be relative to the UFO. This is only allowed - for files in the data and image directories. - """ - # make sure that only data or images is being changed - d = path - parts = [] - while d: - d, p = os.path.split(d) - if p: - parts.append(p) - if parts[-1] not in ("images", "data"): - raise UFOLibError("Removing \"%s\" is not legal." % path) - # remove the file - self._removeFileForPath(path, raiseErrorIfMissing=True) - def copyFromReader(self, reader, sourcePath, destPath): """ Copy the sourcePath in the provided UFOReader to destPath - in this writer. The paths must be relative. They may represent - directories or paths. This uses the most memory efficient - method possible for copying the data possible. + in this writer. The paths must be relative. This only + works with individual files, not directories. """ if not isinstance(reader, UFOReader): raise UFOLibError("The reader must be an instance of UFOReader.") - fullSourcePath = os.path.join(reader._path, sourcePath) - if not reader._checkForFile(fullSourcePath): - raise UFOLibError("No file named \"%s\" to copy from." % sourcePath) - fullDestPath = os.path.join(self._path, destPath) - if os.path.exists(fullDestPath): - raise UFOLibError("A file named \"%s\" already exists." % sourcePath) - self._buildDirectoryTree(destPath) - if os.path.isdir(fullSourcePath): - shutil.copytree(fullSourcePath, fullDestPath) - else: - shutil.copy(fullSourcePath, fullDestPath) + if not reader.exists(sourcePath): + raise UFOLibError("The reader does not have data located at \"%s\"." % sourcePath) + if reader.isDirectory(sourcePath): + raise UFOLibError("Directories can not be copied from a reader to a writer.") + if self.exists(destPath): + raise UFOLibError("A file named \"%s\" already exists." % destPath) + data = reader.readBytesFromPath(sourcePath) + self.writeBytesToPath(data, destPath) # UFO mod time @@ -681,7 +566,9 @@ def setModificationTime(self): This is never called automatically. It is up to the caller to call this when finished working on the UFO. """ - os.utime(self._path, None) + path = self.path + if path is not None: + os.utime(path, None) # metainfo.plist @@ -690,7 +577,7 @@ def _writeMetaInfo(self): creator=self._fileCreator, formatVersion=self._formatVersion ) - self._writePlist(METAINFO_FILENAME, metaInfo) + self.writePlist(METAINFO_FILENAME, metaInfo) # groups.plist @@ -759,9 +646,9 @@ def writeGroups(self, groups): for key, value in list(groups.items()): groupsNew[key] = list(value) if groupsNew: - self._writePlist(GROUPS_FILENAME, groupsNew) + self.writePlist(GROUPS_FILENAME, groupsNew) else: - self._deleteFile(GROUPS_FILENAME) + self.remove(GROUPS_FILENAME) # fontinfo.plist @@ -795,7 +682,7 @@ def writeInfo(self, info): infoData = validateInfoVersion2Data(infoData) infoData = _convertFontInfoDataVersion2ToVersion1(infoData) # write file - self._writePlist(FONTINFO_FILENAME, infoData) + self.writePlist(FONTINFO_FILENAME, infoData) # kerning.plist @@ -841,9 +728,9 @@ def writeKerning(self, kerning): kerningDict[left] = {} kerningDict[left][right] = value if kerningDict: - self._writePlist(KERNING_FILENAME, kerningDict) + self.writePlist(KERNING_FILENAME, kerningDict) else: - self._deleteFile(KERNING_FILENAME) + self.remove(KERNING_FILENAME) # lib.plist @@ -856,9 +743,9 @@ def writeLib(self, libDict): if not valid: raise UFOLibError(message) if libDict: - self._writePlist(LIB_FILENAME, libDict) + self.writePlist(LIB_FILENAME, libDict) else: - self._deleteFile(LIB_FILENAME) + self.remove(LIB_FILENAME) # features.fea @@ -871,9 +758,7 @@ def writeFeatures(self, features): raise UFOLibError("features.fea is not allowed in UFO Format Version 1.") if not isinstance(features, basestring): raise UFOLibError("The features are not text.") - self._makeDirectory() - path = os.path.join(self._path, FEATURES_FILENAME) - writeFileAtomically(features, path) + self.writeBytesToPath(FEATURES_FILENAME, features.encode("utf8")) # glyph sets & layers @@ -883,7 +768,7 @@ def _readLayerContents(self): are available on disk. """ # read the file on disk - raw = self._getPlist(LAYERCONTENTS_FILENAME) + raw = self.readPlist(LAYERCONTENTS_FILENAME) contents = {} valid, error = layerContentsValidator(raw, self._path) if not valid: @@ -912,7 +797,7 @@ def writeLayerContents(self, layerOrder=None): if set(layerOrder) != set(self.layerContents.keys()): raise UFOLibError("The layer order contents does not match the glyph sets that have been created.") layerContents = [(layerName, self.layerContents[layerName]) for layerName in layerOrder] - self._writePlist(LAYERCONTENTS_FILENAME, layerContents) + self.writePlist(LAYERCONTENTS_FILENAME, layerContents) def _findDirectoryForLayerName(self, layerName): foundDirectory = None @@ -957,12 +842,10 @@ def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc return self._getGlyphSetFormatVersion3(layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc) def _getGlyphSetFormatVersion1(self, glyphNameToFileNameFunc=None): - glyphDir = self._makeDirectory(DEFAULT_GLYPHS_DIRNAME) - return GlyphSet(glyphDir, glyphNameToFileNameFunc, ufoFormatVersion=1) + return GlyphSet(DEFAULT_GLYPHS_DIRNAME, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=1) def _getGlyphSetFormatVersion2(self, glyphNameToFileNameFunc=None): - glyphDir = self._makeDirectory(DEFAULT_GLYPHS_DIRNAME) - return GlyphSet(glyphDir, glyphNameToFileNameFunc, ufoFormatVersion=2) + return GlyphSet(DEFAULT_GLYPHS_DIRNAME, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=2) def _getGlyphSetFormatVersion3(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None): # if the default flag is on, make sure that the default in the file @@ -993,13 +876,11 @@ def _getGlyphSetFormatVersion3(self, layerName=None, defaultLayer=True, glyphNam raise UFOLibError("The specified layer name is not a Unicode string.") directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.") # make the directory - path = os.path.join(self._path, directory) - if not os.path.exists(path): - self._makeDirectory(subDirectory=directory) + self.makeDirectory(directory) # store the mapping self.layerContents[layerName] = directory # load the glyph set - return GlyphSet(path, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3) + return GlyphSet(path, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3) def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False): """ @@ -1040,9 +921,7 @@ def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False): del self.layerContents[layerName] self.layerContents[newLayerName] = newDirectory # do the file system copy - oldDirectory = os.path.join(self._path, oldDirectory) - newDirectory = os.path.join(self._path, newDirectory) - shutil.move(oldDirectory, newDirectory) + self.move(oldDirectory, newDirectory) def deleteGlyphSet(self, layerName): """ @@ -1066,7 +945,7 @@ def writeImage(self, fileName, data): valid, error = pngValidator(data=data) if not valid: raise UFOLibError(error) - path = os.path.join(IMAGES_DIRNAME, fileName) + path = self.joinPath(IMAGES_DIRNAME, fileName) self.writeBytesToPath(path, data) def removeImage(self, fileName): @@ -1076,8 +955,8 @@ def removeImage(self, fileName): """ if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - path = os.path.join(IMAGES_DIRNAME, fileName) - self.removeFileForPath(path) + path = self.joinPath(IMAGES_DIRNAME, fileName) + self.remove(path) def copyImageFromReader(self, reader, sourceFileName, destFileName): """ @@ -1087,8 +966,8 @@ def copyImageFromReader(self, reader, sourceFileName, destFileName): """ if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - sourcePath = os.path.join("images", sourceFileName) - destPath = os.path.join("images", destFileName) + sourcePath = reader.joinPath(IMAGES_DIRNAME, sourceFileName) + destPath = self.joinPath(IMAGES_DIRNAME, destFileName) self.copyFromReader(reader, sourcePath, destPath) @@ -1123,26 +1002,6 @@ def writePlistAtomically(obj, path): data = f.getvalue() writeDataFileAtomically(data, path) -def writeFileAtomically(text, path, encoding="utf-8"): - """ - Write text into a file at path. Do this sort of atomically - making it harder to cause corrupt files. This also checks to see - if text matches the text that is already in the file at path. - If so, the file is not rewritten so that the modification date - is preserved. An encoding may be passed if needed. - """ - if os.path.exists(path): - with open(path, "r", encoding=encoding) as f: - oldText = f.read() - if text == oldText: - return - # if the text is empty, remove the existing file - if not text: - os.remove(path) - if text: - with open(path, "w", encoding=encoding) as f: - f.write(text) - def writeDataFileAtomically(data, path): """ Write data into a file at path. Do this sort of atomically diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index cfbcc33..6768e5e 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -1,5 +1,5 @@ import os -from io import StringIO +from io import StringIO, BytesIO, open import zipfile from fs.osfs import OSFS from fs.zipfs import ZipFS, ZipOpenError @@ -11,26 +11,57 @@ except NameError: basestring = str +def sniffFileStructure(path): + if zipfile.is_zipfile(path): + return "zip" + elif os.path.isdir(path): + return "package" + raise UFOLibError("The specified UFO does not have a known structure.") + class FileSystem(object): - def __init__(self, path, mode="r"): + def __init__(self, path, mode="r", structure=None): + """ + path can be a path or a fs file system object. + + mode can be r or w. + + structure is only applicable in "w" mode. Options: + None: existing structure + package: package structure + zip: zipped package + + mode and structure are both ignored if a + fs file system object is given for path. + """ self._root = None self._path = "" if isinstance(path, basestring): self._path = path - if not os.path.exists(path): - raise UFOLibError("The specified UFO doesn't exist.") - if os.path.isdir(path): + if mode == "w": + if os.path.exists(path): + existingStructure = sniffFileStructure(path) + if structure is not None: + if structure != existingStructure: + raise UFOLibError("A UFO with a different structure already exists at the given path.") + else: + structure = existingStructure + elif mode == "r": + if not os.path.exists(path): + raise UFOLibError("The specified UFO doesn't exist.") + structure = sniffFileStructure(path) + if structure == "package": path = OSFS(path) - elif zipfile.is_zipfile(path): - path = ZipFS(path, mode=mode, allow_zip_64=True) + elif structure == "zip": + path = ZipFS(path, mode=mode, allow_zip_64=True, encoding="utf8") roots = path.listdir("") - if len(roots) > 1: + if not roots: + self._root = "contents" + elif len(roots) > 1: raise UFOLibError("The UFO contains more than one root.") - self._root = roots[0] - else: - raise UFOLibError("The specified UFO is not in a proper format.") + else: + self._root = roots[0] self._fs = path def close(self): @@ -73,12 +104,21 @@ def _fsRemove(self, path): def _fsMakeDirectory(self, path): path = self._fsRootPath(path) - self._fs.mkdir(path) + self._fs.makedir(path) def _fsRemoveDirectory(self, path): path = self._fsRootPath(path) self._fs.removedir(path) + def _fsMove(self, path1, path2): + if self.isDirectory(path1): + meth = self._fs.movedir + else: + meth = self._fs.move + path1 = self._fsRootPath(path1) + path2 = self._fsRootPath(path2) + meth(path1, path2) + def _fsExists(self, path): path = self._fsRootPath(path) return self._fs.exists(path) @@ -179,8 +219,7 @@ def _buildDirectoryTree(self, path): built = "" for d in directoryTree: d = self.joinPath(built, d) - p = self.joinPath(self._path, d) - self.makeDirectory(p) + self.makeDirectory(d) built = d # ------------------ @@ -223,7 +262,7 @@ def writeBytesToPath(self, path, data, encoding=None): """ if encoding: data = StringIO(data).encode(encoding) - self._writeFileAtomically(data, fullPath) + self._writeFileAtomically(data, path) def _writeFileAtomically(self, data, path): """ @@ -232,7 +271,7 @@ def _writeFileAtomically(self, data, path): if data matches the data that is already in the file at path. If so, the file is not rewritten so that the modification date is preserved. - """ + """ assert isinstance(data, bytes) if self.exists(path): f = self.open(path, "rb") @@ -288,6 +327,17 @@ def _removeEmptyDirectoriesForPath(self, directory): if directory: self._removeEmptyDirectoriesForPath(directory) + # ---- + # Move + # ---- + + def move(self, path1, path2): + if not self.exists(path1): + raise UFOLibError("%s does not exist." % path1) + if self.exists(path2): + raise UFOLibError("%s already exists." % path2) + self._fsMove(path1, path2) + # -------------- # Property Lists # -------------- @@ -315,16 +365,59 @@ def readPlist(self, path, default=None): except: raise UFOLibError("The file %s could not be read." % fileName) + def writePlist(self, path, obj): + """ + Write a property list. + + Do this sort of atomically, making it harder to + cause corrupt files, for example when writePlist + encounters an error halfway during write. This + also checks to see if text matches the text that + is already in the file at path. If so, the file + is not rewritten so that the modification date + is preserved. + + The errors that could be raised during the writing + of a plist are unpredictable and/or too large to list, + so, a blind try: except: is done. If an exception occurs, + a UFOLibError will be raised. + """ + try: + f = BytesIO() + writePlist(obj, f) + data = f.getvalue() + except: + raise UFOLibError("The data for the file %s could not be written because it is not properly formatted." % path) + self.writeBytesToPath(path, data) + if __name__ == "__main__": - from ufoLib import UFOReader + import shutil + from defcon import Font + path = os.path.dirname(__file__) path = os.path.dirname(path) path = os.path.dirname(path) path = os.path.join(path, "TestData", "TestFont1 (UFO2).ufo") - # path += ".zip" + font = Font(path) + print font["A"].bounds - reader = UFOReader(path) - glyphSet = reader.getGlyphSet() - print glyphSet.getGLIF("A") + path = path.replace(".ufo", " w.ufoz") + + if os.path.exists(path): + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + + font.save(path) + + font = Font(path) + print font["A"].bounds + + if os.path.exists(path): + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index b28266d..bccabbf 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -165,14 +165,13 @@ def getReverseContents(self): self._reverseContents = d return self._reverseContents - # def writeContents(self): - # """ - # Write the contents.plist file out to disk. Call this method when - # you're done writing glyphs. - # """ - # contentsPath = os.path.join(self.dirName, "contents.plist") - # with open(contentsPath, "wb") as f: - # writePlist(self.contents, f) + def writeContents(self): + """ + Write the contents.plist file out to disk. Call this method when + you're done writing glyphs. + """ + path = self.fileSystem.joinPath(self.dirName, "contents.plist") + self.fileSystem.writePlist(path, self.contents) # layer info @@ -191,26 +190,25 @@ def readLayerInfo(self, info): except AttributeError: raise GlifLibError("The supplied layer info object does not support setting a necessary attribute (%s)." % attr) - # def writeLayerInfo(self, info): - # if self.ufoFormatVersion < 3: - # raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion) - # # gather data - # infoData = {} - # for attr in list(layerInfoVersion3ValueData.keys()): - # if hasattr(info, attr): - # try: - # value = getattr(info, attr) - # except AttributeError: - # raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr) - # if value is None: - # continue - # infoData[attr] = value - # # validate - # infoData = validateLayerInfoVersion3Data(infoData) - # # write file - # path = os.path.join(self.dirName, LAYERINFO_FILENAME) - # with open(path, "wb") as f: - # writePlist(infoData, f) + def writeLayerInfo(self, info): + if self.ufoFormatVersion < 3: + raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion) + # gather data + infoData = {} + for attr in list(layerInfoVersion3ValueData.keys()): + if hasattr(info, attr): + try: + value = getattr(info, attr) + except AttributeError: + raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr) + if value is None: + continue + infoData[attr] = value + # validate + infoData = validateLayerInfoVersion3Data(infoData) + # write file + path = self.fileSystem.joinPath(self.dirName, LAYERINFO_FILENAME) + self.fileSystem.writePlist(infoData, path) # # read caching @@ -302,71 +300,73 @@ def readGlyph(self, glyphName, glyphObject=None, pointPen=None): formatVersions = (1, 2) _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions) - # def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None): - # """ - # Write a .glif file for 'glyphName' to the glyph set. The - # 'glyphObject' argument can be any kind of object (even None); - # the writeGlyph() method will attempt to get the following - # attributes from it: - # "width" the advance with of the glyph - # "height" the advance height of the glyph - # "unicodes" a list of unicode values for this glyph - # "note" a string - # "lib" a dictionary containing custom data - # "image" a dictionary containing image data - # "guidelines" a list of guideline data dictionaries - # "anchors" a list of anchor data dictionaries - - # All attributes are optional: if 'glyphObject' doesn't - # have the attribute, it will simply be skipped. - - # To write outline data to the .glif file, writeGlyph() needs - # a function (any callable object actually) that will take one - # argument: an object that conforms to the PointPen protocol. - # The function will be called by writeGlyph(); it has to call the - # proper PointPen methods to transfer the outline to the .glif file. - - # The GLIF format version will be chosen based on the ufoFormatVersion - # passed during the creation of this object. If a particular format - # version is desired, it can be passed with the formatVersion argument. - # """ - # if formatVersion is None: - # if self.ufoFormatVersion >= 3: - # formatVersion = 2 - # else: - # formatVersion = 1 - # else: - # if formatVersion not in supportedGLIFFormatVersions: - # raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) - # if formatVersion == 2 and self.ufoFormatVersion < 3: - # raise GlifLibError("Unsupported GLIF format version (%d) for UFO format version %d." % (formatVersion, self.ufoFormatVersion)) - # self._purgeCachedGLIF(glyphName) - # data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion) - # fileName = self.contents.get(glyphName) - # if fileName is None: - # fileName = self.glyphNameToFileName(glyphName, self) - # self.contents[glyphName] = fileName - # if self._reverseContents is not None: - # self._reverseContents[fileName.lower()] = glyphName - # path = os.path.join(self.dirName, fileName) - # if os.path.exists(path): - # with open(path, "rb") as f: - # oldData = f.read() - # if data == oldData: - # return - # with open(path, "wb") as f: - # f.write(tobytes(data, encoding="utf-8")) - - # def deleteGlyph(self, glyphName): - # """Permanently delete the glyph from the glyph set on disk. Will - # raise KeyError if the glyph is not present in the glyph set. - # """ - # self._purgeCachedGLIF(glyphName) - # fileName = self.contents[glyphName] - # os.remove(os.path.join(self.dirName, fileName)) - # if self._reverseContents is not None: - # del self._reverseContents[self.contents[glyphName].lower()] - # del self.contents[glyphName] + def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None): + """ + Write a .glif file for 'glyphName' to the glyph set. The + 'glyphObject' argument can be any kind of object (even None); + the writeGlyph() method will attempt to get the following + attributes from it: + "width" the advance with of the glyph + "height" the advance height of the glyph + "unicodes" a list of unicode values for this glyph + "note" a string + "lib" a dictionary containing custom data + "image" a dictionary containing image data + "guidelines" a list of guideline data dictionaries + "anchors" a list of anchor data dictionaries + + All attributes are optional: if 'glyphObject' doesn't + have the attribute, it will simply be skipped. + + To write outline data to the .glif file, writeGlyph() needs + a function (any callable object actually) that will take one + argument: an object that conforms to the PointPen protocol. + The function will be called by writeGlyph(); it has to call the + proper PointPen methods to transfer the outline to the .glif file. + + The GLIF format version will be chosen based on the ufoFormatVersion + passed during the creation of this object. If a particular format + version is desired, it can be passed with the formatVersion argument. + """ + if formatVersion is None: + if self.ufoFormatVersion >= 3: + formatVersion = 2 + else: + formatVersion = 1 + else: + if formatVersion not in supportedGLIFFormatVersions: + raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) + if formatVersion == 2 and self.ufoFormatVersion < 3: + raise GlifLibError("Unsupported GLIF format version (%d) for UFO format version %d." % (formatVersion, self.ufoFormatVersion)) + self._purgeCachedGLIF(glyphName) + data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion) + fileName = self.contents.get(glyphName) + if fileName is None: + fileName = self.glyphNameToFileName(glyphName, self) + self.contents[glyphName] = fileName + if self._reverseContents is not None: + self._reverseContents[fileName.lower()] = glyphName + self.fileSystem.makeDirectory(self.dirName) + path = self.fileSystem.joinPath(self.dirName, fileName) + if self.fileSystem.exists(path): + with self.fileSystem.open(path, "rb") as f: + oldData = f.read() + if data == oldData: + return + with self.fileSystem.open(path, "wb") as f: + f.write(tobytes(data, encoding="utf-8")) + + def deleteGlyph(self, glyphName): + """Permanently delete the glyph from the glyph set on disk. Will + raise KeyError if the glyph is not present in the glyph set. + """ + self._purgeCachedGLIF(glyphName) + fileName = self.contents[glyphName] + path = self.fileSystem.joinPath(self.dirName, fileName) + self.fileSystem.remove(path) + if self._reverseContents is not None: + del self._reverseContents[self.contents[glyphName].lower()] + del self.contents[glyphName] # dict-like support From a8c7788d663daf455c892e5a84400c7ee89788fb Mon Sep 17 00:00:00 2001 From: Masaya Nakamura Date: Wed, 11 May 2016 17:50:14 +0900 Subject: [PATCH 05/71] Fix _getGlyphSetFormatVersion3() --- Lib/ufoLib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 602d61e..5e9f468 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -880,7 +880,7 @@ def _getGlyphSetFormatVersion3(self, layerName=None, defaultLayer=True, glyphNam # store the mapping self.layerContents[layerName] = directory # load the glyph set - return GlyphSet(path, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3) + return GlyphSet(directory, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3) def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False): """ From db9467ff1ed0431da63803c201c549c54169196a Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 16 May 2016 10:24:09 -0400 Subject: [PATCH 06/71] Merge branch 'master' into ufo4 --- Lib/ufoLib/kerning.py | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 Lib/ufoLib/kerning.py diff --git a/Lib/ufoLib/kerning.py b/Lib/ufoLib/kerning.py new file mode 100644 index 0000000..f9ce242 --- /dev/null +++ b/Lib/ufoLib/kerning.py @@ -0,0 +1,87 @@ +def lookupKerningValue(pair, kerning, groups, fallback=0, glyphToFirstGroup=None, glyphToSecondGroup=None): + """ + Note: This expects kerning to be a flat dictionary + of kerning pairs, not the nested structure used + in kerning.plist. + + >>> groups = { + ... "public.kern1.O" : ["O", "D", "Q"], + ... "public.kern2.E" : ["E", "F"] + ... } + >>> kerning = { + ... ("public.kern1.O", "public.kern2.E") : -100, + ... ("public.kern1.O", "F") : -200, + ... ("D", "F") : -300 + ... } + >>> lookupKerningValue(("D", "F"), kerning, groups) + -300 + >>> lookupKerningValue(("O", "F"), kerning, groups) + -200 + >>> lookupKerningValue(("O", "E"), kerning, groups) + -100 + >>> lookupKerningValue(("O", "O"), kerning, groups) + 0 + >>> lookupKerningValue(("E", "E"), kerning, groups) + 0 + >>> lookupKerningValue(("E", "O"), kerning, groups) + 0 + >>> lookupKerningValue(("X", "X"), kerning, groups) + 0 + >>> lookupKerningValue(("public.kern1.O", "public.kern2.E"), + ... kerning, groups) + -100 + >>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups) + -200 + >>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups) + -100 + >>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups) + 0 + """ + # quickly check to see if the pair is in the kerning dictionary + if pair in kerning: + return kerning[pair] + # create glyph to group mapping + if glyphToFirstGroup is not None: + assert glyphToSecondGroup is not None + if glyphToSecondGroup is not None: + assert glyphToFirstGroup is not None + if glyphToFirstGroup is None: + glyphToFirstGroup = {} + glyphToSecondGroup = {} + for group, groupMembers in groups.items(): + if group.startswith("public.kern1."): + for glyph in groupMembers: + glyphToFirstGroup[glyph] = group + elif group.startswith("public.kern2."): + for glyph in groupMembers: + glyphToSecondGroup[glyph] = group + # get group names and make sure first and second are glyph names + first, second = pair + firstGroup = secondGroup = None + if first.startswith("public.kern1."): + firstGroup = first + first = None + else: + firstGroup = glyphToFirstGroup.get(first) + if second.startswith("public.kern2."): + secondGroup = second + second = None + else: + secondGroup = glyphToSecondGroup.get(second) + # make an ordered list of pairs to look up + pairs = [ + (first, second), + (first, secondGroup), + (firstGroup, second), + (firstGroup, secondGroup) + ] + # look up the pairs and return any matches + for pair in pairs: + if pair in kerning: + return kerning[pair] + # use the fallback value + return fallback + +if __name__ == "__main__": + import doctest + doctest.testmod() From 0b94969fcd033db56f1b179ba3e7bf73c82c9659 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 16 May 2016 14:04:56 -0400 Subject: [PATCH 07/71] Merge from master. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b06128f..78ecccb 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,12 @@ A low-level [UFO] reader and writer. [UFO] is a human-readable, XML-based file format that stores font source files. [UFO]: http://unifiedfontobject.org/ + + +UFO 4 +----- + +This is a branch in which we are **experimenting** with UFO 4 ideas/support. + +- Single file structure (99.9% sure this will be zip) +- Other stuff. \ No newline at end of file From 173669b2651bd2ec71580b94750c6df4d1dbb212 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 16 May 2016 14:57:46 -0400 Subject: [PATCH 08/71] Add a fallback for OSFS when fs isn't installed. --- Lib/ufoLib/filesystem.py | 76 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 6768e5e..1fb6708 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -1,8 +1,16 @@ import os +import shutil from io import StringIO, BytesIO, open import zipfile -from fs.osfs import OSFS -from fs.zipfs import ZipFS, ZipOpenError + +haveFS = False +try: + from fs.osfs import OSFS + from fs.zipfs import ZipFS, ZipOpenError + haveFS = True +except ImportError: + pass + from ufoLib.plistlibShim import readPlist, writePlist from ufoLib.errors import UFOLibError @@ -54,6 +62,8 @@ def __init__(self, path, mode="r", structure=None): if structure == "package": path = OSFS(path) elif structure == "zip": + if not haveFS: + raise UFOLibError("The fs module is required for reading and writing UFO ZIP.") path = ZipFS(path, mode=mode, allow_zip_64=True, encoding="utf8") roots = path.listdir("") if not roots: @@ -391,6 +401,68 @@ def writePlist(self, path, obj): self.writeBytesToPath(path, data) +class _NOFS(object): + + def __init__(self, path): + self._path = path + + def _absPath(self, path): + return os.path.join(self._path, path) + + def close(self): + pass + + def open(self, path, mode, encoding=None): + path = self._absPath(path) + return open(path, mode=mode, encoding=encoding) + + def remove(self, path): + path = self._absPath(path) + os.remove(path) + + def makedir(self, path): + path = self._absPath(path) + os.mkdir(path) + + def removedir(self, path): + path = self._absPath(path) + shutil.rmtree(path) + + def move(self, path1, path2): + path1 = self._absPath(path1) + path2 = self._absPath(path2) + os.move(path1, path2) + + def movedir(self, path1, path2): + path1 = self._absPath(path1) + path2 = self._absPath(path2) + shutil.move(path1, path2) + + def exists(self, path): + path = self._absPath(path) + return os.path.exists(path) + + def isdir(self, path): + path = self._absPath(path) + return os.path.isdir(path) + + def listdir(self, path): + path = self._absPath(path) + return os.listdir(path) + + def getinfo(self, path): + path = self._absPath(path) + stat = os.stat(path) + info = dict( + modified_time=stat.st_mtime + ) + return info + + +if not haveFS: + OSFS = _NOFS + + if __name__ == "__main__": import shutil from defcon import Font From ca217854baf795f8d91f08d1c479b87fb461303b Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 16 May 2016 14:58:32 -0400 Subject: [PATCH 09/71] Remove local testing code. --- Lib/ufoLib/filesystem.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 1fb6708..ad149d7 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -461,35 +461,3 @@ def getinfo(self, path): if not haveFS: OSFS = _NOFS - - -if __name__ == "__main__": - import shutil - from defcon import Font - - path = os.path.dirname(__file__) - path = os.path.dirname(path) - path = os.path.dirname(path) - path = os.path.join(path, "TestData", "TestFont1 (UFO2).ufo") - - font = Font(path) - print font["A"].bounds - - path = path.replace(".ufo", " w.ufoz") - - if os.path.exists(path): - if os.path.isdir(path): - shutil.rmtree(path) - else: - os.remove(path) - - font.save(path) - - font = Font(path) - print font["A"].bounds - - if os.path.exists(path): - if os.path.isdir(path): - shutil.rmtree(path) - else: - os.remove(path) From 29db50cd01b8e4cff03abb2f671fcb5d6bde185e Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 16 May 2016 15:02:19 -0400 Subject: [PATCH 10/71] Remove plistlib shim. --- Lib/ufoLib/__init__.py | 2 +- Lib/ufoLib/filesystem.py | 2 +- Lib/ufoLib/glifLib.py | 2 +- Lib/ufoLib/plistFromTree.py | 2 +- Lib/ufoLib/{plistlibShim.py => plistlib.py} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename Lib/ufoLib/{plistlibShim.py => plistlib.py} (100%) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 5e9f468..7dc0dd1 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -8,7 +8,7 @@ from ufoLib.validators import * from ufoLib.filenames import userNameToFileName from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning -from ufoLib.plistlibShim import readPlist, writePlist +from ufoLib.plistlib import readPlist, writePlist from ufoLib.errors import UFOLibError """ diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index ad149d7..3f82b56 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -11,7 +11,7 @@ except ImportError: pass -from ufoLib.plistlibShim import readPlist, writePlist +from ufoLib.plistlib import readPlist, writePlist from ufoLib.errors import UFOLibError try: diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index bccabbf..ae7cad6 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -17,7 +17,7 @@ from fontTools.misc.py23 import tobytes, tostr from ufoLib.filesystem import FileSystem from ufoLib.xmlTreeBuilder import buildTree, stripCharacterData -from ufoLib.plistlibShim import PlistWriter, readPlist, writePlist +from ufoLib.plistlib import PlistWriter, readPlist, writePlist from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen from ufoLib.filenames import userNameToFileName from ufoLib.validators import isDictEnough, genericTypeValidator, colorValidator,\ diff --git a/Lib/ufoLib/plistFromTree.py b/Lib/ufoLib/plistFromTree.py index 31ce615..1a74395 100644 --- a/Lib/ufoLib/plistFromTree.py +++ b/Lib/ufoLib/plistFromTree.py @@ -1,4 +1,4 @@ -from ufoLib.plistlibShim import PlistParser +from ufoLib.plistlib import PlistParser """ Small helper module to parse Plist-formatted data from trees as created by xmlTreeBuilder. diff --git a/Lib/ufoLib/plistlibShim.py b/Lib/ufoLib/plistlib.py similarity index 100% rename from Lib/ufoLib/plistlibShim.py rename to Lib/ufoLib/plistlib.py From 26863a2b796aad6f9ca9d369c89c35765a31bde3 Mon Sep 17 00:00:00 2001 From: Ben Kiel Date: Tue, 24 May 2016 11:07:53 -0500 Subject: [PATCH 11/71] Update __init__.py Add in ufoz for testing. --- Lib/ufoLib/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 7dc0dd1..b055652 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -481,6 +481,8 @@ def __init__(self, path, formatVersion=3, structure=None, fileCreator="org.robof ext = os.path.splitext(path)[-1].lower() if ext == ".ufo": structure = "package" + elif ext == ".ufoz": + structure = "zip" else: structure = "zip" # /XXX From 317214dfb1dad365f5072a6e95d099b445c3e97a Mon Sep 17 00:00:00 2001 From: Ben Kiel Date: Tue, 24 May 2016 11:21:00 -0500 Subject: [PATCH 12/71] If structure is zip, make sure saved file is ufos --- Lib/ufoLib/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index b055652..f580c11 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -485,6 +485,12 @@ def __init__(self, path, formatVersion=3, structure=None, fileCreator="org.robof structure = "zip" else: structure = "zip" + + if isinstance(structure, basestring) and structure == "zip": + file, ext = os.path.splitext(path) + ext.lower() + if ext != ".ufoz": + path = file + '.ufoz' # /XXX if formatVersion not in supportedUFOFormatVersions: From f5a8299e5902e137c0e4b44ad806b05f7d3f87b3 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Sat, 28 May 2016 12:02:51 -0400 Subject: [PATCH 13/71] Don't restrict what can be removed at the filesystem level. --- Lib/ufoLib/filesystem.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 3f82b56..643d5e6 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -60,6 +60,8 @@ def __init__(self, path, mode="r", structure=None): raise UFOLibError("The specified UFO doesn't exist.") structure = sniffFileStructure(path) if structure == "package": + if mode == "w" and not os.path.exists(path): + os.mkdir(path) path = OSFS(path) elif structure == "zip": if not haveFS: @@ -301,17 +303,10 @@ def _writeFileAtomically(self, data, path): def remove(self, path): """ Remove the file (or directory) at path. The path - must be relative to the UFO. This is only allowed - for files in the data and image directories. + must be relative to the UFO. """ - d = path - parts = [] - while d: - d, p = self.splitPath(d) - if p: - parts.append(p) - if parts[-1] not in ("images", "data"): - raise UFOLibError("Removing \"%s\" is not legal." % path) + if not self.exists(path): + return self._removeFileForPath(path, raiseErrorIfMissing=True) def _removeFileForPath(self, path, raiseErrorIfMissing=False): From c9ced7f718293ca57708a944daf2cb464646188c Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Tue, 7 Jun 2016 13:24:30 -0400 Subject: [PATCH 14/71] Use fs.path instead of os.path. --- Lib/ufoLib/filesystem.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 643d5e6..bb5745a 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -153,16 +153,16 @@ def _fsGetFileModificationTime(self, path): # ----------------- def joinPath(self, *parts): - return os.path.join(*parts) + return self._fs.path.join(*parts) def splitPath(self, path): - return os.path.split(path) + return self._fs.path.split(path) def directoryName(self, path): - return self.splitPath(path)[0] + return self._fs.path.dirname(path) def relativePath(self, path, start): - return os.path.relpath(path, start) + return self._fs.relativefrom(path, start) # --------- # Existence @@ -404,6 +404,18 @@ def __init__(self, path): def _absPath(self, path): return os.path.join(self._path, path) + def joinPath(self, *parts): + return os.path.join(*parts) + + def splitPath(self, path): + return os.path.split(path) + + def directoryName(self, path): + return os.path.split(path)[0] + + def relativePath(self, path, start): + return os.path.relpath(path, start) + def close(self): pass From 5014279665ff8e01b21056cc3909876dbb392c94 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 09:04:29 -0400 Subject: [PATCH 15/71] Don't subclass FileSystem. --- Lib/ufoLib/__init__.py | 94 +++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index f580c11..a7f72c4 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -121,12 +121,12 @@ def _getPlist(self, fileName, default=None): # UFO Reader # ---------- -class UFOReader(FileSystem): +class UFOReader(object): """Read the various components of the .ufo.""" def __init__(self, path): - super(UFOReader, self).__init__(path) + self.fileSystem = FileSystem(path) self.readMetaInfo() self._upConvertedKerningData = None @@ -189,7 +189,7 @@ def readMetaInfo(self): """ Read metainfo.plist. Only used for internal operations. """ - data = self.readPlist(METAINFO_FILENAME) + data = self.fileSystem.readPlist(METAINFO_FILENAME) if not isinstance(data, dict): raise UFOLibError("metainfo.plist is not properly formatted.") formatVersion = data["formatVersion"] @@ -200,7 +200,7 @@ def readMetaInfo(self): # groups.plist def _readGroups(self): - return self.readPlist(GROUPS_FILENAME, {}) + return self.fileSystem.readPlist(GROUPS_FILENAME, {}) def readGroups(self): """ @@ -242,7 +242,7 @@ def getKerningGroupConversionRenameMaps(self): # fontinfo.plist def _readInfo(self): - data = self.readPlist(FONTINFO_FILENAME, {}) + data = self.fileSystem.readPlist(FONTINFO_FILENAME, {}) if not isinstance(data, dict): raise UFOLibError("fontinfo.plist is not properly formatted.") return data @@ -294,7 +294,7 @@ def readInfo(self, info): # kerning.plist def _readKerning(self): - data = self.readPlist(KERNING_FILENAME, {}) + data = self.fileSystem.readPlist(KERNING_FILENAME, {}) return data def readKerning(self): @@ -325,7 +325,7 @@ def readLib(self): """ Read lib.plist. Returns a dict. """ - data = self.readPlist(LIB_FILENAME, {}) + data = self.fileSystem.readPlist(LIB_FILENAME, {}) valid, message = fontLibValidator(data) if not valid: raise UFOLibError(message) @@ -337,9 +337,9 @@ def readFeatures(self): """ Read features.fea. Returns a string. """ - if not self.exists(FEATURES_FILENAME): + if not self.fileSystem.exists(FEATURES_FILENAME): return "" - with self.open(FEATURES_FILENAME, "r") as f: + with self.fileSystem.open(FEATURES_FILENAME, "r") as f: text = f.read() return text @@ -352,7 +352,7 @@ def _readLayerContents(self): """ if self._formatVersion < 3: return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] - contents = self.readPlist(LAYERCONTENTS_FILENAME) + contents = self.fileSystem.readPlist(LAYERCONTENTS_FILENAME) valid, error = layerContentsValidator(contents, self._path) if not valid: raise UFOLibError(error) @@ -395,7 +395,7 @@ def getGlyphSet(self, layerName=None): break if directory is None: raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName) - return GlyphSet(directory, fileSystem=self, ufoFormatVersion=self._formatVersion) + return GlyphSet(directory, fileSystem=self.fileSystem, ufoFormatVersion=self._formatVersion) def getCharacterMapping(self, layerName=None): """ @@ -422,9 +422,9 @@ def getDataDirectoryListing(self): This will not list directory names, only file names. Thus, empty directories will be skipped. """ - if not self.exists(DATA_DIRNAME): + if not self.fileSystem.exists(DATA_DIRNAME): return [] - listing = self.listDirectory(path, recurse=True) + listing = self.fileSystem.listDirectory(path, recurse=True) return listing def getImageDirectoryListing(self): @@ -435,16 +435,17 @@ def getImageDirectoryListing(self): """ if self._formatVersion < 3: return [] - if not self.exists(IMAGES_DIRNAME): + if not self.fileSystem.exists(IMAGES_DIRNAME): return [] - if not self.isDirectory(IMAGES_DIRNAME): + if not self.fileSystem.isDirectory(IMAGES_DIRNAME): raise UFOLibError("The UFO contains an \"images\" file instead of a directory.") result = [] - for fileName in self.listDirectory(path): - if self.isDirectory(fileName): + for fileName in self.fileSystem.listDirectory(path): + if self.fileSystem.isDirectory(fileName): # silently skip this as version control # systems often have hidden directories continue + # XXX this is sending a path to the validator. that won't work in the abstracted filesystem. valid, error = pngValidator(path=p) if valid: result.append(fileName) @@ -456,8 +457,8 @@ def readImage(self, fileName): """ if self._formatVersion < 3: raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion) - path = self.joinPath(IMAGES_DIRNAME, fileName) - data = self.readBytesFromPath(path) + path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) + data = self.fileSystem.readBytesFromPath(path) if data is None: raise UFOLibError("No image file named %s." % fileName) valid, error = pngValidator(data=data) @@ -470,7 +471,7 @@ def readImage(self, fileName): # ---------- -class UFOWriter(FileSystem): +class UFOWriter(object): """Write the various components of the .ufo.""" @@ -498,10 +499,10 @@ def __init__(self, path, formatVersion=3, structure=None, fileCreator="org.robof havePreviousFile = False if isinstance(path, basestring) and os.path.exists(path): havePreviousFile = True - super(UFOWriter, self).__init__(path, mode="w", structure=structure) + self.fileSystem = FileSystem(path, mode="w", structure=structure) previousFormatVersion = None if havePreviousFile: - metaInfo = self.readPlist(METAINFO_FILENAME) + metaInfo = self.fileSystem.readPlist(METAINFO_FILENAME) previousFormatVersion = metaInfo.get("formatVersion") try: previousFormatVersion = int(previousFormatVersion) @@ -526,8 +527,7 @@ def __init__(self, path, formatVersion=3, structure=None, fileCreator="org.robof else: # previous < 3 # imply the layer contents - p = os.path.join(path, DEFAULT_GLYPHS_DIRNAME) - if os.path.exists(p): + if self.fileSystem.exists(DEFAULT_GLYPHS_DIRNAME): self.layerContents = {DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME} # write the new metainfo self._writeMetaInfo() @@ -561,10 +561,10 @@ def copyFromReader(self, reader, sourcePath, destPath): raise UFOLibError("The reader does not have data located at \"%s\"." % sourcePath) if reader.isDirectory(sourcePath): raise UFOLibError("Directories can not be copied from a reader to a writer.") - if self.exists(destPath): + if self.fileSystem.exists(destPath): raise UFOLibError("A file named \"%s\" already exists." % destPath) - data = reader.readBytesFromPath(sourcePath) - self.writeBytesToPath(data, destPath) + data = reader.fileSystem.readBytesFromPath(sourcePath) + self.fileSystem.writeBytesToPath(data, destPath) # UFO mod time @@ -585,7 +585,7 @@ def _writeMetaInfo(self): creator=self._fileCreator, formatVersion=self._formatVersion ) - self.writePlist(METAINFO_FILENAME, metaInfo) + self.fileSystem.writePlist(METAINFO_FILENAME, metaInfo) # groups.plist @@ -654,9 +654,9 @@ def writeGroups(self, groups): for key, value in list(groups.items()): groupsNew[key] = list(value) if groupsNew: - self.writePlist(GROUPS_FILENAME, groupsNew) + self.fileSystem.writePlist(GROUPS_FILENAME, groupsNew) else: - self.remove(GROUPS_FILENAME) + self.fileSystem.remove(GROUPS_FILENAME) # fontinfo.plist @@ -690,7 +690,7 @@ def writeInfo(self, info): infoData = validateInfoVersion2Data(infoData) infoData = _convertFontInfoDataVersion2ToVersion1(infoData) # write file - self.writePlist(FONTINFO_FILENAME, infoData) + self.fileSystem.writePlist(FONTINFO_FILENAME, infoData) # kerning.plist @@ -736,9 +736,9 @@ def writeKerning(self, kerning): kerningDict[left] = {} kerningDict[left][right] = value if kerningDict: - self.writePlist(KERNING_FILENAME, kerningDict) + self.fileSystem.writePlist(KERNING_FILENAME, kerningDict) else: - self.remove(KERNING_FILENAME) + self.fileSystem.remove(KERNING_FILENAME) # lib.plist @@ -751,9 +751,9 @@ def writeLib(self, libDict): if not valid: raise UFOLibError(message) if libDict: - self.writePlist(LIB_FILENAME, libDict) + self.fileSystem.writePlist(LIB_FILENAME, libDict) else: - self.remove(LIB_FILENAME) + self.fileSystem.remove(LIB_FILENAME) # features.fea @@ -766,7 +766,7 @@ def writeFeatures(self, features): raise UFOLibError("features.fea is not allowed in UFO Format Version 1.") if not isinstance(features, basestring): raise UFOLibError("The features are not text.") - self.writeBytesToPath(FEATURES_FILENAME, features.encode("utf8")) + self.fileSystem.writeBytesToPath(FEATURES_FILENAME, features.encode("utf8")) # glyph sets & layers @@ -776,7 +776,7 @@ def _readLayerContents(self): are available on disk. """ # read the file on disk - raw = self.readPlist(LAYERCONTENTS_FILENAME) + raw = self.fileSystem.readPlist(LAYERCONTENTS_FILENAME) contents = {} valid, error = layerContentsValidator(raw, self._path) if not valid: @@ -805,7 +805,7 @@ def writeLayerContents(self, layerOrder=None): if set(layerOrder) != set(self.layerContents.keys()): raise UFOLibError("The layer order contents does not match the glyph sets that have been created.") layerContents = [(layerName, self.layerContents[layerName]) for layerName in layerOrder] - self.writePlist(LAYERCONTENTS_FILENAME, layerContents) + self.fileSystem.writePlist(LAYERCONTENTS_FILENAME, layerContents) def _findDirectoryForLayerName(self, layerName): foundDirectory = None @@ -850,10 +850,10 @@ def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc return self._getGlyphSetFormatVersion3(layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc) def _getGlyphSetFormatVersion1(self, glyphNameToFileNameFunc=None): - return GlyphSet(DEFAULT_GLYPHS_DIRNAME, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=1) + return GlyphSet(DEFAULT_GLYPHS_DIRNAME, fileSystem=self.fileSystem, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=1) def _getGlyphSetFormatVersion2(self, glyphNameToFileNameFunc=None): - return GlyphSet(DEFAULT_GLYPHS_DIRNAME, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=2) + return GlyphSet(DEFAULT_GLYPHS_DIRNAME, fileSystem=self.fileSystem, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=2) def _getGlyphSetFormatVersion3(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None): # if the default flag is on, make sure that the default in the file @@ -884,11 +884,11 @@ def _getGlyphSetFormatVersion3(self, layerName=None, defaultLayer=True, glyphNam raise UFOLibError("The specified layer name is not a Unicode string.") directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.") # make the directory - self.makeDirectory(directory) + self.fileSystem.makeDirectory(directory) # store the mapping self.layerContents[layerName] = directory # load the glyph set - return GlyphSet(directory, fileSystem=self, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3) + return GlyphSet(directory, fileSystem=self.fileSystem, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3) def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False): """ @@ -929,7 +929,7 @@ def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False): del self.layerContents[layerName] self.layerContents[newLayerName] = newDirectory # do the file system copy - self.move(oldDirectory, newDirectory) + self.fileSystem.move(oldDirectory, newDirectory) def deleteGlyphSet(self, layerName): """ @@ -953,8 +953,8 @@ def writeImage(self, fileName, data): valid, error = pngValidator(data=data) if not valid: raise UFOLibError(error) - path = self.joinPath(IMAGES_DIRNAME, fileName) - self.writeBytesToPath(path, data) + path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) + self.fileSystem.writeBytesToPath(path, data) def removeImage(self, fileName): """ @@ -963,8 +963,8 @@ def removeImage(self, fileName): """ if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - path = self.joinPath(IMAGES_DIRNAME, fileName) - self.remove(path) + path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) + self.fileSystem.remove(path) def copyImageFromReader(self, reader, sourceFileName, destFileName): """ From 3fa83a1924250f18f9ca83d19cb3f678c932897d Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 09:05:43 -0400 Subject: [PATCH 16/71] Remove a deprecated function. --- Lib/ufoLib/__init__.py | 60 ------------------------------------------ 1 file changed, 60 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index a7f72c4..b3b47a2 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -1033,66 +1033,6 @@ def writeDataFileAtomically(data, path): f.write(data) f.close() -# --------------------------- -# Format Conversion Functions -# --------------------------- - -def convertUFOFormatVersion1ToFormatVersion2(inPath, outPath=None): - """ - Function for converting a version format 1 UFO - to version format 2. inPath should be a path - to a UFO. outPath is the path where the new UFO - should be written. If outPath is not given, the - inPath will be used and, therefore, the UFO will - be converted in place. Otherwise, if outPath is - specified, nothing must exist at that path. - """ - from warnings import warn - warn("convertUFOFormatVersion1ToFormatVersion2 is deprecated.", DeprecationWarning) - if outPath is None: - outPath = inPath - if inPath != outPath and os.path.exists(outPath): - raise UFOLibError("A file already exists at %s." % outPath) - # use a reader for loading most of the data - reader = UFOReader(inPath) - if reader.formatVersion == 2: - raise UFOLibError("The UFO at %s is already format version 2." % inPath) - groups = reader.readGroups() - kerning = reader.readKerning() - libData = reader.readLib() - # read the info data manually and convert - infoPath = os.path.join(inPath, FONTINFO_FILENAME) - if not os.path.exists(infoPath): - infoData = {} - else: - with open(infoPath, "rb") as f: - infoData = readPlist(f) - infoData = _convertFontInfoDataVersion1ToVersion2(infoData) - # if the paths are the same, only need to change the - # fontinfo and meta info files. - infoPath = os.path.join(outPath, FONTINFO_FILENAME) - if inPath == outPath: - metaInfoPath = os.path.join(inPath, METAINFO_FILENAME) - metaInfo = dict( - creator="org.robofab.ufoLib", - formatVersion=2 - ) - writePlistAtomically(metaInfo, metaInfoPath) - writePlistAtomically(infoData, infoPath) - # otherwise write everything. - else: - writer = UFOWriter(outPath, formatVersion=2) - writer.writeGroups(groups) - writer.writeKerning(kerning) - writer.writeLib(libData) - # write the info manually - writePlistAtomically(infoData, infoPath) - # copy the glyph tree - inGlyphs = os.path.join(inPath, DEFAULT_GLYPHS_DIRNAME) - outGlyphs = os.path.join(outPath, DEFAULT_GLYPHS_DIRNAME) - if os.path.exists(inGlyphs): - shutil.copytree(inGlyphs, outGlyphs) - # ---------------------- # fontinfo.plist Support # ---------------------- From 1ed9fb14f57d55e032b799831ffe9073571f5c22 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 09:36:57 -0400 Subject: [PATCH 17/71] Remove some unneeded functions. --- Lib/ufoLib/__init__.py | 70 +----------------------------------------- 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index b3b47a2..028e3a3 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -59,8 +59,6 @@ "validateFontInfoVersion3ValueForAttribute", "convertFontInfoValueForAttributeFromVersion1ToVersion2", "convertFontInfoValueForAttributeFromVersion2ToVersion1" - # deprecated - "convertUFOFormatVersion1ToFormatVersion2" ] @@ -88,35 +86,6 @@ supportedUFOFormatVersions = [1, 2, 3] -# -------------- -# Shared Methods -# -------------- - -def _getPlist(self, fileName, default=None): - """ - Read a property list relative to the - path argument of UFOReader. If the file - is missing and default is None a - UFOLibError will be raised otherwise - default is returned. The errors that - could be raised during the reading of - a plist are unpredictable and/or too - large to list, so, a blind try: except: - is done. If an exception occurs, a - UFOLibError will be raised. - """ - path = os.path.join(self._path, fileName) - if not os.path.exists(path): - if default is not None: - return default - else: - raise UFOLibError("%s is missing in %s. This file is required" % (fileName, self._path)) - try: - with open(path, "rb") as f: - return readPlist(f) - except: - raise UFOLibError("The file %s could not be read." % fileName) - # ---------- # UFO Reader # ---------- @@ -466,11 +435,11 @@ def readImage(self, fileName): raise UFOLibError(error) return data + # ---------- # UFO Writer # ---------- - class UFOWriter(object): """Write the various components of the .ufo.""" @@ -996,43 +965,6 @@ def makeUFOPath(path): name = ".".join([".".join(name.split(".")[:-1]), "ufo"]) return os.path.join(dir, name) -def writePlistAtomically(obj, path): - """ - Write a plist for "obj" to "path". Do this sort of atomically, - making it harder to cause corrupt files, for example when writePlist - encounters an error halfway during write. This also checks to see - if text matches the text that is already in the file at path. - If so, the file is not rewritten so that the modification date - is preserved. - """ - f = BytesIO() - writePlist(obj, f) - data = f.getvalue() - writeDataFileAtomically(data, path) - -def writeDataFileAtomically(data, path): - """ - Write data into a file at path. Do this sort of atomically - making it harder to cause corrupt files. This also checks to see - if data matches the data that is already in the file at path. - If so, the file is not rewritten so that the modification date - is preserved. - """ - assert isinstance(data, bytes) - if os.path.exists(path): - f = open(path, "rb") - oldData = f.read() - f.close() - if data == oldData: - return - # if the data is empty, remove the existing file - if not data: - os.remove(path) - if data: - f = open(path, "wb") - f.write(data) - f.close() - # ---------------------- # fontinfo.plist Support # ---------------------- From 1eebdaf84a6a8bc7229409901cdf3534e344f975 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 10:30:52 -0400 Subject: [PATCH 18/71] These are no longer around. --- Lib/ufoLib/glifLib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index 91a3bf1..9048106 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -17,7 +17,6 @@ from warnings import warn from fontTools.misc.py23 import tobytes from ufoLib.filesystem import FileSystem -from ufoLib.xmlTreeBuilder import buildTree, stripCharacterData from ufoLib.plistlib import PlistWriter, readPlist, writePlist from ufoLib.plistFromETree import readPlistFromTree from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen From 82958e2711b3b7afdc517a71b92396e54a2104be Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 10:31:07 -0400 Subject: [PATCH 19/71] Name the argument. --- Lib/ufoLib/test/test_glifLib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/test/test_glifLib.py b/Lib/ufoLib/test/test_glifLib.py index d232b00..9296596 100644 --- a/Lib/ufoLib/test/test_glifLib.py +++ b/Lib/ufoLib/test/test_glifLib.py @@ -84,7 +84,7 @@ def testCustomFileNamingScheme(self): def myGlyphNameToFileName(glyphName, glyphSet): return "prefix" + glyphNameToFileName(glyphName, glyphSet) src = GlyphSet(GLYPHSETDIR) - dst = GlyphSet(self.dstDir, myGlyphNameToFileName) + dst = GlyphSet(self.dstDir, glyphNameToFileNameFunc=myGlyphNameToFileName) for glyphName in src.keys(): g = src[glyphName] g.drawPoints(None) # load attrs From 76480ec37f43c6caa64eacbed47a5c7185d1fbfa Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 10:31:21 -0400 Subject: [PATCH 20/71] fs.path not self._fs.path. --- Lib/ufoLib/filesystem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index bb5745a..bb8aa79 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -5,6 +5,7 @@ haveFS = False try: + import fs from fs.osfs import OSFS from fs.zipfs import ZipFS, ZipOpenError haveFS = True @@ -153,16 +154,16 @@ def _fsGetFileModificationTime(self, path): # ----------------- def joinPath(self, *parts): - return self._fs.path.join(*parts) + return fs.path.join(*parts) def splitPath(self, path): - return self._fs.path.split(path) + return fs.path.split(path) def directoryName(self, path): - return self._fs.path.dirname(path) + return fs.path.dirname(path) def relativePath(self, path, start): - return self._fs.relativefrom(path, start) + return fs.relativefrom(path, start) # --------- # Existence From 21f36611f097c9f2577f9054aac8a262ef9e22d5 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 10:31:39 -0400 Subject: [PATCH 21/71] Found something hardwired to the package structure. --- Lib/ufoLib/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 028e3a3..07f8751 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -322,6 +322,7 @@ def _readLayerContents(self): if self._formatVersion < 3: return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] contents = self.fileSystem.readPlist(LAYERCONTENTS_FILENAME) + # XXX this won't work with the abstracted file system valid, error = layerContentsValidator(contents, self._path) if not valid: raise UFOLibError(error) From cea3ba9b43f5a3e79c7775425286db2189aed01b Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 11:09:37 -0400 Subject: [PATCH 22/71] Fix some minor issues to resolve some failing tests. --- Lib/ufoLib/__init__.py | 66 +++++++++++++++++++++++++++++++++++++--- Lib/ufoLib/filesystem.py | 2 +- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 07f8751..0211683 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -98,6 +98,7 @@ def __init__(self, path): self.fileSystem = FileSystem(path) self.readMetaInfo() self._upConvertedKerningData = None + self._path = path # properties @@ -106,6 +107,26 @@ def _get_formatVersion(self): formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.") + def readBytesFromPath(self, path, encoding=None): + """ + Returns the bytes in the file at the given path. + The path must be relative to the UFO path. + Returns None if the file does not exist. + An encoding may be passed if needed. + """ + return self.fileSystem.readBytesFromPath(path, encoding=encoding) + + def getReadFileForPath(self, path, encoding=None): + """ + Returns a file (or file-like) object for the + file at the given path. The path must be relative + to the UFO path. Returns None if the file does + not exist An encoding may be passed if needed. + + Note: The caller is responsible for closing the open file. + """ + return self.fileSystem.open(path, mode="rb", encoding=encoding) + # up conversion def _upConvertKerning(self): @@ -394,7 +415,7 @@ def getDataDirectoryListing(self): """ if not self.fileSystem.exists(DATA_DIRNAME): return [] - listing = self.fileSystem.listDirectory(path, recurse=True) + listing = self.fileSystem.listDirectory(DATA_DIRNAME, recurse=True) return listing def getImageDirectoryListing(self): @@ -464,6 +485,7 @@ def __init__(self, path, formatVersion=3, structure=None, fileCreator="org.robof path = file + '.ufoz' # /XXX + self._path = path if formatVersion not in supportedUFOFormatVersions: raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) havePreviousFile = False @@ -527,15 +549,51 @@ def copyFromReader(self, reader, sourcePath, destPath): """ if not isinstance(reader, UFOReader): raise UFOLibError("The reader must be an instance of UFOReader.") - if not reader.exists(sourcePath): + if not reader.fileSystem.exists(sourcePath): raise UFOLibError("The reader does not have data located at \"%s\"." % sourcePath) - if reader.isDirectory(sourcePath): + if reader.fileSystem.isDirectory(sourcePath): raise UFOLibError("Directories can not be copied from a reader to a writer.") if self.fileSystem.exists(destPath): raise UFOLibError("A file named \"%s\" already exists." % destPath) data = reader.fileSystem.readBytesFromPath(sourcePath) self.fileSystem.writeBytesToPath(data, destPath) + def writeBytesToPath(self, path, data, encoding=None): + """ + Write bytes to path. If needed, the directory tree + for the given path will be built. The path must be + relative to the UFO. An encoding may be passed if needed. + """ + self.fileSystem.writeBytesToPath(path, data, encoding=encoding) + + def readBytesFromPath(self, path, encoding=None): + """ + Returns the bytes in the file at the given path. + The path must be relative to the UFO path. + Returns None if the file does not exist. + An encoding may be passed if needed. + """ + return self.fileSystem.readBytesFromPath(path, encoding=encoding) + + def getFileObjectForPath(self, path, mode="w", encoding=None): + """ + Returns a file (or file-like) object for the + file at the given path. The path must be relative + to the UFO path. Returns None if the file does + not exist and the mode is "r" or "rb. An encoding + may be passed if needed. + + Note: The caller is responsible for closing the open file. + """ + return self.fileSystem.open(path, mode=mode, encoding=encoding) + + def removeFileForPath(self, path): + """ + Remove the file (or directory) at path. The path + must be relative to the UFO. + """ + self.fileSystem.remove(path) + # UFO mod time def setModificationTime(self): @@ -908,7 +966,7 @@ def deleteGlyphSet(self, layerName): if self._formatVersion < 3: raise UFOLibError("Deleting a glyph set is not allowed in UFO %d." % self._formatVersion) foundDirectory = self._findDirectoryForLayerName(layerName) - self._removeFileForPath(foundDirectory) + self.fileSystem.remove(foundDirectory) del self.layerContents[layerName] # /images diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index bb8aa79..d4f5679 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -369,7 +369,7 @@ def readPlist(self, path, default=None): with self.open(path, "rb") as f: return readPlist(f) except: - raise UFOLibError("The file %s could not be read." % fileName) + raise UFOLibError("The file %s could not be read." % path) def writePlist(self, path, obj): """ From 6043db4f1145ab790f58cf03c6f350f0a42b12f2 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 12:06:35 -0400 Subject: [PATCH 23/71] Switch to rb mode. --- Lib/ufoLib/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index d4f5679..b8356af 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -260,7 +260,7 @@ def readBytesFromPath(self, path, encoding=None): Returns None if the file does not exist. An encoding may be passed if needed. """ - f = self.open(path, mode="r", encoding=encoding) + f = self.open(path, mode="rb", encoding=encoding) if f is None: return None data = f.read() From 4162429c539a98933c2e635a59b1fa3cc9be6e15 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 13:39:14 -0400 Subject: [PATCH 24/71] Flipped args. --- Lib/ufoLib/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 0211683..3dcf3f6 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -547,6 +547,7 @@ def copyFromReader(self, reader, sourcePath, destPath): in this writer. The paths must be relative. This only works with individual files, not directories. """ + print sourcePath, destPath if not isinstance(reader, UFOReader): raise UFOLibError("The reader must be an instance of UFOReader.") if not reader.fileSystem.exists(sourcePath): @@ -556,7 +557,7 @@ def copyFromReader(self, reader, sourcePath, destPath): if self.fileSystem.exists(destPath): raise UFOLibError("A file named \"%s\" already exists." % destPath) data = reader.fileSystem.readBytesFromPath(sourcePath) - self.fileSystem.writeBytesToPath(data, destPath) + self.fileSystem.writeBytesToPath(destPath, data) def writeBytesToPath(self, path, data, encoding=None): """ From a0a942bc7d488af9cbcef06693cfe8a5af4f31cb Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Wed, 8 Jun 2016 14:26:01 -0400 Subject: [PATCH 25/71] Make copying a directory from a source to a dest work. --- Lib/ufoLib/__init__.py | 29 ++++++++++++++++++++++++----- Lib/ufoLib/filesystem.py | 9 ++++++--- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 3dcf3f6..2bd70be 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -547,17 +547,36 @@ def copyFromReader(self, reader, sourcePath, destPath): in this writer. The paths must be relative. This only works with individual files, not directories. """ - print sourcePath, destPath if not isinstance(reader, UFOReader): raise UFOLibError("The reader must be an instance of UFOReader.") if not reader.fileSystem.exists(sourcePath): raise UFOLibError("The reader does not have data located at \"%s\"." % sourcePath) - if reader.fileSystem.isDirectory(sourcePath): - raise UFOLibError("Directories can not be copied from a reader to a writer.") if self.fileSystem.exists(destPath): raise UFOLibError("A file named \"%s\" already exists." % destPath) - data = reader.fileSystem.readBytesFromPath(sourcePath) - self.fileSystem.writeBytesToPath(destPath, data) + if reader.fileSystem.isDirectory(sourcePath): + self._copyDirectoryFromReader(reader, sourcePath, destPath) + else: + data = reader.fileSystem.readBytesFromPath(sourcePath) + self.fileSystem.writeBytesToPath(destPath, data) + + def _copyDirectoryFromReader(self, reader, sourcePath, destPath): + # create the destination directory if it doesn't exist + if not self.fileSystem.exists(destPath): + destDirectory = destPath + destTree = [] + while destDirectory: + destDirectory, b = self.fileSystem.splitPath(destDirectory) + destTree.insert(0, b) + for i, d in enumerate(destTree): + p = self.fileSystem.joinPath(*(destTree[:i] + [d])) + if not self.fileSystem.exists(p): + self.fileSystem.makeDirectory(p) + # copy everything in the source directory + for fileName in reader.fileSystem.listDirectory(sourcePath): + fullSourcePath = self.fileSystem.joinPath(sourcePath, fileName) + fullDestPath = self.fileSystem.joinPath(destPath, fileName) + self.copyFromReader(reader, fullSourcePath, fullDestPath) + def writeBytesToPath(self, path, data, encoding=None): """ diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index b8356af..78473c7 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -176,17 +176,20 @@ def isDirectory(self, path): return self._fsIsDirectory(path) def listDirectory(self, path, recurse=False): - return self._listDirectory(path, recurse=recurse) + return self._listDirectory(path, recurse=recurse, relativeTo=path) - def _listDirectory(self, path, recurse=False, depth=0, maxDepth=100): + def _listDirectory(self, path, recurse=False, relativeTo=None, depth=0, maxDepth=100): + if not relativeTo.endswith("/"): + relativeTo += "/" if depth > maxDepth: raise UFOLibError("Maximum recusion depth reached.") result = [] for fileName in self._fsListDirectory(path): p = self.joinPath(path, fileName) if self.isDirectory(p) and recurse: - result += self._listDirectory(p, recurse=True, depth=depth+1, maxDepth=maxDepth) + result += self._listDirectory(p, recurse=True, relativeTo=relativeTo, depth=depth+1, maxDepth=maxDepth) else: + p = p[len(relativeTo):] result.append(p) return result From 15c4ff20a340a512c94a1b1d6ee397c1b3ba86f1 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 13 Jun 2016 13:10:55 -0400 Subject: [PATCH 26/71] UFOLibError is now raised instead of GlifLibError. --- Lib/ufoLib/test/test_UFO3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index 7a92c48..04dac1e 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -4379,7 +4379,7 @@ def testBogusLayerInfo(self): reader = UFOReader(self.ufoPath) glyphSet = reader.getGlyphSet() info = TestLayerInfoObject() - self.assertRaises(GlifLibError, glyphSet.readLayerInfo, info) + self.assertRaises(UFOLibError, glyphSet.readLayerInfo, info) def testInvalidFormatLayerInfo(self): self.makeUFO() From bfba532621370262e3356cd491c2ac7b924476ab Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 13 Jun 2016 13:13:56 -0400 Subject: [PATCH 27/71] Force the removal of non-empty directories during directory removal. --- Lib/ufoLib/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 78473c7..4506c51 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -121,7 +121,7 @@ def _fsMakeDirectory(self, path): def _fsRemoveDirectory(self, path): path = self._fsRootPath(path) - self._fs.removedir(path) + self._fs.removedir(path, force=True) def _fsMove(self, path1, path2): if self.isDirectory(path1): From 0e569793a7632a8aa268c63fa4dd9c60ff6f9e65 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 13 Jun 2016 13:17:36 -0400 Subject: [PATCH 28/71] The populated info objects should be written, not an empty object. --- Lib/ufoLib/test/test_UFO3.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index 04dac1e..05274a0 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -2662,7 +2662,7 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataUniqueID = None writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## not a dict infoObject = self.makeInfoObject() @@ -2692,7 +2692,7 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataUniqueID = dict(id="") writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() # woffMetadataVendor ## no name @@ -2711,13 +2711,13 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataVendor = dict(name="", url="foo") writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## no URL infoObject = self.makeInfoObject() infoObject.woffMetadataVendor = dict(name="foo") writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## url not a string infoObject = self.makeInfoObject() @@ -2729,18 +2729,18 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataVendor = dict(name="foo", url="") writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## have dir infoObject = self.makeInfoObject() infoObject.woffMetadataVendor = dict(name="foo", url="bar", dir="ltr") writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() infoObject = self.makeInfoObject() infoObject.woffMetadataVendor = dict(name="foo", url="bar", dir="rtl") writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## dir not a string infoObject = self.makeInfoObject() @@ -2758,7 +2758,7 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataVendor = {"name" : "foo", "url" : "bar", "class" : "hello"} writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## class not a string infoObject = self.makeInfoObject() @@ -2770,7 +2770,7 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataVendor = {"name" : "foo", "url" : "bar", "class" : ""} writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() # woffMetadataCredits ## no credits attribute @@ -2855,7 +2855,7 @@ def testWOFFWrite(self): ## no url infoObject = self.makeInfoObject() infoObject.woffMetadataDescription = dict(text=[dict(text="foo")]) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## url not a string infoObject = self.makeInfoObject() @@ -2928,7 +2928,7 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataLicense = dict(text=[dict(text="foo")]) writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## url not a string infoObject = self.makeInfoObject() @@ -2946,7 +2946,7 @@ def testWOFFWrite(self): infoObject = self.makeInfoObject() infoObject.woffMetadataLicense = dict(url="foo") writer = UFOWriter(self.dstDir, formatVersion=3) - writer.writeInfo(TestInfoObject()) + writer.writeInfo(infoObject) self.tearDownUFO() ## text not a list infoObject = self.makeInfoObject() From c864359fb26fa91984f5fa8db0e6c5041f7398b1 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 13 Jun 2016 13:19:40 -0400 Subject: [PATCH 29/71] Define writer. --- Lib/ufoLib/test/test_UFO3.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index 05274a0..f714e1d 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -2855,6 +2855,7 @@ def testWOFFWrite(self): ## no url infoObject = self.makeInfoObject() infoObject.woffMetadataDescription = dict(text=[dict(text="foo")]) + writer = UFOWriter(self.dstDir, formatVersion=3) writer.writeInfo(infoObject) self.tearDownUFO() ## url not a string From be90029b220933ff32c2fa7759a86988f3f4f4d3 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 13 Jun 2016 13:22:36 -0400 Subject: [PATCH 30/71] Arguments were transposed. --- Lib/ufoLib/glifLib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index 9048106..1b9789b 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -214,7 +214,7 @@ def writeLayerInfo(self, info): infoData = validateLayerInfoVersion3Data(infoData) # write file path = self.fileSystem.joinPath(self.dirName, LAYERINFO_FILENAME) - self.fileSystem.writePlist(infoData, path) + self.fileSystem.writePlist(path, infoData) # # read caching From ea54c776704c86479d2e7c558d6c7720f97ed4b2 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 13 Jun 2016 13:33:31 -0400 Subject: [PATCH 31/71] Don't try to remove the root directory. --- Lib/ufoLib/filesystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 4506c51..ab92dfe 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -323,7 +323,8 @@ def _removeFileForPath(self, path, raiseErrorIfMissing=False): else: self._fsRemove(path) directory = self.directoryName(path) - self._removeEmptyDirectoriesForPath(directory) + if directory: + self._removeEmptyDirectoriesForPath(directory) def _removeEmptyDirectoriesForPath(self, directory): if not self.exists(directory): From 89d8cfa7ebbae2f3a0bc87fb5dee117f1cc50a8b Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Mon, 13 Jun 2016 13:36:30 -0400 Subject: [PATCH 32/71] Removing metainfo.plist is no longer expressly prohibited (it shouldn't be done though). Raise an error when removing a missing file. --- Lib/ufoLib/__init__.py | 2 ++ Lib/ufoLib/test/test_UFO3.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 2bd70be..18e18c5 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -612,6 +612,8 @@ def removeFileForPath(self, path): Remove the file (or directory) at path. The path must be relative to the UFO. """ + if not self.fileSystem.exists(path): + raise UFOLibError("%s does not exist." % path) self.fileSystem.remove(path) # UFO mod time diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index f714e1d..8e5dedc 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -4264,7 +4264,6 @@ def testUFOWriterRemoveFile(self): self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), False) self.assertEqual(os.path.exists(os.path.dirname(os.path.join(self.dstDir, path2))), False) self.assertEqual(os.path.exists(os.path.join(self.dstDir, "data/org.unifiedfontobject.removefile")), False) - self.assertRaises(UFOLibError, writer.removeFileForPath, path="metainfo.plist") self.assertRaises(UFOLibError, writer.removeFileForPath, path="data/org.unifiedfontobject.doesNotExist.txt") self.tearDownUFO() From 50f381dec6184a3e040f23d58f9c7421b879cca5 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 10:14:16 +0100 Subject: [PATCH 33/71] test_UFOConversion: remove test1To2 as support for UFO1 is deprecated --- Lib/ufoLib/test/test_UFOConversion.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Lib/ufoLib/test/test_UFOConversion.py b/Lib/ufoLib/test/test_UFOConversion.py index cd7a36a..f0efa4c 100644 --- a/Lib/ufoLib/test/test_UFOConversion.py +++ b/Lib/ufoLib/test/test_UFOConversion.py @@ -4,7 +4,7 @@ import unittest import tempfile from io import open -from ufoLib import convertUFOFormatVersion1ToFormatVersion2, UFOReader, UFOWriter +from ufoLib import UFOReader, UFOWriter from ufoLib.plistlib import readPlist, writePlist from ufoLib.test.testSupport import expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion @@ -116,13 +116,6 @@ def compareFileStructures(self, path1, path2, expectedInfoData, testFeatures): data2 = readPlist(f) self.assertEqual(data1, data2) - def test1To2(self): - path1 = self.getFontPath("TestFont1 (UFO1).ufo") - path2 = self.getFontPath("TestFont1 (UFO1) converted.ufo") - path3 = self.getFontPath("TestFont1 (UFO2).ufo") - convertUFOFormatVersion1ToFormatVersion2(path1, path2) - self.compareFileStructures(path2, path3, expectedFontInfo1To2Conversion, False) - # --------------------- # kerning up conversion From af8b0e322457dafededbae08d022fef757299283 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 10:14:58 +0100 Subject: [PATCH 34/71] [filesystem] use removetree as removedir was removed from pyfilesystem2 https://github.com/PyFilesystem/pyfilesystem2/issues/32 --- Lib/ufoLib/filesystem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index ab92dfe..5164c65 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -119,9 +119,9 @@ def _fsMakeDirectory(self, path): path = self._fsRootPath(path) self._fs.makedir(path) - def _fsRemoveDirectory(self, path): + def _fsRemoveTree(self, path): path = self._fsRootPath(path) - self._fs.removedir(path, force=True) + self._fs.removetree(path) def _fsMove(self, path1, path2): if self.isDirectory(path1): @@ -319,7 +319,7 @@ def _removeFileForPath(self, path, raiseErrorIfMissing=False): raise UFOLibError("The file %s does not exist." % path) else: if self.isDirectory(path): - self._fsRemoveDirectory(path) + self._fsRemoveTree(path) else: self._fsRemove(path) directory = self.directoryName(path) @@ -330,7 +330,7 @@ def _removeEmptyDirectoriesForPath(self, directory): if not self.exists(directory): return if not len(self._fsListDirectory(directory)): - self._fsRemoveDirectory(directory) + self._fsRemoveTree(directory) else: return directory = self.directoryName(directory) @@ -436,7 +436,7 @@ def makedir(self, path): path = self._absPath(path) os.mkdir(path) - def removedir(self, path): + def removetree(self, path): path = self._absPath(path) shutil.rmtree(path) From 3c49e59864e35d79056c484507db9c438031f0b4 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 12:16:01 +0100 Subject: [PATCH 35/71] [ci] test with and without fs; test on 3.6 instead of 3.4 --- .travis.yml | 28 +++++++++++++++++------- appveyor.yml | 60 +++++++++++++++++++++++++++++++++++++++++----------- tox.ini | 5 +++-- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4c9ed88..d2f926a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,24 @@ language: python sudo: false -python: - - "2.7" - - "3.4" - - "3.5" +matrix: + include: + - python: 2.7 + env: TOXENV=py27-fs + - python: 2.7 + env: TOXENV=py27-nofs + - python: 3.5 + env: TOXENV=py35-fs + - python: 3.5 + env: TOXENV=py35-nofs + - python: 3.6 + env: TOXENV=py36-fs + - python: 3.6 + env: + - TOXENV=py36-nofs + - BUILD_DIST=true + install: - - pip install --upgrade pip setuptools wheel - - pip install tox-travis + - pip install --upgrade pip setuptools wheel tox script: tox before_deploy: - pip wheel --no-deps -w dist . @@ -26,7 +38,7 @@ deploy: repo: unified-font-object/ufoLib tags: true all_branches: true - python: 3.5 + condition: "$BUILD_DIST == true" # deploy to PyPI on tags - provider: pypi server: https://upload.pypi.org/legacy/ @@ -34,7 +46,7 @@ deploy: repo: unified-font-object/ufoLib tags: true all_branches: true - python: 3.5 + condition: "$BUILD_DIST == true" user: anthrotype password: secure: m0tl6kKKOE/V1WsTkdn1yCYdI6dEnZZbz0SChVm9XNuloTti++I25oJvfcDFDtq5K+gHZ8hPmym/sFH+dozjf39fCrURiFnL9tIumCJvehubDXYGw2ZLN8SLayG6QsdBqyjQB8mS9L+Ag0vaC10Nj6zgmXPQQERckqDiLfmT1A1IPDodu18hHH7FM4eNhhB1Ksaem9rurgpNHtORHJxab1aGVIe0kz6UR/e+ldtenyxWcHVX+04kTJkR7mz9c2lTq0DrVZ7uc9slURi5Mw4VgGHG6J/sCsqUFoqtESciN0+2OblacVbvu7avBsnFbSVOFdqurRNpHf1gCPt5cx0nIORpps9VE2TZEj0J4wmTamhUGelIuyqu6jGYExc94Ca6OYLQxqr+YhiPSl0uiWzVA0D7dmKx9EIl/RKtB3nAg9+BX7il2Xt/fPU0qJtMbYwSTtS/KDuNFys6z4wFcvAykdBbilX2ZCjbSP36uVI6HmwWJX+/kAFiGiuW0qQQ3Q64LgBu2t8Ho0HE+J5TmpE59eaOqir1yMd9wo89t0QJNO2zf1/0FO0lakol4+VX+btyt8QfCnBGGk7ZjK46h9oOMhZ5kjdVRqDwrsgy6InLbirQvDbhGI2NZInY5/Y8w8lCdExyfsELSOBwm86ovzIKAG4bPU3cEIjAHuJGBXBjqKk= diff --git a/appveyor.yml b/appveyor.yml index 7866cc5..a407889 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,39 +4,75 @@ environment: - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - TOXENV: "py27" + TOXENV: "py27-fs" TOXPYTHON: "C:\\Python27\\python.exe" - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.x" + - PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7.x" + PYTHON_ARCH: "32" + TOXENV: "py27-nofs" + TOXPYTHON: "C:\\Python27\\python.exe" + + - PYTHON: "C:\\Python35" + PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "32" - TOXENV: "py34" - TOXPYTHON: "C:\\Python34\\python.exe" + TOXENV: "py35-fs" + TOXPYTHON: "C:\\Python35\\python.exe" - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "32" - TOXENV: "py35" + TOXENV: "py35-nofs" TOXPYTHON: "C:\\Python35\\python.exe" + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.x" + PYTHON_ARCH: "32" + TOXENV: "py36-fs" + TOXPYTHON: "C:\\Python36\\python.exe" + + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.x" + PYTHON_ARCH: "32" + TOXENV: "py36-nofs" + TOXPYTHON: "C:\\Python36\\python.exe" + - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - TOXENV: "py27" + TOXENV: "py27-fs" TOXPYTHON: "C:\\Python27-x64\\python.exe" - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.x" + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" + PYTHON_ARCH: "64" + TOXENV: "py27-nofs" + TOXPYTHON: "C:\\Python27-x64\\python.exe" + + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" - TOXENV: "py34" - TOXPYTHON: "C:\\Python34-x64\\python.exe" + TOXENV: "py35-fs" + TOXPYTHON: "C:\\Python35-x64\\python.exe" - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" - TOXENV: "py35" + TOXENV: "py35-nofs" TOXPYTHON: "C:\\Python35-x64\\python.exe" + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + PYTHON_ARCH: "64" + TOXENV: "py36-fs" + TOXPYTHON: "C:\\Python36-x64\\python.exe" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + PYTHON_ARCH: "64" + TOXENV: "py36-nofs" + TOXPYTHON: "C:\\Python36-x64\\python.exe" + init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" diff --git a/tox.ini b/tox.ini index 3edc2fe..11e9bbc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,16 @@ [tox] -envlist = py27, py35 +envlist = py{27,36}-{fs,nofs} [testenv] basepython = # we use TOXPYTHON env variable to specify the location of Appveyor Python py27: {env:TOXPYTHON:python2.7} - py34: {env:TOXPYTHON:python3.4} py35: {env:TOXPYTHON:python3.5} + py36: {env:TOXPYTHON:python3.6} deps = pytest -rrequirements.txt + fs: fs==2.0.4 commands = # run the test suite against the package installed inside tox env. # any extra positional arguments after `tox -- ...` are passed on to pytest From 758307d8bb8c90c1a2f0cad97d3bd26706bf449a Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 12:59:56 +0100 Subject: [PATCH 36/71] [filesystem] use os.path if not haveFS --- Lib/ufoLib/filesystem.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 5164c65..128b15b 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -154,16 +154,28 @@ def _fsGetFileModificationTime(self, path): # ----------------- def joinPath(self, *parts): - return fs.path.join(*parts) + if haveFS: + return fs.path.join(*parts) + else: + return os.path.join(*parts) def splitPath(self, path): - return fs.path.split(path) + if haveFS: + return fs.path.split(path) + else: + return os.path.split(path) def directoryName(self, path): - return fs.path.dirname(path) + if haveFS: + return fs.path.dirname(path) + else: + return os.path.dirname(path) def relativePath(self, path, start): - return fs.relativefrom(path, start) + if haveFS: + return fs.relativefrom(path, start) + else: + return os.path.relpath(path, start) # --------- # Existence @@ -409,18 +421,6 @@ def __init__(self, path): def _absPath(self, path): return os.path.join(self._path, path) - def joinPath(self, *parts): - return os.path.join(*parts) - - def splitPath(self, path): - return os.path.split(path) - - def directoryName(self, path): - return os.path.split(path)[0] - - def relativePath(self, path, start): - return os.path.relpath(path, start) - def close(self): pass From 07ba88f55a505469e692ecca7dacb8de1b16d790 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 13:13:15 +0100 Subject: [PATCH 37/71] [filesystem] fs.zipfs module no longer exports ZipOperError it's unused anyway --- Lib/ufoLib/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 128b15b..6a4d689 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -7,7 +7,7 @@ try: import fs from fs.osfs import OSFS - from fs.zipfs import ZipFS, ZipOpenError + from fs.zipfs import ZipFS haveFS = True except ImportError: pass From 62cc1c5805f4961454dc48fe85b3366ab8483ac3 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 13:23:04 +0100 Subject: [PATCH 38/71] [filesystem] allow to init FileSystem class from another instance --- Lib/ufoLib/filesystem.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 6a4d689..fec6367 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -32,7 +32,7 @@ class FileSystem(object): def __init__(self, path, mode="r", structure=None): """ - path can be a path or a fs file system object. + path can be a path or another FileSystem object. mode can be r or w. @@ -41,11 +41,10 @@ def __init__(self, path, mode="r", structure=None): package: package structure zip: zipped package - mode and structure are both ignored if a - fs file system object is given for path. + mode and structure are both ignored if a FileSystem + object is given for path. """ self._root = None - self._path = "" if isinstance(path, basestring): self._path = path if mode == "w": @@ -58,7 +57,7 @@ def __init__(self, path, mode="r", structure=None): structure = existingStructure elif mode == "r": if not os.path.exists(path): - raise UFOLibError("The specified UFO doesn't exist.") + raise UFOLibError("The specified UFO doesn't exist: %r" % path) structure = sniffFileStructure(path) if structure == "package": if mode == "w" and not os.path.exists(path): @@ -75,6 +74,12 @@ def __init__(self, path, mode="r", structure=None): raise UFOLibError("The UFO contains more than one root.") else: self._root = roots[0] + elif isinstance(path, self.__class__): + self._root = path._root + self._path = path._path + path = path._fs + else: + raise TypeError(path) self._fs = path def close(self): From 590d00ba118953e84b9f3a1e127934eaccb52ee4 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 13:29:33 +0100 Subject: [PATCH 39/71] [filesystem] use unicode strings for path names as pyfilesystem2 works internally with unicode strings, some methods (e.g. self._fs.exists()) fail with TypeError if passed bytes strings (on python 2 bytes == str). We decode bytes path strings using the system's default filesystem encoding. --- Lib/ufoLib/filesystem.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index fec6367..2f9770a 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -1,7 +1,9 @@ import os +import sys import shutil from io import StringIO, BytesIO, open import zipfile +from fontTools.misc.py23 import tounicode haveFS = False try: @@ -20,6 +22,9 @@ except NameError: basestring = str +_SYS_FS_ENCODING = sys.getfilesystemencoding() + + def sniffFileStructure(path): if zipfile.is_zipfile(path): return "zip" @@ -46,6 +51,7 @@ def __init__(self, path, mode="r", structure=None): """ self._root = None if isinstance(path, basestring): + path = tounicode(path, encoding=_SYS_FS_ENCODING) self._path = path if mode == "w": if os.path.exists(path): @@ -99,6 +105,7 @@ def close(self): """ def _fsRootPath(self, path): + path = tounicode(path, encoding=_SYS_FS_ENCODING) if self._root is None: return path return self.joinPath(self._root, path) From 1de6eb1eb566ae1ac97b315f2e9c98d689806cc0 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 13:33:15 +0100 Subject: [PATCH 40/71] [filesystem] pass create=True to movedir otherwise it raises `ResourceNotFound` --- Lib/ufoLib/filesystem.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 2f9770a..5db4761 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -136,13 +136,12 @@ def _fsRemoveTree(self, path): self._fs.removetree(path) def _fsMove(self, path1, path2): - if self.isDirectory(path1): - meth = self._fs.movedir - else: - meth = self._fs.move path1 = self._fsRootPath(path1) path2 = self._fsRootPath(path2) - meth(path1, path2) + if self.isDirectory(path1): + self._fs.movedir(path1, path2, create=True) + else: + self._fs.move(path1, path2) def _fsExists(self, path): path = self._fsRootPath(path) From 337a68715e89bd2f01681826ec545470ccc8b2e5 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 13:41:14 +0100 Subject: [PATCH 41/71] [filesystem] getinfo now returns a namespace, not a dict We bumped the minimum required fonttools to 3.10, so we can use the backported SimpleNamespace as the return value of _NOFS.getinfo method. --- Lib/ufoLib/filesystem.py | 7 ++++--- setup.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 5db4761..d4237f9 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -158,7 +158,7 @@ def _fsListDirectory(self, path): def _fsGetFileModificationTime(self, path): path = self._fsRootPath(path) info = self._fs.getinfo(path) - return info["modified_time"] + return info.modified # ----------------- # Path Manipulation @@ -474,10 +474,11 @@ def listdir(self, path): return os.listdir(path) def getinfo(self, path): + from fontTools.misc.py23 import SimpleNamespace path = self._absPath(path) stat = os.stat(path) - info = dict( - modified_time=stat.st_mtime + info = SimpleNamespace( + modified=stat.st_mtime ) return info diff --git a/setup.py b/setup.py index 2367fbb..9fc3005 100755 --- a/setup.py +++ b/setup.py @@ -165,7 +165,7 @@ def run(self): 'pytest>=3.0.2', ], install_requires=[ - "fonttools>=3.1.2", + "fonttools>=3.10.0", ], cmdclass={ "release": release, From 46f32d4cd41ab5a26284a396728d50b15b64c473 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 13:43:33 +0100 Subject: [PATCH 42/71] [filesystem] print exception in readPlist --- Lib/ufoLib/filesystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index d4237f9..aa3eec3 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -395,8 +395,8 @@ def readPlist(self, path, default=None): try: with self.open(path, "rb") as f: return readPlist(f) - except: - raise UFOLibError("The file %s could not be read." % path) + except Exception as e: + raise UFOLibError("The file %s could not be read: %s" % (path, str(e))) def writePlist(self, path, obj): """ From 3c0d41bac9d71c6e98daa631148c175b55adc959 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 13:50:15 +0100 Subject: [PATCH 43/71] [filesystem] use the current ZipFS API; create "contents" dir The 'mode' and 'allow_zip_64' arguments are no longer there in pyfileststem2's zipfs module; allow_zip_64 is the default now, and the 'mode' is replaced by a 'write' boolean argument The "contents" root dir must be created manually. --- Lib/ufoLib/filesystem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index aa3eec3..37ad153 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -72,10 +72,12 @@ def __init__(self, path, mode="r", structure=None): elif structure == "zip": if not haveFS: raise UFOLibError("The fs module is required for reading and writing UFO ZIP.") - path = ZipFS(path, mode=mode, allow_zip_64=True, encoding="utf8") + path = ZipFS( + path, write=True if mode == 'w' else False, encoding="utf8") roots = path.listdir("") if not roots: - self._root = "contents" + self._root = u"contents" + path.makedir(self._root) elif len(roots) > 1: raise UFOLibError("The UFO contains more than one root.") else: From 22a6f917e43eb5deb9add3d94acdeb268212a22d Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 14:04:10 +0100 Subject: [PATCH 44/71] [filesystem] implement 'create' argument for _NOFS.movedir in pyfilesystem2, movedir copies the content from one directory to antoher, optionally creating the destination if create=True. When the destination is exists and is a directory, shutil.move moves the source directory inside the destinatinn directory. shutil.copytree also fails if the destination already exists; so here we resort to distutils.dir_util.copy_tree (it's part of python std lib) --- Lib/ufoLib/filesystem.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 37ad153..7d3af97 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -458,10 +458,35 @@ def move(self, path1, path2): path2 = self._absPath(path2) os.move(path1, path2) - def movedir(self, path1, path2): + def movedir(self, path1, path2, create=False): path1 = self._absPath(path1) path2 = self._absPath(path2) - shutil.move(path1, path2) + exists = False + if not create: + if not os.path.exists(path2): + raise UFOLibError("%r not found" % path2) + elif not os.path.isdir(path2): + raise UFOLibError("%r should be a directory" % path2) + else: + exists = True + else: + if os.path.exists(path2): + if not os.path.isdir(path2): + raise UFOLibError("%r should be a directory" % path2) + else: + exists = True + if exists: + # if destination is an existing directory, shutil.move then moves + # the source directory inside that directory; in pyfilesytem2, + # movedir only moves the content between the src and dst folders. + # Here we use distutils' copy_tree instead of shutil.copytree, as + # the latter does not work if destination exists + from distutils.dir_util import copy_tree + copy_tree(path1, path2) + shutil.rmtree(path1) + else: + # shutil.move creates destination if not exists yet + shutil.move(path1, path2) def exists(self, path): path = self._absPath(path) From c06c38cadb9fa0e426eca6d46368ff11da5de0c8 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 14:05:59 +0100 Subject: [PATCH 45/71] [ufoLib] UFOReader.getFileModificationTime is used by defcon --- Lib/ufoLib/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 1896675..214fa0e 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -171,6 +171,11 @@ def _upConvertKerning(self): self._upConvertedKerningData["groups"] = groups self._upConvertedKerningData["groupRenameMaps"] = conversionMaps + # support methods + + def getFileModificationTime(self, path): + return self.fileSystem.getFileModificationTime(path) + # metainfo.plist def readMetaInfo(self): From e3d89cee7f2c7434e2fdaecdf2ce091c2e4e0ad3 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 14:08:41 +0100 Subject: [PATCH 46/71] [ufoLib] fix listing images dir; pass a file object to pngValidator --- Lib/ufoLib/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 214fa0e..29dabff 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -434,13 +434,14 @@ def getImageDirectoryListing(self): if not self.fileSystem.isDirectory(IMAGES_DIRNAME): raise UFOLibError("The UFO contains an \"images\" file instead of a directory.") result = [] - for fileName in self.fileSystem.listDirectory(path): - if self.fileSystem.isDirectory(fileName): + for fileName in self.fileSystem.listDirectory(IMAGES_DIRNAME): + path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) + if self.fileSystem.isDirectory(path): # silently skip this as version control # systems often have hidden directories continue - # XXX this is sending a path to the validator. that won't work in the abstracted filesystem. - valid, error = pngValidator(path=p) + with self.fileSystem.open(path, mode='rb') as fp: + valid, error = pngValidator(fileObj=fp) if valid: result.append(fileName) return result From 2bef40c30d811d9a0e48fe219f866d2be99bc750 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 14:11:13 +0100 Subject: [PATCH 47/71] [ufoLib] use self.fileSystem.joinPath instead of reader.joinPath --- Lib/ufoLib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 29dabff..f37ebd4 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -1028,8 +1028,8 @@ def copyImageFromReader(self, reader, sourceFileName, destFileName): """ if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - sourcePath = reader.joinPath(IMAGES_DIRNAME, sourceFileName) - destPath = self.joinPath(IMAGES_DIRNAME, destFileName) + sourcePath = self.fileSystem.joinPath(IMAGES_DIRNAME, sourceFileName) + destPath = self.fileSystem.joinPath(IMAGES_DIRNAME, destFileName) self.copyFromReader(reader, sourcePath, destPath) From ec8d58f0b83a2db5b88fbf7179eaf92371b3a333 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 16:40:28 +0100 Subject: [PATCH 48/71] [filesystem] in listDirectory, always use Unix '/' as separator the _NOFS implementation is using `os.listdir` which uses the native `os.sep`; however, pyfilesystem2 FS paths always uses forward slashes '/'. --- Lib/ufoLib/filesystem.py | 8 ++++++-- Lib/ufoLib/test/test_UFO3.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 7d3af97..4fa2e53 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -204,8 +204,9 @@ def listDirectory(self, path, recurse=False): return self._listDirectory(path, recurse=recurse, relativeTo=path) def _listDirectory(self, path, recurse=False, relativeTo=None, depth=0, maxDepth=100): - if not relativeTo.endswith("/"): - relativeTo += "/" + sep = os.sep + if not relativeTo.endswith(sep): + relativeTo += sep if depth > maxDepth: raise UFOLibError("Maximum recusion depth reached.") result = [] @@ -215,6 +216,9 @@ def _listDirectory(self, path, recurse=False, relativeTo=None, depth=0, maxDepth result += self._listDirectory(p, recurse=True, relativeTo=relativeTo, depth=depth+1, maxDepth=maxDepth) else: p = p[len(relativeTo):] + if sep != "/": + # replace '\\' with '/' + path = path.replace(sep, "/") result.append(p) return result diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index 4410cd5..87f82b2 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -4147,8 +4147,8 @@ def testUFOReaderDataDirectoryListing(self): reader = UFOReader(self.getFontPath()) found = reader.getDataDirectoryListing() expected = [ - 'org.unifiedfontobject.directory%(s)sbar%(s)slol.txt' % {'s': os.sep}, - 'org.unifiedfontobject.directory%(s)sfoo.txt' % {'s': os.sep}, + 'org.unifiedfontobject.directory/bar/lol.txt', + 'org.unifiedfontobject.directory/foo.txt', 'org.unifiedfontobject.file.txt' ] self.assertEqual(set(found), set(expected)) From a06da7681e7e38971519315bb74abb5abb4d862c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 16:45:22 +0100 Subject: [PATCH 49/71] fixup --- Lib/ufoLib/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 4fa2e53..ab2b723 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -218,7 +218,7 @@ def _listDirectory(self, path, recurse=False, relativeTo=None, depth=0, maxDepth p = p[len(relativeTo):] if sep != "/": # replace '\\' with '/' - path = path.replace(sep, "/") + p = p.replace(sep, "/") result.append(p) return result From f1f4e120f539d3264741104cd64c76e1813487c0 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 17:43:00 +0100 Subject: [PATCH 50/71] =?UTF-8?q?[filesystem]=20exclude=20'=5F=5FMACOSX'?= =?UTF-8?q?=20metadata=C2=A0dir=20contained=20in=20zip=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/ufoLib/filesystem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index ab2b723..34e6269 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -74,7 +74,11 @@ def __init__(self, path, mode="r", structure=None): raise UFOLibError("The fs module is required for reading and writing UFO ZIP.") path = ZipFS( path, write=True if mode == 'w' else False, encoding="utf8") - roots = path.listdir("") + roots = [ + p for p in path.listdir("") + # exclude macOS metadata contained in zip file + if path.isdir(p) and p != "__MACOSX" + ] if not roots: self._root = u"contents" path.makedir(self._root) From 4d9291fe71725850fac1a0358b2188b640adc407 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Jul 2017 17:46:00 +0100 Subject: [PATCH 51/71] [plistlib] in python3, explicitly use fmt=PlistFormat.FMT_XML Otherwise it will try to sniff the plist format and call `seek` method on the file object, but the zip filesystem does not support that... UnsupportedOperation: seek --- Lib/ufoLib/plistlib.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Lib/ufoLib/plistlib.py b/Lib/ufoLib/plistlib.py index 2a84c42..69cd444 100644 --- a/Lib/ufoLib/plistlib.py +++ b/Lib/ufoLib/plistlib.py @@ -5,8 +5,22 @@ import sys try: from plistlib import ( - load as readPlist, dump as writePlist, - loads as readPlistFromString, dumps as writePlistToString) + load as _readPlist, dump as _writePlist, + loads as _readPlistFromString, dumps as _writePlistToString, + PlistFormat) + + def readPlist(*args, **kwargs): + return _readPlist(fmt=PlistFormat.FMT_XML, *args, **kwargs) + + def writePlist(*args, **kwargs): + return _writePlist(fmt=PlistFormat.FMT_XML, *args, **kwargs) + + def readPlistFromString(*args, **kwargs): + return _readPlistFromString(fmt=PlistFormat.FMT_XML, *args, **kwargs) + + def writePlistToString(*args, **kwargs): + return _writePlistToString(fmt=PlistFormat.FMT_XML, *args, **kwargs) + from plistlib import _PlistParser, _PlistWriter # Public API changed in Python 3.4 if sys.version_info >= (3, 4): From 2be975d42a7332a406fb50a852cd64ee6c443f20 Mon Sep 17 00:00:00 2001 From: Masaya Nakamura Date: Wed, 2 Aug 2017 18:15:50 +0900 Subject: [PATCH 52/71] [filesystem] Fix error on opening zip in Python 2 --- Lib/ufoLib/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index 34e6269..ad6a241 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -75,7 +75,7 @@ def __init__(self, path, mode="r", structure=None): path = ZipFS( path, write=True if mode == 'w' else False, encoding="utf8") roots = [ - p for p in path.listdir("") + p for p in path.listdir(u"") # exclude macOS metadata contained in zip file if path.isdir(p) and p != "__MACOSX" ] From d8c02c2f138b69a761963c7312ad653431ebe6ec Mon Sep 17 00:00:00 2001 From: Masaya Nakamura Date: Mon, 7 Aug 2017 13:52:33 +0900 Subject: [PATCH 53/71] [validators] Make layerContentsValidator() compatible with FileSystem --- Lib/ufoLib/__init__.py | 5 ++--- Lib/ufoLib/validators.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index f37ebd4..b54961c 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -346,8 +346,7 @@ def _readLayerContents(self): if self._formatVersion < 3: return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] contents = self.fileSystem.readPlist(LAYERCONTENTS_FILENAME) - # XXX this won't work with the abstracted file system - valid, error = layerContentsValidator(contents, self._path) + valid, error = layerContentsValidator(contents, self.fileSystem) if not valid: raise UFOLibError(error) return contents @@ -832,7 +831,7 @@ def _readLayerContents(self): # read the file on disk raw = self.fileSystem.readPlist(LAYERCONTENTS_FILENAME) contents = {} - valid, error = layerContentsValidator(raw, self._path) + valid, error = layerContentsValidator(raw, self.fileSystem) if not valid: raise UFOLibError(error) for entry in raw: diff --git a/Lib/ufoLib/validators.py b/Lib/ufoLib/validators.py index b453615..f666a8e 100644 --- a/Lib/ufoLib/validators.py +++ b/Lib/ufoLib/validators.py @@ -1,6 +1,5 @@ """Various low level data validators.""" -import os import calendar from io import open @@ -772,11 +771,17 @@ def pngValidator(path=None, data=None, fileObj=None): # layercontents.plist # ------------------- -def layerContentsValidator(value, ufoPath): +def layerContentsValidator(value, ufoPathOrFileSystem): """ Check the validity of layercontents.plist. Version 3+. """ + from ufoLib.filesystem import FileSystem + if isinstance(ufoPathOrFileSystem, FileSystem): + fileSystem = ufoPathOrFileSystem + else: + fileSystem = FileSystem(ufoPathOrFileSystem) + bogusFileMessage = "layercontents.plist in not in the correct format." # file isn't in the right format if not isinstance(value, list): @@ -802,8 +807,7 @@ def layerContentsValidator(value, ufoPath): if len(layerName) == 0: return False, "Empty layer name in layercontents.plist." # directory doesn't exist - p = os.path.join(ufoPath, directoryName) - if not os.path.exists(p): + if not fileSystem.exists(directoryName): return False, "A glyphset does not exist at %s." % directoryName # default layer name if layerName == "public.default" and directoryName != "glyphs": From c7882c84b25684054299d0d4b422bbc084a6565d Mon Sep 17 00:00:00 2001 From: Masaya Nakamura Date: Tue, 7 Nov 2017 18:12:07 +0900 Subject: [PATCH 54/71] [filesystem] Specify namespaces for getinfo() https://pyfilesystem2.readthedocs.io/en/latest/info.html#namespaces --- Lib/ufoLib/filesystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index ad6a241..458cba4 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -163,7 +163,7 @@ def _fsListDirectory(self, path): def _fsGetFileModificationTime(self, path): path = self._fsRootPath(path) - info = self._fs.getinfo(path) + info = self._fs.getinfo(path, namespaces=["details"]) return info.modified # ----------------- @@ -508,7 +508,7 @@ def listdir(self, path): path = self._absPath(path) return os.listdir(path) - def getinfo(self, path): + def getinfo(self, path, namespaces=None): from fontTools.misc.py23 import SimpleNamespace path = self._absPath(path) stat = os.stat(path) From 7426785166d5336c227052e14e29732f7e3d6fd9 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 3 Oct 2018 13:04:49 +0100 Subject: [PATCH 55/71] tox.ini: update fs to 2.1.1 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 11e9bbc..2752b6c 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ basepython = deps = pytest -rrequirements.txt - fs: fs==2.0.4 + fs: fs==2.1.1 commands = # run the test suite against the package installed inside tox env. # any extra positional arguments after `tox -- ...` are passed on to pytest From 56b450591389b64c170a4aa31c60a6def104358c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 8 Oct 2018 18:01:01 +0100 Subject: [PATCH 56/71] various cleanups: use pyfilesystem2 API directly, remove ufoLib.filesystem Among other things, this allows clients to inject an already instantiated FS object in both the UFOReader/UFOWriter 'path' argument. --- .travis.yml | 6 +- Lib/ufoLib/__init__.py | 600 ++++++++++++++++++++++++++++----------- Lib/ufoLib/_common.py | 127 +++++++++ Lib/ufoLib/errors.py | 10 +- Lib/ufoLib/filesystem.py | 523 ---------------------------------- Lib/ufoLib/glifLib.py | 172 +++++++---- Lib/ufoLib/validators.py | 7 +- appveyor.yml | 6 +- requirements.txt | 1 + setup.py | 5 + tox.ini | 3 +- 11 files changed, 695 insertions(+), 765 deletions(-) create mode 100644 Lib/ufoLib/_common.py delete mode 100644 Lib/ufoLib/filesystem.py diff --git a/.travis.yml b/.travis.yml index 236c1e6..3093733 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,11 @@ env: - secure: MgeE4CfZKuKkG9Qzl/OobMprNgsLExuGGKbWNDbYmOj7qyhEcPsKIPgPrOiY5n7ZCdXZ5V/m2ZgA18HwGOsV2VliZIalyzKqYE9tEu3yObhccqoAgrm+HzqaKHZnpuZf0AlGRc3h8jY7GAZITlUEIEYQ6hBKP1GdXOgv4GeaqK0nHwHIE/SzQc/VQR1edzJBpj+IX4tYpyVMEd/PhuL89WgiJdPkpNP6ArY1dDyc6qCp8Y9H9+Bay+A/xJ1MSd3KYVoPYPWuMfuea7YxVoQT7UPJJrWmmafZgPYbdS56HvVtHQBsKXkIzjTxHEta7lKP/EVxjtjTAIdRn1pNAeNaVhVPPBF4JJNuAYv54eVlE2MJeftoYN8CSVq93TM+b3H3osWDWilFRPjsEyZjOKLEkekMp5hPL4+AJOSGde7+cN25CM8/DymU4I0fM458Cibq8CUZmt4VSTYGIa6jS6MJUuTGg1RU6nYHZU8PBRmhBvMxz0oL3lZGM24OBRCOlSgeObQQMtUZnX9gE9+Lfo5KfDW8Tn1OJjeBxboCXcca4MEjn0IiXaP8s+Vk8g1yO2bletfT5apTPfhuqOWfqzAQMI+KJP7pmcR8SuXNd9bjqX6ZHol7s3PiHTjzdV4l3nPr2vFy7UlnTvuirdPXrk4qhcOqL/3KOILE5AG64QO2WRk= matrix: include: - - env: TOXENV=py27-cov,py27-cov-lxml,py27-cov-lxml-fs + - env: TOXENV=py27-cov,py27-cov-lxml python: 2.7 - - env: TOXENV=py36-cov,py36-cov-lxml,py36-cov-lxml-fs + - env: TOXENV=py36-cov,py36-cov-lxml python: 3.6 - - env: TOXENV=py37-cov,py37-cov-lxml,py37-cov-lxml-fs + - env: TOXENV=py37-cov,py37-cov-lxml python: 3.7 dist: xenial sudo: true diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index e8ad67a..c46362e 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -1,10 +1,18 @@ from __future__ import absolute_import, unicode_literals import os -import shutil -from io import StringIO, BytesIO, open from copy import deepcopy +import zipfile +import enum +import fs +import fs.base +import fs.subfs +import fs.errors +import fs.copy +import fs.osfs +import fs.zipfs +import fs.tempfs +import fs.tools from fontTools.misc.py23 import basestring, unicode, tounicode -from ufoLib.filesystem import FileSystem from ufoLib.glifLib import GlyphSet from ufoLib.validators import * from ufoLib.filenames import userNameToFileName @@ -59,9 +67,6 @@ __version__ = "2.3.2" - - - # ---------- # File Names # ---------- @@ -83,6 +88,29 @@ supportedUFOFormatVersions = [1, 2, 3] +class UFOFileStructure(enum.Enum): + ZIP = "zip" + PACKAGE = "package" + + +def _sniffFileStructure(ufo_path): + """Return UFOFileStructure.ZIP if the UFO at path 'ufo_path' (basestring) + is a zip file, else return UFOFileStructure.PACKAGE if 'ufo_path' is a + directory. + Raise UFOLibError if it is a file with unknown structure, or if the path + does not exist. + """ + if zipfile.is_zipfile(ufo_path): + return UFOFileStructure.ZIP + elif os.path.isdir(ufo_path): + return UFOFileStructure.PACKAGE + elif os.path.isfile(ufo_path): + raise UFOLibError( + "The specified UFO does not have a known structure: '%s'" % ufo_path + ) + else: + raise UFOLibError("No such file or directory: '%s'" % ufo_path) + # ---------- # UFO Reader # ---------- @@ -97,7 +125,69 @@ class UFOReader(object): """ def __init__(self, path, validate=True): - self.fileSystem = FileSystem(path) + if hasattr(path, "__fspath__"): # support os.PathLike objects + path = path.__fspath__() + + if isinstance(path, basestring): + structure = self.sniffFileStructure(path) + try: + if structure is UFOFileStructure.ZIP: + parentFS = fs.zipfs.ZipFS(path, write=False, encoding="utf-8") + else: + parentFS = fs.osfs.OSFS(path) + except fs.errors.CreateFailed as e: + raise UFOLibError("unable to open '%s': %s" % (path, e)) + + if structure is UFOFileStructure.ZIP: + # .ufoz zip files must contain a single root directory, with arbitrary + # name, containing all the UFO files + rootDirs = [ + p.name for p in parentFS.scandir("/") + # exclude macOS metadata contained in zip file + if p.is_dir and p.name != "__MACOSX" + ] + if len(rootDirs) == 1: + # 'ClosingSubFS' ensures that the parent zip file is closed when + # its root subdirectory is closed + self.fs = parentFS.opendir( + rootDirs[0], factory=fs.subfs.ClosingSubFS + ) + else: + raise UFOLibError( + "Expected exactly 1 root directory, found %d" % len(rootDirs) + ) + else: + # normal UFO 'packages' are just a single folder + self.fs = parentFS + # when passed a path string, we make sure we close the newly opened fs + # upon calling UFOReader.close method or context manager's __exit__ + self._shouldClose = True + self._fileStructure = structure + elif isinstance(path, fs.base.FS): + filesystem = path + try: + filesystem.check() + except fs.errors.FilesystemClosed: + raise UFOLibError("the filesystem '%s' is closed" % path) + else: + self.fs = filesystem + try: + path = filesystem.getsyspath("/") + except fs.errors.NoSysPath: + # network or in-memory FS may not map to the local one + path = unicode(filesystem) + else: + path = path.rstrip("/") + # when user passed an already initialized fs instance, it is her + # responsibility to close it, thus UFOReader.close/__exit__ are no-op + self._shouldClose = False + # default to a 'package' structure + self._fileStructure = UFOFileStructure.PACKAGE + else: + raise TypeError( + "Expected a path string or fs.base.FS object, found '%s'" + % type(path).__name__ + ) self._path = path self._validate = validate self.readMetaInfo(validate=validate) @@ -110,25 +200,16 @@ def _get_formatVersion(self): formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.") - def readBytesFromPath(self, path, encoding=None): - """ - Returns the bytes in the file at the given path. - The path must be relative to the UFO path. - Returns None if the file does not exist. - An encoding may be passed if needed. - """ - return self.fileSystem.readBytesFromPath(path, encoding=encoding) - - def getReadFileForPath(self, path, encoding=None): - """ - Returns a file (or file-like) object for the - file at the given path. The path must be relative - to the UFO path. Returns None if the file does - not exist An encoding may be passed if needed. + def _get_fileStructure(self): + return self._fileStructure - Note: The caller is responsible for closing the open file. - """ - return self.fileSystem.open(path, mode="rb", encoding=encoding) + fileStructure = property( + _get_fileStructure, + doc=( + "The current file structure of the UFO: " + "either UFOFileStructure.ZIP or UFOFileStructure.PACKAGE" + ) + ) # up conversion @@ -181,9 +262,31 @@ def _upConvertKerning(self, validate): # support methods - def getFileModificationTime(self, path): - return self.fileSystem.getFileModificationTime(path) + from ufoLib._common import ( + _getPlist, + getFileModificationTime, + readBytesFromPath, + ) + + sniffFileStructure = staticmethod(_sniffFileStructure) + def getReadFileForPath(self, path, encoding=None): + """ + Returns a file (or file-like) object for the file at the given path. + The path must be relative to the UFO path. + Returns None if the file does not exist. + By default the file is opened in binary mode (reads bytes). + If encoding is passed, the file is opened in text mode (reads unicode). + + Note: The caller is responsible for closing the open file. + """ + try: + if encoding is None: + return self.fs.openbin(path) + else: + return self.fs.open(path, mode="r", encoding=encoding) + except fs.errors.ResourceNotFound: + return None # metainfo.plist def readMetaInfo(self, validate=None): @@ -195,22 +298,27 @@ def readMetaInfo(self, validate=None): """ if validate is None: validate = self._validate - data = self.fileSystem.readPlist(METAINFO_FILENAME) + data = self._getPlist(METAINFO_FILENAME) if validate and not isinstance(data, dict): raise UFOLibError("metainfo.plist is not properly formatted.") formatVersion = data["formatVersion"] if validate: if not isinstance(formatVersion, int): - metaplist_path = os.path.join(self._path, METAINFO_FILENAME) - raise UFOLibError("formatVersion must be specified as an integer in " + metaplist_path) + raise UFOLibError( + "formatVersion must be specified as an integer in '%s' on %s" + % (METAINFO_FILENAME, self.fs) + ) if formatVersion not in supportedUFOFormatVersions: - raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path)) + raise UFOLibError( + "Unsupported UFO format (%d) in '%s' on %s" + % (formatVersion, METAINFO_FILENAME, self.fs) + ) self._formatVersion = formatVersion # groups.plist def _readGroups(self): - return self.fileSystem.readPlist(GROUPS_FILENAME, {}) + return self._getPlist(GROUPS_FILENAME, {}) def readGroups(self, validate=None): """ @@ -262,7 +370,7 @@ def getKerningGroupConversionRenameMaps(self, validate=None): # fontinfo.plist def _readInfo(self, validate): - data = self.fileSystem.readPlist(FONTINFO_FILENAME, {}) + data = self._getPlist(FONTINFO_FILENAME, {}) if validate and not isinstance(data, dict): raise UFOLibError("fontinfo.plist is not properly formatted.") return data @@ -320,7 +428,7 @@ def readInfo(self, info, validate=None): # kerning.plist def _readKerning(self): - data = self.fileSystem.readPlist(KERNING_FILENAME, {}) + data = self._getPlist(KERNING_FILENAME, {}) return data def readKerning(self, validate=None): @@ -362,7 +470,7 @@ def readLib(self, validate=None): """ if validate is None: validate = self._validate - data = self.fileSystem.readPlist(LIB_FILENAME, {}) + data = self._getPlist(LIB_FILENAME, {}) if validate: valid, message = fontLibValidator(data) if not valid: @@ -373,13 +481,14 @@ def readLib(self, validate=None): def readFeatures(self): """ - Read features.fea. Returns a string. + Read features.fea. Return a unicode string. + The returned string is empty if the file is missing. """ - if not self.fileSystem.exists(FEATURES_FILENAME): + try: + with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8") as f: + return f.read() + except fs.errors.ResourceNotFound: return "" - with self.fileSystem.open(FEATURES_FILENAME, "r", encoding="utf-8") as f: - text = f.read() - return text # glyph sets & layers @@ -392,9 +501,9 @@ def _readLayerContents(self, validate): """ if self._formatVersion < 3: return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)] - contents = self.fileSystem.readPlist(LAYERCONTENTS_FILENAME) + contents = self._getPlist(LAYERCONTENTS_FILENAME) if validate: - valid, error = layerContentsValidator(contents, self.fileSystem) + valid, error = layerContentsValidator(contents, self.fs) if not valid: raise UFOLibError(error) return contents @@ -455,9 +564,14 @@ def getGlyphSet(self, layerName=None, validateRead=None, validateWrite=None): break if directory is None: raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName) + try: + glyphSubFS = self.fs.opendir(directory) + except fs.errors.ResourceNotFound: + raise UFOLibError( + "No '%s' directory for layer '%s'" % (directory, layerName) + ) return GlyphSet( - directory, - fileSystem=self.fileSystem, + glyphSubFS, ufoFormatVersion=self._formatVersion, validateRead=validateRead, validateWrite=validateWrite, @@ -490,10 +604,15 @@ def getDataDirectoryListing(self): This will not list directory names, only file names. Thus, empty directories will be skipped. """ - if not self.fileSystem.exists(DATA_DIRNAME): + try: + # fs Walker.files method returns "absolute" paths (in terms of the + # root of the 'data' SubFS), so we strip the leading '/' to make + # them relative + return [ + p.lstrip("/") for p in self.fs.opendir(DATA_DIRNAME).walk.files() + ] + except fs.errors.ResourceError: return [] - listing = self.fileSystem.listDirectory(DATA_DIRNAME, recurse=True) - return listing def getImageDirectoryListing(self, validate=None): """ @@ -508,24 +627,24 @@ def getImageDirectoryListing(self, validate=None): return [] if validate is None: validate = self._validate - if not self.fileSystem.exists(IMAGES_DIRNAME): + if not self.fs.exists(IMAGES_DIRNAME): return [] - if not self.fileSystem.isDirectory(IMAGES_DIRNAME): + elif not self.fs.isdir(IMAGES_DIRNAME): raise UFOLibError("The UFO contains an \"images\" file instead of a directory.") result = [] - for fileName in self.fileSystem.listDirectory(IMAGES_DIRNAME): - path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) - if self.fileSystem.isDirectory(path): + self._imagesFS = imagesFS = self.fs.opendir(IMAGES_DIRNAME) + for path in imagesFS.scandir("/"): + if path.is_dir: # silently skip this as version control # systems often have hidden directories continue if validate: - with self.fileSystem.open(path, mode='rb') as fp: + with imagesFS.openbin(path.name) as fp: valid, error = pngValidator(fileObj=fp) if valid: - result.append(fileName) + result.append(path.name) else: - result.append(fileName) + result.append(path.name) return result def readImage(self, fileName, validate=None): @@ -539,8 +658,12 @@ def readImage(self, fileName, validate=None): validate = self._validate if self._formatVersion < 3: raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion) - path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) - data = self.fileSystem.readBytesFromPath(path) + try: + imagesFS = self._imagesFS + except AttributeError: + # in case readImage is called before getImageDirectoryListing + imagesFS = self.fs.opendir(IMAGES_DIRNAME) + data = imagesFS.getbytes(fileName) if data is None: raise UFOLibError("No image file named %s." % fileName) if validate: @@ -549,6 +672,16 @@ def readImage(self, fileName, validate=None): raise UFOLibError(error) return data + def close(self): + if self._shouldClose: + self.fs.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + # ---------- # UFO Writer @@ -572,26 +705,105 @@ def __init__( structure=None, validate=True, ): - # XXX - # for testing only - if isinstance(path, basestring) and structure is None: - ext = os.path.splitext(path)[-1].lower() - if ext == ".ufo": - structure = "package" - elif ext == ".ufoz": - structure = "zip" - else: - structure = "zip" - - if isinstance(structure, basestring) and structure == "zip": - file, ext = os.path.splitext(path) - ext.lower() - if ext != ".ufoz": - path = file + '.ufoz' - # /XXX - if formatVersion not in supportedUFOFormatVersions: raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) + + if hasattr(path, "__fspath__"): # support os.PathLike objects + path = path.__fspath__() + + if isinstance(path, basestring): + havePreviousFile = os.path.exists(path) + if havePreviousFile: + # ensure we use the same structure as the destination + existingStructure = self.sniffFileStructure(path) + if structure is not None: + try: + structure = UFOFileStructure(structure) + except ValueError: + raise UFOLibError( + "Invalid or unsupported structure: '%s'" % structure + ) + if structure is not existingStructure: + raise UFOLibError( + "A UFO with a different structure (%s) already exists " + "at the given path: '%s'" % (existingStructure, path) + ) + else: + structure = existingStructure + else: + # if not exists, default to 'package' structure + if structure is None: + structure = UFOFileStructure.PACKAGE + dirName = os.path.dirname(path) + if dirName and not os.path.isdir(dirName): + raise UFOLibError( + "Cannot write to '%s': directory does not exist" % path + ) + if structure is UFOFileStructure.ZIP: + if havePreviousFile: + # we can't write a zip in-place, so we have to copy its + # contents to a temporary location and work from there, then + # upon closing UFOWriter we create the final zip file + parentFS = fs.tempfs.TempFS() + with fs.zipfs.ZipFS(path, encoding="utf-8") as origFS: + fs.copy.copy_fs(origFS, parentFS) + # if output path is an existing zip, we require that it contains + # one, and only one, root directory (with arbitrary name), in turn + # containing all the existing UFO contents + rootDirs = [ + p.name for p in parentFS.scandir("/") + # exclude macOS metadata contained in zip file + if p.is_dir and p.name != "__MACOSX" + ] + if len(rootDirs) != 1: + raise UFOLibError( + "Expected exactly 1 root directory, found %d" % len(rootDirs) + ) + else: + # 'ClosingSubFS' ensures that the parent filesystem is closed + # when its root subdirectory is closed + self.fs = parentFS.opendir( + rootDirs[0], factory=fs.subfs.ClosingSubFS + ) + else: + # if the output zip file didn't exist, we create the root folder; + # we name it the same as input 'path', but with '.ufo' extension + rootDir = os.path.splitext(os.path.basename(path))[0] + ".ufo" + parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8") + parentFS.makedir(rootDir) + self.fs = parentFS.opendir(rootDir, factory=fs.subfs.ClosingSubFS) + else: + self.fs = fs.osfs.OSFS(path, create=True) + self._fileStructure = structure + self._havePreviousFile = havePreviousFile + self._shouldClose = True + elif isinstance(path, fs.base.FS): + filesystem = path + try: + filesystem.check() + except fs.errors.FilesystemClosed: + raise UFOLibError("the filesystem '%s' is closed" % path) + else: + self.fs = filesystem + try: + path = filesystem.getsyspath("/") + except fs.errors.NoSysPath: + # network or in-memory FS may not map to the local one + path = str(filesystem) + else: + path = path.rstrip("/") + # if passed an FS object, always default to 'package' structure + self._fileStructure = UFOFileStructure.PACKAGE + # if FS contains a "metainfo.plist", we consider it non-empty + self._havePreviousFile = filesystem.exists(METAINFO_FILENAME) + # the user is responsible for closing the FS object + self._shouldClose = False + else: + raise TypeError( + "Expected a path string or fs object, found %s" + % type(path).__name__ + ) + # establish some basic stuff self._path = path self._formatVersion = formatVersion @@ -600,21 +812,17 @@ def __init__( self._validate = validate # if the file already exists, get the format version. # this will be needed for up and down conversion. - havePreviousFile = False - if isinstance(path, basestring) and os.path.exists(path): - havePreviousFile = True - self.fileSystem = FileSystem(path, mode="w", structure=structure) previousFormatVersion = None if havePreviousFile: - metaInfo = self.fileSystem.readPlist(METAINFO_FILENAME) + metaInfo = self._getPlist(METAINFO_FILENAME) previousFormatVersion = metaInfo.get("formatVersion") try: previousFormatVersion = int(previousFormatVersion) - except: - self.fileSystem.close() + except (ValueError, TypeError): + self.fs.close() raise UFOLibError("The existing metainfo.plist is not properly formatted.") if previousFormatVersion not in supportedUFOFormatVersions: - self.fileSystem.close() + self.fs.close() raise UFOLibError("Unsupported UFO format (%d)." % formatVersion) # catch down conversion if previousFormatVersion is not None and previousFormatVersion > formatVersion: @@ -627,7 +835,7 @@ def __init__( else: # previous < 3 # imply the layer contents - if self.fileSystem.exists(DEFAULT_GLYPHS_DIRNAME): + if self.fs.exists(DEFAULT_GLYPHS_DIRNAME): self.layerContents = {DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME} # write the new metainfo self._writeMetaInfo() @@ -635,9 +843,16 @@ def __init__( # properties def _get_path(self): + import warnings + + warnings.warn( + "The 'path' attribute is deprecated; use the 'fs' attribute instead", + DeprecationWarning, + stacklevel=2, + ) return self._path - path = property(_get_path, doc="The path the UFO is being written to.") + path = property(_get_path, doc="The path the UFO is being written to (DEPRECATED).") def _get_formatVersion(self): return self._formatVersion @@ -649,80 +864,115 @@ def _get_fileCreator(self): fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.") + def _get_fileStructure(self): + return self._fileStructure + + fileStructure = property( + _get_fileStructure, + doc=( + "The file structure of the destination UFO: " + "either UFOFileStrucure.ZIP or UFOFileStructure.PACKAGE" + ) + ) + + # support methods for file system interaction + + from ufoLib._common import ( + _getPlist, + _writePlist, + readBytesFromPath, + getFileModificationTime, + ) + + sniffFileStructure = staticmethod(_sniffFileStructure) + def copyFromReader(self, reader, sourcePath, destPath): """ Copy the sourcePath in the provided UFOReader to destPath - in this writer. The paths must be relative. This only - works with individual files, not directories. + in this writer. The paths must be relative. This works with + both individual files and directories. """ if not isinstance(reader, UFOReader): raise UFOLibError("The reader must be an instance of UFOReader.") - if not reader.fileSystem.exists(sourcePath): + if not reader.fs.exists(sourcePath): raise UFOLibError("The reader does not have data located at \"%s\"." % sourcePath) - if self.fileSystem.exists(destPath): + if self.fs.exists(destPath): raise UFOLibError("A file named \"%s\" already exists." % destPath) - if reader.fileSystem.isDirectory(sourcePath): - self._copyDirectoryFromReader(reader, sourcePath, destPath) + # create the destination directory if it doesn't exist + self.fs.makedirs(fs.path.dirname(destPath), recreate=True) + if reader.fs.isdir(sourcePath): + fs.copy.copy_dir(reader.fs, sourcePath, self.fs, destPath) else: - data = reader.fileSystem.readBytesFromPath(sourcePath) - self.fileSystem.writeBytesToPath(destPath, data) + fs.copy.copy_file(reader.fs, sourcePath, self.fs, destPath) - def _copyDirectoryFromReader(self, reader, sourcePath, destPath): - # create the destination directory if it doesn't exist - if not self.fileSystem.exists(destPath): - destDirectory = destPath - destTree = [] - while destDirectory: - destDirectory, b = self.fileSystem.splitPath(destDirectory) - destTree.insert(0, b) - for i, d in enumerate(destTree): - p = self.fileSystem.joinPath(*(destTree[:i] + [d])) - if not self.fileSystem.exists(p): - self.fileSystem.makeDirectory(p) - # copy everything in the source directory - for fileName in reader.fileSystem.listDirectory(sourcePath): - fullSourcePath = self.fileSystem.joinPath(sourcePath, fileName) - fullDestPath = self.fileSystem.joinPath(destPath, fileName) - self.copyFromReader(reader, fullSourcePath, fullDestPath) - - - def writeBytesToPath(self, path, data, encoding=None): - """ - Write bytes to path. If needed, the directory tree - for the given path will be built. The path must be - relative to the UFO. An encoding may be passed if needed. - """ - self.fileSystem.writeBytesToPath(path, data, encoding=encoding) - - def readBytesFromPath(self, path, encoding=None): - """ - Returns the bytes in the file at the given path. - The path must be relative to the UFO path. - Returns None if the file does not exist. - An encoding may be passed if needed. + def writeBytesToPath(self, path, data): + """ + Write bytes to a path relative to the UFO filesystem's root. + If writing to an existing UFO, check to see if data matches the data + that is already in the file at path; if so, the file is not rewritten + so that the modification date is preserved. + If needed, the directory tree for the given path will be built. """ - return self.fileSystem.readBytesFromPath(path, encoding=encoding) + if self._havePreviousFile: + if self.fs.isfile(path) and data == self.fs.getbytes(path): + return + try: + self.fs.setbytes(path, data) + except fs.errors.FileExpected: + raise UFOLibError("A directory exists at '%s'" % path) + except fs.errors.ResourceNotFound: + self.fs.makedirs(fs.path.dirname(path), recreate=True) + self.fs.setbytes(path, data) def getFileObjectForPath(self, path, mode="w", encoding=None): """ Returns a file (or file-like) object for the file at the given path. The path must be relative to the UFO path. Returns None if the file does - not exist and the mode is "r" or "rb. An encoding - may be passed if needed. + not exist and the mode is "r" or "rb. + An encoding may be passed if the file is opened in text mode. Note: The caller is responsible for closing the open file. """ - return self.fileSystem.open(path, mode=mode, encoding=encoding) + try: + return self.fs.open(path, mode=mode, encoding=encoding) + except fs.errors.ResourceNotFound as e: + m = mode[0] + if m == "r": + # XXX I think we should just let it raise. The docstring, + # however, says that this returns None if mode is 'r' + return None + elif m == "w" or m == "a" or m == "x": + self.fs.makedirs(fs.path.dirname(path), recreate=True) + return self.fs.open(path, mode=mode, encoding=encoding) + except fs.errors.ResourceError as e: + return UFOLibError( + "unable to open '%s' on %s: %s" % (path, self.fs, e) + ) - def removeFileForPath(self, path): + def removePath(self, path, force=False, removeEmptyParents=True): """ Remove the file (or directory) at path. The path must be relative to the UFO. + Raises UFOLibError if the path doesn't exist. + If force=True, ignore non-existent paths. """ - if not self.fileSystem.exists(path): - raise UFOLibError("%s does not exist." % path) - self.fileSystem.remove(path) + try: + self.fs.remove(path) + except fs.errors.FileExpected: + self.fs.removetree(path) + except fs.errors.ResourceNotFound: + if not force: + raise UFOLibError( + "'%s' does not exist on %s" % (path, self.fs) + ) + if removeEmptyParents: + parent = fs.path.dirname(path) + if parent: + fs.tools.remove_empty(self.fs, parent) + + # alias kept for backward compatibility with old API + removeFileForPath = removePath # UFO mod time @@ -743,7 +993,7 @@ def _writeMetaInfo(self): creator=self._fileCreator, formatVersion=self._formatVersion ) - self.fileSystem.writePlist(METAINFO_FILENAME, metaInfo) + self._writePlist(METAINFO_FILENAME, metaInfo) # groups.plist @@ -815,12 +1065,12 @@ def writeGroups(self, groups, validate=None): groups = remappedGroups # pack and write groupsNew = {} - for key, value in list(groups.items()): + for key, value in groups.items(): groupsNew[key] = list(value) if groupsNew: - self.fileSystem.writePlist(GROUPS_FILENAME, groupsNew) - else: - self.fileSystem.remove(GROUPS_FILENAME) + self._writePlist(GROUPS_FILENAME, groupsNew) + elif self._havePreviousFile: + self.removePath(GROUPS_FILENAME, force=True, removeEmptyParents=False) # fontinfo.plist @@ -862,7 +1112,7 @@ def writeInfo(self, info, validate=None): infoData = validateInfoVersion2Data(infoData) infoData = _convertFontInfoDataVersion2ToVersion1(infoData) # write file - self.fileSystem.writePlist(FONTINFO_FILENAME, infoData) + self._writePlist(FONTINFO_FILENAME, infoData) # kerning.plist @@ -908,15 +1158,15 @@ def writeKerning(self, kerning, validate=None): kerning = remappedKerning # pack and write kerningDict = {} - for left, right in list(kerning.keys()): + for left, right in kerning.keys(): value = kerning[left, right] if left not in kerningDict: kerningDict[left] = {} kerningDict[left][right] = value if kerningDict: - self.fileSystem.writePlist(KERNING_FILENAME, kerningDict) + self._writePlist(KERNING_FILENAME, kerningDict) else: - self.fileSystem.remove(KERNING_FILENAME) + self.removePath(KERNING_FILENAME, force=True, removeEmptyParents=False) # lib.plist @@ -935,9 +1185,9 @@ def writeLib(self, libDict, validate=None): if not valid: raise UFOLibError(message) if libDict: - self.fileSystem.writePlist(LIB_FILENAME, libDict) + self._writePlist(LIB_FILENAME, libDict) else: - self.fileSystem.remove(LIB_FILENAME) + self.removePath(LIB_FILENAME, force=True, removeEmptyParents=False) # features.fea @@ -953,7 +1203,10 @@ def writeFeatures(self, features, validate=None): if validate: if not isinstance(features, basestring): raise UFOLibError("The features are not text.") - self.fileSystem.writeBytesToPath(FEATURES_FILENAME, features.encode("utf8")) + if features: + self.writeBytesToPath(FEATURES_FILENAME, features.encode("utf8")) + else: + self.removePath(FEATURES_FILENAME, force=True, removeEmptyParents=False) # glyph sets & layers @@ -965,10 +1218,10 @@ def _readLayerContents(self, validate): ``validate`` will validate the data. """ # read the file on disk - raw = self.fileSystem.readPlist(LAYERCONTENTS_FILENAME) + raw = self._getPlist(LAYERCONTENTS_FILENAME) contents = {} if validate: - valid, error = layerContentsValidator(raw, self.fileSystem) + valid, error = layerContentsValidator(raw, self.fs) if not valid: raise UFOLibError(error) for entry in raw: @@ -999,7 +1252,7 @@ def writeLayerContents(self, layerOrder=None, validate=None): if validate and set(layerOrder) != set(self.layerContents.keys()): raise UFOLibError("The layer order content does not match the glyph sets that have been created.") layerContents = [(layerName, self.layerContents[layerName]) for layerName in layerOrder] - self.fileSystem.writePlist(LAYERCONTENTS_FILENAME, layerContents) + self._writePlist(LAYERCONTENTS_FILENAME, layerContents) def _findDirectoryForLayerName(self, layerName): foundDirectory = None @@ -1051,11 +1304,13 @@ def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc) elif self.formatVersion == 3: return self._getGlyphSetFormatVersion3(validateRead, validateWrite, layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc) + else: + raise AssertionError(self.formatVersion) def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): + glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True), return GlyphSet( - DEFAULT_GLYPHS_DIRNAME, - fileSystem=self.fileSystem, + glyphSubFS, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=1, validateRead=validateRead, @@ -1063,9 +1318,9 @@ def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFil ) def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): + glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True) return GlyphSet( - DEFAULT_GLYPHS_DIRNAME, - fileSystem=self.fileSystem, + glyphSubFS, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=2, validateRead=validateRead, @@ -1101,13 +1356,12 @@ def _getGlyphSetFormatVersion3(self, validateRead, validateWrite, layerName=None raise UFOLibError("The specified layer name is not a Unicode string.") directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.") # make the directory - self.fileSystem.makeDirectory(directory) + glyphSubFS = self.fs.makedir(directory, recreate=True) # store the mapping self.layerContents[layerName] = directory # load the glyph set return GlyphSet( - directory, - fileSystem=self.fileSystem, + glyphSubFS, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3, validateRead=validateRead, @@ -1155,7 +1409,7 @@ def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False): del self.layerContents[layerName] self.layerContents[newLayerName] = newDirectory # do the file system copy - self.fileSystem.move(oldDirectory, newDirectory) + self.fs.movedir(oldDirectory, newDirectory, create=True) def deleteGlyphSet(self, layerName): """ @@ -1166,7 +1420,7 @@ def deleteGlyphSet(self, layerName): # just write the data from the default layer return foundDirectory = self._findDirectoryForLayerName(layerName) - self.fileSystem.remove(foundDirectory) + self.removePath(foundDirectory, removeEmptyParents=False) del self.layerContents[layerName] # /images @@ -1184,20 +1438,16 @@ def writeImage(self, fileName, data, validate=None): valid, error = pngValidator(data=data) if not valid: raise UFOLibError(error) - path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) - self.fileSystem.writeBytesToPath(path, data) + self.writeBytesToPath("%s/%s" % (IMAGES_DIRNAME, fileName), data) - def removeImage(self, fileName, validate=None): + def removeImage(self, fileName, validate=None): # XXX remove unused 'validate'? """ Remove the file named fileName from the images directory. """ - if validate is None: - validate = self._validate if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName) - self.fileSystem.remove(path) + self.removePath("%s/%s" % (IMAGES_DIRNAME, fileName)) def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None): """ @@ -1209,10 +1459,26 @@ def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=Non validate = self._validate if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - sourcePath = self.fileSystem.joinPath(IMAGES_DIRNAME, sourceFileName) - destPath = self.fileSystem.joinPath(IMAGES_DIRNAME, destFileName) + sourcePath = "%s/%s" % (IMAGES_DIRNAME, sourceFileName) + destPath = "%s/%s" % (IMAGES_DIRNAME, destFileName) self.copyFromReader(reader, sourcePath, destPath) + def close(self): + if self._havePreviousFile and self._fileStructure is UFOFileStructure.ZIP: + # if we are updating an existing zip file, we can now compress the + # contents of the temporary filesystem in the destination path + rootDir = os.path.splitext(os.path.basename(self._path))[0] + ".ufo" + with fs.zipfs.ZipFS(self._path, write=True, encoding="utf-8") as destFS: + fs.copy.copy_fs(self.fs, destFS.makedir(rootDir)) + if self._shouldClose: + self.fs.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + # ---------------- # Helper Functions diff --git a/Lib/ufoLib/_common.py b/Lib/ufoLib/_common.py new file mode 100644 index 0000000..8802aef --- /dev/null +++ b/Lib/ufoLib/_common.py @@ -0,0 +1,127 @@ +"""Private support methods shared by UFOReader, UFOWriter or glifLib.GlyphSet. +""" +from __future__ import absolute_import, unicode_literals +from ufoLib import plistlib +from ufoLib.errors import UFOLibError +import fs.errors +from datetime import datetime + + +if hasattr(datetime, "timestamp"): # python >= 3.3 + + def _timestamp(dt): + return dt.timestamp() + +else: + from datetime import tzinfo, timedelta + + ZERO = timedelta(0) + + class UTC(tzinfo): + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + + utc = UTC() + + EPOCH = datetime.fromtimestamp(0, tz=utc) + + def _timestamp(dt): + return (dt - EPOCH).total_seconds() + + +def getFileModificationTime(self, path): + """ + Returns the modification time for the file at the given path, as a + floating point number giving the number of seconds since the epoch. + The path must be relative to the UFO path. + Returns None if the file does not exist. + """ + try: + dt = self.fs.getinfo(path, namespaces=["details"]).modified + except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound): + return None + else: + return _timestamp(dt) + + +def readBytesFromPath(self, path): + """ + Returns the bytes in the file at the given path. + The path must be relative to the UFO's filesystem root. + Returns None if the file does not exist. + """ + try: + return self.fs.getbytes(path) + except fs.errors.ResourceNotFound: + return None + + +def _getPlist(self, fileName, default=None): + """ + Read a property list relative to the UFO filesystem's root. + Raises UFOLibError if the file is missing and default is None, + otherwise default is returned. + + The errors that could be raised during the reading of a plist are + unpredictable and/or too large to list, so, a blind try: except: + is done. If an exception occurs, a UFOLibError will be raised. + """ + try: + with self.fs.open(fileName, "rb") as f: + return plistlib.load(f) + except fs.errors.ResourceNotFound: + if default is None: + raise UFOLibError( + "'%s' is missing on %s. This file is required" + % (fileName, self.fs) + ) + else: + return default + except Exception as e: + # TODO(anthrotype): try to narrow this down a little + raise UFOLibError( + "'%s' could not be read on %s: %s" % (fileName, self.fs, e) + ) + + +_msg = ( + "'%s' could not be written on %s because " + "the data is not properly formatted: %s" +) + + +def _writePlist(self, fileName, obj): + """ + Write a property list to a file relative to the UFO filesystem's root. + + Do this sort of atomically, making it harder to corrupt existing files, + for example when plistlib encounters an error halfway during write. + This also checks to see if text matches the text that is already in the + file at path. If so, the file is not rewritten so that the modification + date is preserved. + + The errors that could be raised during the writing of a plist are + unpredictable and/or too large to list, so, a blind try: except: is done. + If an exception occurs, a UFOLibError will be raised. + """ + if self._havePreviousFile: + try: + data = plistlib.dumps(obj) + except Exception as e: + raise UFOLibError(_msg % (fileName, self.fs, e)) + if self.fs.exists(fileName) and data == self.fs.getbytes(fileName): + return + self.fs.setbytes(fileName, data) + else: + with self.fs.openbin(fileName, mode="w") as fp: + try: + plistlib.dump(obj, fp) + except Exception as e: + raise UFOLibError(_msg % (fileName, self.fs, e)) diff --git a/Lib/ufoLib/errors.py b/Lib/ufoLib/errors.py index 984d189..fb048d1 100644 --- a/Lib/ufoLib/errors.py +++ b/Lib/ufoLib/errors.py @@ -1 +1,9 @@ -class UFOLibError(Exception): pass \ No newline at end of file +from __future__ import absolute_import, unicode_literals + + +class UFOLibError(Exception): + pass + + +class GlifLibError(UFOLibError): + pass diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py deleted file mode 100644 index 26041d4..0000000 --- a/Lib/ufoLib/filesystem.py +++ /dev/null @@ -1,523 +0,0 @@ -import os -import sys -import shutil -from io import StringIO, BytesIO, open -import zipfile -from fontTools.misc.py23 import tounicode - -haveFS = False -try: - import fs - from fs.osfs import OSFS - from fs.zipfs import ZipFS - haveFS = True -except ImportError: - pass - -from ufoLib import plistlib -from ufoLib.errors import UFOLibError - -try: - basestring -except NameError: - basestring = str - -_SYS_FS_ENCODING = sys.getfilesystemencoding() - - -def sniffFileStructure(path): - if zipfile.is_zipfile(path): - return "zip" - elif os.path.isdir(path): - return "package" - raise UFOLibError("The specified UFO does not have a known structure.") - - -class FileSystem(object): - - def __init__(self, path, mode="r", structure=None): - """ - path can be a path or another FileSystem object. - - mode can be r or w. - - structure is only applicable in "w" mode. Options: - None: existing structure - package: package structure - zip: zipped package - - mode and structure are both ignored if a FileSystem - object is given for path. - """ - self._root = None - if isinstance(path, basestring): - path = tounicode(path, encoding=_SYS_FS_ENCODING) - self._path = path - if mode == "w": - if os.path.exists(path): - existingStructure = sniffFileStructure(path) - if structure is not None: - if structure != existingStructure: - raise UFOLibError("A UFO with a different structure already exists at the given path.") - else: - structure = existingStructure - elif mode == "r": - if not os.path.exists(path): - raise UFOLibError("The specified UFO doesn't exist: %r" % path) - structure = sniffFileStructure(path) - if structure == "package": - if mode == "w" and not os.path.exists(path): - os.mkdir(path) - path = OSFS(path) - elif structure == "zip": - if not haveFS: - raise UFOLibError("The fs module is required for reading and writing UFO ZIP.") - path = ZipFS( - path, write=True if mode == 'w' else False, encoding="utf8") - roots = [ - p for p in path.listdir(u"") - # exclude macOS metadata contained in zip file - if path.isdir(p) and p != "__MACOSX" - ] - if not roots: - self._root = u"contents" - path.makedir(self._root) - elif len(roots) > 1: - raise UFOLibError("The UFO contains more than one root.") - else: - self._root = roots[0] - elif isinstance(path, self.__class__): - self._root = path._root - self._path = path._path - path = path._fs - else: - raise TypeError(path) - self._fs = path - - def close(self): - self._fsClose() - - # -------- - # fs Calls - # -------- - - """ - All actual low-level file system interaction - MUST be done through these methods. - - This is necessary because ZIPs will have a - top level root directory that packages will - not have. - """ - - def _fsRootPath(self, path): - path = tounicode(path, encoding=_SYS_FS_ENCODING) - if self._root is None: - return path - return self.joinPath(self._root, path) - - def _fsUnRootPath(self, path): - if self._root is None: - return path - return self.relativePath(path, self._root) - - def _fsClose(self): - self._fs.close() - - def _fsOpen(self, path, mode="r", encoding=None): - path = self._fsRootPath(path) - f = self._fs.open(path, mode, encoding=encoding) - return f - - def _fsRemove(self, path): - path = self._fsRootPath(path) - self._fs.remove(path) - - def _fsMakeDirectory(self, path): - path = self._fsRootPath(path) - self._fs.makedir(path) - - def _fsRemoveTree(self, path): - path = self._fsRootPath(path) - self._fs.removetree(path) - - def _fsMove(self, path1, path2): - path1 = self._fsRootPath(path1) - path2 = self._fsRootPath(path2) - if self.isDirectory(path1): - self._fs.movedir(path1, path2, create=True) - else: - self._fs.move(path1, path2) - - def _fsExists(self, path): - path = self._fsRootPath(path) - return self._fs.exists(path) - - def _fsIsDirectory(self, path): - path = self._fsRootPath(path) - return self._fs.isdir(path) - - def _fsListDirectory(self, path): - path = self._fsRootPath(path) - return self._fs.listdir(path) - - def _fsGetFileModificationTime(self, path): - path = self._fsRootPath(path) - info = self._fs.getinfo(path, namespaces=["details"]) - return info.modified - - # ----------------- - # Path Manipulation - # ----------------- - - def joinPath(self, *parts): - if haveFS: - return fs.path.join(*parts) - else: - return os.path.join(*parts) - - def splitPath(self, path): - if haveFS: - return fs.path.split(path) - else: - return os.path.split(path) - - def directoryName(self, path): - if haveFS: - return fs.path.dirname(path) - else: - return os.path.dirname(path) - - def relativePath(self, path, start): - if haveFS: - return fs.relativefrom(path, start) - else: - return os.path.relpath(path, start) - - # --------- - # Existence - # --------- - - def exists(self, path): - return self._fsExists(path) - - def isDirectory(self, path): - return self._fsIsDirectory(path) - - def listDirectory(self, path, recurse=False): - return self._listDirectory(path, recurse=recurse, relativeTo=path) - - def _listDirectory(self, path, recurse=False, relativeTo=None, depth=0, maxDepth=100): - sep = os.sep - if not relativeTo.endswith(sep): - relativeTo += sep - if depth > maxDepth: - raise UFOLibError("Maximum recusion depth reached.") - result = [] - for fileName in self._fsListDirectory(path): - p = self.joinPath(path, fileName) - if self.isDirectory(p) and recurse: - result += self._listDirectory(p, recurse=True, relativeTo=relativeTo, depth=depth+1, maxDepth=maxDepth) - else: - p = p[len(relativeTo):] - if sep != "/": - # replace '\\' with '/' - p = p.replace(sep, "/") - result.append(p) - return result - - def makeDirectory(self, path): - if not self.exists(path): - self._fsMakeDirectory(path) - - # ----------- - # File Opener - # ----------- - - def open(self, path, mode="r", encoding=None): - """ - Returns a file (or file-like) object for the - file at the given path. The path must be relative - to the UFO path. Returns None if the file does - not exist and the mode is "r" or "rb. An encoding - may be passed if needed. - - Note: The caller is responsible for closing the open file. - """ - if encoding: - if mode == "r": - mode = "rb" - elif mode == "w": - mode = "wb" - if mode in ("r", "rb") and not self.exists(path): - return None - if self.exists(path) and self.isDirectory(path): - raise UFOLibError("%s is a directory." % path) - if mode in ("w", "wb"): - self._buildDirectoryTree(path) - f = self._fsOpen(path, mode, encoding=encoding) - return f - - def _buildDirectoryTree(self, path): - directory, fileName = self.splitPath(path) - directoryTree = [] - while directory: - directory, d = self.splitPath(directory) - directoryTree.append(d) - directoryTree.reverse() - built = "" - for d in directoryTree: - d = self.joinPath(built, d) - self.makeDirectory(d) - built = d - - # ------------------ - # Modification Times - # ------------------ - - def getFileModificationTime(self, path): - """ - Returns the modification time (as reported by os.path.getmtime) - for the file at the given path. The path must be relative to - the UFO path. Returns None if the file does not exist. - """ - if not self.exists(path): - return None - return self._fsGetFileModificationTime(path) - - # -------------- - # Raw Read/Write - # -------------- - - def readBytesFromPath(self, path, encoding=None): - """ - Returns the bytes in the file at the given path. - The path must be relative to the UFO path. - Returns None if the file does not exist. - An encoding may be passed if needed. - """ - f = self.open(path, mode="rb", encoding=encoding) - if f is None: - return None - data = f.read() - f.close() - return data - - def writeBytesToPath(self, path, data, encoding=None): - """ - Write bytes to path. If needed, the directory tree - for the given path will be built. The path must be - relative to the UFO. An encoding may be passed if needed. - """ - if encoding: - data = StringIO(data).encode(encoding) - self._writeFileAtomically(data, path) - - def _writeFileAtomically(self, data, path): - """ - Write data into a file at path. Do this sort of atomically - making it harder to cause corrupt files. This also checks to see - if data matches the data that is already in the file at path. - If so, the file is not rewritten so that the modification date - is preserved. - """ - assert isinstance(data, bytes) - if self.exists(path): - f = self.open(path, "rb") - oldData = f.read() - f.close() - if data == oldData: - return - if data: - f = self.open(path, "wb") - f.write(data) - f.close() - - # ------------ - # File Removal - # ------------ - - def remove(self, path): - """ - Remove the file (or directory) at path. The path - must be relative to the UFO. - """ - if not self.exists(path): - return - self._removeFileForPath(path, raiseErrorIfMissing=True) - - def _removeFileForPath(self, path, raiseErrorIfMissing=False): - if not self.exists(path): - if raiseErrorIfMissing: - raise UFOLibError("The file %s does not exist." % path) - else: - if self.isDirectory(path): - self._fsRemoveTree(path) - else: - self._fsRemove(path) - directory = self.directoryName(path) - if directory: - self._removeEmptyDirectoriesForPath(directory) - - def _removeEmptyDirectoriesForPath(self, directory): - if not self.exists(directory): - return - if not len(self._fsListDirectory(directory)): - self._fsRemoveTree(directory) - else: - return - directory = self.directoryName(directory) - if directory: - self._removeEmptyDirectoriesForPath(directory) - - # ---- - # Move - # ---- - - def move(self, path1, path2): - if not self.exists(path1): - raise UFOLibError("%s does not exist." % path1) - if self.exists(path2): - raise UFOLibError("%s already exists." % path2) - self._fsMove(path1, path2) - - # -------------- - # Property Lists - # -------------- - - def readPlist(self, path, default=None): - """ - Read a property list relative to the - UFO path. If the file is missing and - default is None a UFOLibError will be - raised. Otherwise default is returned. - The errors that could be raised during - the reading of a plist are unpredictable - and/or too large to list, so, a blind - try: except: is done. If an exception - occurs, a UFOLibError will be raised. - """ - if not self.exists(path): - if default is not None: - return default - else: - raise UFOLibError("%s is missing. This file is required" % path) - try: - with self.open(path, "rb") as f: - return plistlib.load(f) - except Exception as e: - raise UFOLibError("The file %s could not be read: %s" % (path, str(e))) - - def writePlist(self, path, obj): - """ - Write a property list. - - Do this sort of atomically, making it harder to - cause corrupt files, for example when writePlist - encounters an error halfway during write. This - also checks to see if text matches the text that - is already in the file at path. If so, the file - is not rewritten so that the modification date - is preserved. - - The errors that could be raised during the writing - of a plist are unpredictable and/or too large to list, - so, a blind try: except: is done. If an exception occurs, - a UFOLibError will be raised. - """ - try: - data = plistlib.dumps(obj) - except Exception as e: - raise UFOLibError( - "The data for the file %s could not be written because " - "it is not properly formatted: %s" % (path, e) - ) - self.writeBytesToPath(path, data) - - -class _NOFS(object): - - def __init__(self, path): - self._path = path - - def _absPath(self, path): - return os.path.join(self._path, path) - - def close(self): - pass - - def open(self, path, mode, encoding=None): - path = self._absPath(path) - return open(path, mode=mode, encoding=encoding) - - def remove(self, path): - path = self._absPath(path) - os.remove(path) - - def makedir(self, path): - path = self._absPath(path) - os.mkdir(path) - - def removetree(self, path): - path = self._absPath(path) - shutil.rmtree(path) - - def move(self, path1, path2): - path1 = self._absPath(path1) - path2 = self._absPath(path2) - os.move(path1, path2) - - def movedir(self, path1, path2, create=False): - path1 = self._absPath(path1) - path2 = self._absPath(path2) - exists = False - if not create: - if not os.path.exists(path2): - raise UFOLibError("%r not found" % path2) - elif not os.path.isdir(path2): - raise UFOLibError("%r should be a directory" % path2) - else: - exists = True - else: - if os.path.exists(path2): - if not os.path.isdir(path2): - raise UFOLibError("%r should be a directory" % path2) - else: - exists = True - if exists: - # if destination is an existing directory, shutil.move then moves - # the source directory inside that directory; in pyfilesytem2, - # movedir only moves the content between the src and dst folders. - # Here we use distutils' copy_tree instead of shutil.copytree, as - # the latter does not work if destination exists - from distutils.dir_util import copy_tree - copy_tree(path1, path2) - shutil.rmtree(path1) - else: - # shutil.move creates destination if not exists yet - shutil.move(path1, path2) - - def exists(self, path): - path = self._absPath(path) - return os.path.exists(path) - - def isdir(self, path): - path = self._absPath(path) - return os.path.isdir(path) - - def listdir(self, path): - path = self._absPath(path) - return os.listdir(path) - - def getinfo(self, path, namespaces=None): - from fontTools.misc.py23 import SimpleNamespace - path = self._absPath(path) - stat = os.stat(path) - info = SimpleNamespace( - modified=stat.st_mtime - ) - return info - - -if not haveFS: - OSFS = _NOFS diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index c1df445..bae6552 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -12,16 +12,27 @@ """ from __future__ import absolute_import, unicode_literals -from io import BytesIO, open from warnings import warn from collections import OrderedDict +import fs +import fs.base +import fs.errors +import fs.osfs +import fs.path from fontTools.misc.py23 import basestring, unicode, tobytes, tounicode -from ufoLib.filesystem import FileSystem from ufoLib import plistlib +from ufoLib.errors import GlifLibError from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen from ufoLib.filenames import userNameToFileName -from ufoLib.validators import isDictEnough, genericTypeValidator, colorValidator,\ - guidelinesValidator, anchorsValidator, identifierValidator, imageValidator, glyphLibValidator +from ufoLib.validators import ( + genericTypeValidator, + colorValidator, + guidelinesValidator, + anchorsValidator, + identifierValidator, + imageValidator, + glyphLibValidator, +) from ufoLib import etree @@ -33,12 +44,11 @@ ] -class GlifLibError(Exception): pass - # --------- # Constants # --------- +CONTENTS_FILENAME = "contents.plist" LAYERINFO_FILENAME = "layerinfo.plist" supportedUFOFormatVersions = [1, 2, 3] supportedGLIFFormatVersions = [1, 2] @@ -96,15 +106,15 @@ class GlyphSet(object): def __init__( self, - dirName, - fileSystem=None, + path, glyphNameToFileNameFunc=None, ufoFormatVersion=3, validateRead=True, validateWrite=True, ): """ - 'dirName' should be a path to an existing directory. + 'path' should be a path (string) to an existing local directory, or + an instance of fs.base.FS class. The optional 'glyphNameToFileNameFunc' argument must be a callback function that takes two arguments: a glyph name and a list of all @@ -117,22 +127,51 @@ def __init__( """ if ufoFormatVersion not in supportedUFOFormatVersions: raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion) - if fileSystem is None: - fileSystem = FileSystem(dirName) - dirName = "" - self.dirName = dirName - self.fileSystem = fileSystem + if isinstance(path, basestring): + try: + filesystem = fs.osfs.OSFS(path) + except fs.errors.CreateFailed: + raise GlifLibError("No glyphs directory '%s'" % path) + self._shouldClose = True + elif isinstance(path, fs.base.FS): + filesystem = path + try: + filesystem.check() + except fs.errors.FilesystemClosed: + raise GlifLibError("the filesystem '%s' is closed" % filesystem) + self._shouldClose = False + else: + raise TypeError( + "Expected a path string or fs object, found %s" + % type(path).__name__ + ) + try: + path = filesystem.getsyspath("/") + except fs.errors.NoSysPath: + # network or in-memory FS may not map to the local one + path = unicode(filesystem) + # 'dirName' is kept for backward compatibility only, but it's DEPRECATED + # as it's not guaranteed that it maps to an existing OSFS directory. + # Client could use the FS api via the `self.fs` attribute instead. + self.dirName = fs.path.parts(path)[-1] + self.fs = filesystem + # if glyphSet contains no 'contents.plist', we consider it empty + self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME) self.ufoFormatVersion = ufoFormatVersion if glyphNameToFileNameFunc is None: glyphNameToFileNameFunc = glyphNameToFileName self.glyphNameToFileName = glyphNameToFileNameFunc self._validateRead = validateRead self._validateWrite = validateWrite - self.rebuildContents() self._existingFileNames = None self._reverseContents = None self._glifCache = {} + self.rebuildContents() + + # here we reuse the same methods from UFOReader/UFOWriter + from ufoLib._common import _getPlist, _writePlist, getFileModificationTime + def rebuildContents(self, validateRead=None): """ Rebuild the contents dict by loading contents.plist. @@ -142,12 +181,7 @@ def rebuildContents(self, validateRead=None): """ if validateRead is None: validateRead = self._validateRead - contentsPath = self.fileSystem.joinPath(self.dirName, "contents.plist") - if not self.fileSystem.exists(contentsPath): - # missing, consider the glyphset empty. - contents = {} - else: - contents = self.fileSystem.readPlist(contentsPath) + contents = self._getPlist(CONTENTS_FILENAME, {}) # validate the contents if validateRead: invalidFormat = False @@ -159,10 +193,13 @@ def rebuildContents(self, validateRead=None): invalidFormat = True if not isinstance(fileName, basestring): invalidFormat = True - elif not self.fileSystem.exists(self.fileSystem.joinPath(self.dirName, fileName)): - raise GlifLibError("contents.plist references a file that does not exist: %s" % fileName) + elif not self.fs.exists(fileName): + raise GlifLibError( + "%s references a file that does not exist: %s" + % (CONTENTS_FILENAME, fileName) + ) if invalidFormat: - raise GlifLibError("contents.plist is not properly formatted") + raise GlifLibError("%s is not properly formatted" % CONTENTS_FILENAME) self.contents = contents self._existingFileNames = None self._reverseContents = None @@ -188,8 +225,7 @@ def writeContents(self): Write the contents.plist file out to disk. Call this method when you're done writing glyphs. """ - path = self.fileSystem.joinPath(self.dirName, "contents.plist") - self.fileSystem.writePlist(path, self.contents) + self._writePlist(CONTENTS_FILENAME, self.contents) # layer info @@ -200,10 +236,7 @@ def readLayerInfo(self, info, validateRead=None): """ if validateRead is None: validateRead = self._validateRead - path = self.fileSystem.joinPath(self.dirName, LAYERINFO_FILENAME) - if not self.fileSystem.exists(path): - return - infoDict = self.fileSystem.readPlist(path) + infoDict = self._getPlist(LAYERINFO_FILENAME, {}) if validateRead: if not isinstance(infoDict, dict): raise GlifLibError("layerinfo.plist is not properly formatted.") @@ -235,12 +268,15 @@ def writeLayerInfo(self, info, validateWrite=None): if value is None or (attr == 'lib' and not value): continue infoData[attr] = value - # validate - if validateWrite: - infoData = validateLayerInfoVersion3Data(infoData) - # write file - path = self.fileSystem.joinPath(self.dirName, LAYERINFO_FILENAME) - self.fileSystem.writePlist(path, infoData) + if infoData: + # validate + if validateWrite: + infoData = validateLayerInfoVersion3Data(infoData) + # write file + self._writePlist(LAYERINFO_FILENAME, infoData) + elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME): + # data empty, remove existing file + self.fs.remove(LAYERINFO_FILENAME) # # read caching @@ -260,22 +296,15 @@ def getGLIF(self, glyphName): efficiency, the cached GLIF will be purged by various other methods such as readGlyph. """ - needRead = False fileName = self.contents.get(glyphName) - path = None - if fileName is not None: - path = self.fileSystem.joinPath(self.dirName, fileName) - if glyphName not in self._glifCache: - needRead = True - elif fileName is not None and self.fileSystem.getFileModificationTime(path) != self._glifCache[glyphName][1]: - needRead = True - if needRead: - fileName = self.contents[glyphName] - if not self.fileSystem.exists(path): + if glyphName not in self._glifCache or ( + fileName is not None + and self.getFileModificationTime(fileName) != self._glifCache[glyphName][1] + ): + if fileName is None or not self.fs.exists(fileName): raise KeyError(glyphName) - with self.fileSystem.open(path, "rb") as f: - text = f.read() - self._glifCache[glyphName] = (text, self.fileSystem.getFileModificationTime(path)) + data = self.fs.getbytes(fileName) + self._glifCache[glyphName] = (data, self.getFileModificationTime(fileName)) return self._glifCache[glyphName][0] def getGLIFModificationTime(self, glyphName): @@ -376,11 +405,13 @@ def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVer if formatVersion not in supportedGLIFFormatVersions: raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion) if formatVersion == 2 and self.ufoFormatVersion < 3: - raise GlifLibError("Unsupported GLIF format version (%d) for UFO format version %d." % (formatVersion, self.ufoFormatVersion)) + raise GlifLibError( + "Unsupported GLIF format version (%d) for UFO format version %d." + % (formatVersion, self.ufoFormatVersion) + ) if validate is None: validate = self._validateWrite self._purgeCachedGLIF(glyphName) - data = _writeGlyphToBytes(glyphName, glyphObject, drawPointsFunc, formatVersion=formatVersion, validate=validate) fileName = self.contents.get(glyphName) if fileName is None: if self._existingFileNames is None: @@ -392,15 +423,20 @@ def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVer self._existingFileNames[fileName] = fileName.lower() if self._reverseContents is not None: self._reverseContents[fileName.lower()] = glyphName - self.fileSystem.makeDirectory(self.dirName) - path = self.fileSystem.joinPath(self.dirName, fileName) - if self.fileSystem.exists(path): - with self.fileSystem.open(path, "rb") as f: - oldData = f.read() - if data == oldData: - return - with self.fileSystem.open(path, "wb") as f: - f.write(data) + data = _writeGlyphToBytes( + glyphName, + glyphObject, + drawPointsFunc, + formatVersion=formatVersion, + validate=validate, + ) + if ( + self._havePreviousFile + and self.fs.exists(fileName) + and data == self.fs.getbytes(fileName) + ): + return + self.fs.setbytes(fileName, data) def deleteGlyph(self, glyphName): """Permanently delete the glyph from the glyph set on disk. Will @@ -408,8 +444,7 @@ def deleteGlyph(self, glyphName): """ self._purgeCachedGLIF(glyphName) fileName = self.contents[glyphName] - path = self.fileSystem.joinPath(self.dirName, fileName) - self.fileSystem.remove(path) + self.fs.remove(fileName) if self._existingFileNames is not None: del self._existingFileNames[fileName] if self._reverseContents is not None: @@ -481,6 +516,17 @@ def getImageReferences(self, glyphNames=None): images[glyphName] = _fetchImageFileName(text) return images + def close(self): + if self._shouldClose: + self.fs.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + + # ----------------------- # Glyph Name to File Name # ----------------------- diff --git a/Lib/ufoLib/validators.py b/Lib/ufoLib/validators.py index 66ef23c..46c7502 100644 --- a/Lib/ufoLib/validators.py +++ b/Lib/ufoLib/validators.py @@ -3,6 +3,8 @@ from __future__ import absolute_import, unicode_literals import calendar from io import open +import fs.base +import fs.osfs try: from collections.abc import Mapping # python >= 3.3 @@ -779,11 +781,10 @@ def layerContentsValidator(value, ufoPathOrFileSystem): Check the validity of layercontents.plist. Version 3+. """ - from ufoLib.filesystem import FileSystem - if isinstance(ufoPathOrFileSystem, FileSystem): + if isinstance(ufoPathOrFileSystem, fs.base.FS): fileSystem = ufoPathOrFileSystem else: - fileSystem = FileSystem(ufoPathOrFileSystem) + fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem) bogusFileMessage = "layercontents.plist in not in the correct format." # file isn't in the right format diff --git a/appveyor.yml b/appveyor.yml index 4789707..507429a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,17 +4,17 @@ environment: - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - TOXENV: "py27-cov,py27-cov-lxml,py27-cov-lxml-fs" + TOXENV: "py27-cov,py27-cov-lxml" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" - TOXENV: "py36-cov,py36-cov-lxml,py36-cov-lxml-fs" + TOXENV: "py36-cov,py36-cov-lxml" - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "64" - TOXENV: "py37-cov,py37-cov-lxml,py37-cov-lxml-fs" + TOXENV: "py37-cov,py37-cov-lxml" skip_branch_with_pr: true diff --git a/requirements.txt b/requirements.txt index 5553f36..d02d86f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ fonttools==3.30.0 +fs==2.1.1 diff --git a/setup.py b/setup.py index 3bd8d69..5ff29ac 100755 --- a/setup.py +++ b/setup.py @@ -166,6 +166,7 @@ def run(self): ], install_requires=[ "fonttools >= 3.10.0, < 4", + "fs >= 2.1.1, < 3", ], extras_require={ "lxml": [ @@ -177,6 +178,10 @@ def run(self): "pytest-cov >= 2.5.1, <3", "pytest-randomly >= 1.2.3, <2", ], + # conditional dependency syntax compatible with setuptools >= 18 + # https://hynek.me/articles/conditional-python-dependencies/ + # install 'enum34' backport on python < 3.4 + ":python_version < '3.4'": ['enum34 ~= 1.1.6'], }, cmdclass={ "release": release, diff --git a/tox.ini b/tox.ini index f4d147c..1c2a077 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,37}-cov,py{27,37}-cov-lxml,py{27,37}-cov-lxml-fs,coverage +envlist = py{27,37}-cov,py{27,37}-cov-lxml,coverage minversion = 2.9.1 skip_missing_interpreters = true @@ -9,7 +9,6 @@ setenv = COVERAGE_FILE={toxinidir}/.coverage.{envname} deps = -rrequirements.txt - fs: fs==2.1.1 lxml: -rextra_requirements.txt extras = testing commands = From 5131c1a0e6c31ea547a39e81d7367832b9d3e160 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 12:40:08 +0100 Subject: [PATCH 57/71] =?UTF-8?q?Bump=20version:=202.3.2=20=E2=86=92=202.4?= =?UTF-8?q?.0.dev0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/ufoLib/__init__.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index c46362e..52ad00b 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -64,7 +64,7 @@ "convertFontInfoValueForAttributeFromVersion2ToVersion1" ] -__version__ = "2.3.2" +__version__ = "2.4.0.dev0" # ---------- diff --git a/setup.cfg b/setup.cfg index f0c0ca8..76dae27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.3.2 +current_version = 2.4.0.dev0 commit = True tag = False tag_name = v{new_version} diff --git a/setup.py b/setup.py index 5ff29ac..a07888b 100755 --- a/setup.py +++ b/setup.py @@ -147,7 +147,7 @@ def run(self): setup_params = dict( name="ufoLib", - version="2.3.2", + version="2.4.0.dev0", description="A low-level UFO reader and writer.", author="Just van Rossum, Tal Leming, Erik van Blokland, others", author_email="info@robofab.com", From 66ce3d50b5d6e82673720d5fde25ff67b910677e Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 13:15:54 +0100 Subject: [PATCH 58/71] move test suite outside of Lib/ufoLib Fixes https://github.com/unified-font-object/ufoLib/issues/165 --- .coveragerc | 4 ---- setup.cfg | 1 + {Lib/ufoLib/test => tests}/__init__.py | 0 {Lib/ufoLib/test => tests}/testSupport.py | 0 {Lib/ufoLib/test => tests}/test_GLIF1.py | 2 +- {Lib/ufoLib/test => tests}/test_GLIF2.py | 2 +- {Lib/ufoLib/test => tests}/test_UFO1.py | 2 +- {Lib/ufoLib/test => tests}/test_UFO2.py | 2 +- {Lib/ufoLib/test => tests}/test_UFO3.py | 2 +- {Lib/ufoLib/test => tests}/test_UFOConversion.py | 2 +- {Lib/ufoLib/test => tests}/test_etree.py | 0 {Lib/ufoLib/test => tests}/test_filenames.py | 0 {Lib/ufoLib/test => tests}/test_glifLib.py | 2 +- {Lib/ufoLib/test => tests}/test_plistlib.py | 0 .../test => tests}/testdata/DemoFont.ufo/fontinfo.plist | 0 .../test => tests}/testdata/DemoFont.ufo/glyphs/A_.glif | 0 .../test => tests}/testdata/DemoFont.ufo/glyphs/B_.glif | 0 .../test => tests}/testdata/DemoFont.ufo/glyphs/F_.glif | 0 .../test => tests}/testdata/DemoFont.ufo/glyphs/F__A__B_.glif | 0 .../test => tests}/testdata/DemoFont.ufo/glyphs/G_.glif | 0 .../test => tests}/testdata/DemoFont.ufo/glyphs/O_.glif | 0 .../test => tests}/testdata/DemoFont.ufo/glyphs/R_.glif | 0 .../ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/a.glif | 0 .../testdata/DemoFont.ufo/glyphs/contents.plist | 0 .../testdata/DemoFont.ufo/glyphs/testglyph1.glif | 0 .../testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif | 0 {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/lib.plist | 0 .../test => tests}/testdata/DemoFont.ufo/metainfo.plist | 0 .../testdata/TestFont1 (UFO1).ufo/fontinfo.plist | 0 .../testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif | 0 .../testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif | 0 .../testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist | 0 .../test => tests}/testdata/TestFont1 (UFO1).ufo/groups.plist | 0 .../testdata/TestFont1 (UFO1).ufo/kerning.plist | 0 .../test => tests}/testdata/TestFont1 (UFO1).ufo/lib.plist | 0 .../testdata/TestFont1 (UFO1).ufo/metainfo.plist | 0 .../test => tests}/testdata/TestFont1 (UFO2).ufo/features.fea | 0 .../testdata/TestFont1 (UFO2).ufo/fontinfo.plist | 0 .../testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif | 0 .../testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif | 0 .../testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist | 0 .../test => tests}/testdata/TestFont1 (UFO2).ufo/groups.plist | 0 .../testdata/TestFont1 (UFO2).ufo/kerning.plist | 0 .../test => tests}/testdata/TestFont1 (UFO2).ufo/lib.plist | 0 .../testdata/TestFont1 (UFO2).ufo/metainfo.plist | 0 .../data/org.unifiedfontobject.directory/bar/lol.txt | 0 .../data/org.unifiedfontobject.directory/foo.txt | 0 .../UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt | 0 .../test => tests}/testdata/UFO3-Read Data.ufo/metainfo.plist | 0 {Lib/ufoLib/test => tests}/testdata/test.plist | 0 50 files changed, 8 insertions(+), 11 deletions(-) rename {Lib/ufoLib/test => tests}/__init__.py (100%) rename {Lib/ufoLib/test => tests}/testSupport.py (100%) rename {Lib/ufoLib/test => tests}/test_GLIF1.py (99%) rename {Lib/ufoLib/test => tests}/test_GLIF2.py (99%) rename {Lib/ufoLib/test => tests}/test_UFO1.py (98%) rename {Lib/ufoLib/test => tests}/test_UFO2.py (99%) rename {Lib/ufoLib/test => tests}/test_UFO3.py (99%) rename {Lib/ufoLib/test => tests}/test_UFOConversion.py (99%) rename {Lib/ufoLib/test => tests}/test_etree.py (100%) rename {Lib/ufoLib/test => tests}/test_filenames.py (100%) rename {Lib/ufoLib/test => tests}/test_glifLib.py (99%) rename {Lib/ufoLib/test => tests}/test_plistlib.py (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/fontinfo.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/A_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/B_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/F_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/F__A__B_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/G_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/O_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/R_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/a.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/contents.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/testglyph1.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/lib.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/DemoFont.ufo/metainfo.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/fontinfo.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/groups.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/kerning.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/lib.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO1).ufo/metainfo.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/features.fea (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/fontinfo.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/groups.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/kerning.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/lib.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/TestFont1 (UFO2).ufo/metainfo.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt (100%) rename {Lib/ufoLib/test => tests}/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt (100%) rename {Lib/ufoLib/test => tests}/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt (100%) rename {Lib/ufoLib/test => tests}/testdata/UFO3-Read Data.ufo/metainfo.plist (100%) rename {Lib/ufoLib/test => tests}/testdata/test.plist (100%) diff --git a/.coveragerc b/.coveragerc index f159c20..3a1018c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,10 +3,6 @@ # See: http://coverage.readthedocs.io/en/coverage-4.5.1/branch.html branch = True -# Don't run codecov on tests -omit = - */test/* - # list of directories or packages to measure source = ufoLib diff --git a/setup.cfg b/setup.cfg index 76dae27..52567ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ license_file = LICENSE.txt minversion = 3.0.2 testpaths = ufoLib + tests/ doctest_optionflags = ALLOW_UNICODE ALLOW_BYTES addopts = -r a diff --git a/Lib/ufoLib/test/__init__.py b/tests/__init__.py similarity index 100% rename from Lib/ufoLib/test/__init__.py rename to tests/__init__.py diff --git a/Lib/ufoLib/test/testSupport.py b/tests/testSupport.py similarity index 100% rename from Lib/ufoLib/test/testSupport.py rename to tests/testSupport.py diff --git a/Lib/ufoLib/test/test_GLIF1.py b/tests/test_GLIF1.py similarity index 99% rename from Lib/ufoLib/test/test_GLIF1.py rename to tests/test_GLIF1.py index ecd1783..4db1b41 100644 --- a/Lib/ufoLib/test/test_GLIF1.py +++ b/tests/test_GLIF1.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals import unittest from ufoLib.glifLib import GlifLibError, readGlyphFromString, writeGlyphToString -from ufoLib.test.testSupport import Glyph, stripText +from .testSupport import Glyph, stripText from itertools import islice try: diff --git a/Lib/ufoLib/test/test_GLIF2.py b/tests/test_GLIF2.py similarity index 99% rename from Lib/ufoLib/test/test_GLIF2.py rename to tests/test_GLIF2.py index 87ffb53..565f1bd 100644 --- a/Lib/ufoLib/test/test_GLIF2.py +++ b/tests/test_GLIF2.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals import unittest from ufoLib.glifLib import GlifLibError, readGlyphFromString, writeGlyphToString -from ufoLib.test.testSupport import Glyph, stripText +from .testSupport import Glyph, stripText from itertools import islice try: diff --git a/Lib/ufoLib/test/test_UFO1.py b/tests/test_UFO1.py similarity index 98% rename from Lib/ufoLib/test/test_UFO1.py rename to tests/test_UFO1.py index cf1cbbc..f4a5dc3 100644 --- a/Lib/ufoLib/test/test_UFO1.py +++ b/tests/test_UFO1.py @@ -7,7 +7,7 @@ from io import open from ufoLib import UFOReader, UFOWriter, UFOLibError from ufoLib import plistlib -from ufoLib.test.testSupport import fontInfoVersion1, fontInfoVersion2 +from .testSupport import fontInfoVersion1, fontInfoVersion2 class TestInfoObject(object): pass diff --git a/Lib/ufoLib/test/test_UFO2.py b/tests/test_UFO2.py similarity index 99% rename from Lib/ufoLib/test/test_UFO2.py rename to tests/test_UFO2.py index 480390a..1c4439b 100644 --- a/Lib/ufoLib/test/test_UFO2.py +++ b/tests/test_UFO2.py @@ -7,7 +7,7 @@ from io import open from ufoLib import UFOReader, UFOWriter, UFOLibError from ufoLib import plistlib -from ufoLib.test.testSupport import fontInfoVersion2 +from .testSupport import fontInfoVersion2 class TestInfoObject(object): pass diff --git a/Lib/ufoLib/test/test_UFO3.py b/tests/test_UFO3.py similarity index 99% rename from Lib/ufoLib/test/test_UFO3.py rename to tests/test_UFO3.py index c47157c..916e8ff 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/tests/test_UFO3.py @@ -9,7 +9,7 @@ from ufoLib import UFOReader, UFOWriter, UFOLibError from ufoLib.glifLib import GlifLibError from ufoLib import plistlib -from ufoLib.test.testSupport import fontInfoVersion3 +from .testSupport import fontInfoVersion3 class TestInfoObject(object): pass diff --git a/Lib/ufoLib/test/test_UFOConversion.py b/tests/test_UFOConversion.py similarity index 99% rename from Lib/ufoLib/test/test_UFOConversion.py rename to tests/test_UFOConversion.py index 7c321cc..c569a96 100644 --- a/Lib/ufoLib/test/test_UFOConversion.py +++ b/tests/test_UFOConversion.py @@ -7,7 +7,7 @@ from io import open from ufoLib import UFOReader, UFOWriter from ufoLib import plistlib -from ufoLib.test.testSupport import expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion +from .testSupport import expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion # the format version 1 lib.plist contains some data diff --git a/Lib/ufoLib/test/test_etree.py b/tests/test_etree.py similarity index 100% rename from Lib/ufoLib/test/test_etree.py rename to tests/test_etree.py diff --git a/Lib/ufoLib/test/test_filenames.py b/tests/test_filenames.py similarity index 100% rename from Lib/ufoLib/test/test_filenames.py rename to tests/test_filenames.py diff --git a/Lib/ufoLib/test/test_glifLib.py b/tests/test_glifLib.py similarity index 99% rename from Lib/ufoLib/test/test_glifLib.py rename to tests/test_glifLib.py index 7835920..a107e4f 100644 --- a/Lib/ufoLib/test/test_glifLib.py +++ b/tests/test_glifLib.py @@ -4,7 +4,7 @@ import shutil import unittest from io import open -from ufoLib.test.testSupport import getDemoFontGlyphSetPath +from .testSupport import getDemoFontGlyphSetPath from ufoLib.glifLib import ( GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString, _XML_DECLARATION, diff --git a/Lib/ufoLib/test/test_plistlib.py b/tests/test_plistlib.py similarity index 100% rename from Lib/ufoLib/test/test_plistlib.py rename to tests/test_plistlib.py diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/fontinfo.plist b/tests/testdata/DemoFont.ufo/fontinfo.plist similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/fontinfo.plist rename to tests/testdata/DemoFont.ufo/fontinfo.plist diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/A_.glif b/tests/testdata/DemoFont.ufo/glyphs/A_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/A_.glif rename to tests/testdata/DemoFont.ufo/glyphs/A_.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/B_.glif b/tests/testdata/DemoFont.ufo/glyphs/B_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/B_.glif rename to tests/testdata/DemoFont.ufo/glyphs/B_.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F_.glif b/tests/testdata/DemoFont.ufo/glyphs/F_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F_.glif rename to tests/testdata/DemoFont.ufo/glyphs/F_.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F__A__B_.glif b/tests/testdata/DemoFont.ufo/glyphs/F__A__B_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F__A__B_.glif rename to tests/testdata/DemoFont.ufo/glyphs/F__A__B_.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/G_.glif b/tests/testdata/DemoFont.ufo/glyphs/G_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/G_.glif rename to tests/testdata/DemoFont.ufo/glyphs/G_.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/O_.glif b/tests/testdata/DemoFont.ufo/glyphs/O_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/O_.glif rename to tests/testdata/DemoFont.ufo/glyphs/O_.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/R_.glif b/tests/testdata/DemoFont.ufo/glyphs/R_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/R_.glif rename to tests/testdata/DemoFont.ufo/glyphs/R_.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a.glif b/tests/testdata/DemoFont.ufo/glyphs/a.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a.glif rename to tests/testdata/DemoFont.ufo/glyphs/a.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/contents.plist b/tests/testdata/DemoFont.ufo/glyphs/contents.plist similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/contents.plist rename to tests/testdata/DemoFont.ufo/glyphs/contents.plist diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.glif b/tests/testdata/DemoFont.ufo/glyphs/testglyph1.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.glif rename to tests/testdata/DemoFont.ufo/glyphs/testglyph1.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif b/tests/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif rename to tests/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/lib.plist b/tests/testdata/DemoFont.ufo/lib.plist similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/lib.plist rename to tests/testdata/DemoFont.ufo/lib.plist diff --git a/Lib/ufoLib/test/testdata/DemoFont.ufo/metainfo.plist b/tests/testdata/DemoFont.ufo/metainfo.plist similarity index 100% rename from Lib/ufoLib/test/testdata/DemoFont.ufo/metainfo.plist rename to tests/testdata/DemoFont.ufo/metainfo.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/fontinfo.plist b/tests/testdata/TestFont1 (UFO1).ufo/fontinfo.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/fontinfo.plist rename to tests/testdata/TestFont1 (UFO1).ufo/fontinfo.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif b/tests/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif rename to tests/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif b/tests/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif rename to tests/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist b/tests/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist rename to tests/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/groups.plist b/tests/testdata/TestFont1 (UFO1).ufo/groups.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/groups.plist rename to tests/testdata/TestFont1 (UFO1).ufo/groups.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/kerning.plist b/tests/testdata/TestFont1 (UFO1).ufo/kerning.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/kerning.plist rename to tests/testdata/TestFont1 (UFO1).ufo/kerning.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/lib.plist b/tests/testdata/TestFont1 (UFO1).ufo/lib.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/lib.plist rename to tests/testdata/TestFont1 (UFO1).ufo/lib.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/metainfo.plist b/tests/testdata/TestFont1 (UFO1).ufo/metainfo.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/metainfo.plist rename to tests/testdata/TestFont1 (UFO1).ufo/metainfo.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/features.fea b/tests/testdata/TestFont1 (UFO2).ufo/features.fea similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/features.fea rename to tests/testdata/TestFont1 (UFO2).ufo/features.fea diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/fontinfo.plist b/tests/testdata/TestFont1 (UFO2).ufo/fontinfo.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/fontinfo.plist rename to tests/testdata/TestFont1 (UFO2).ufo/fontinfo.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif b/tests/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif rename to tests/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif b/tests/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif rename to tests/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist b/tests/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist rename to tests/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/groups.plist b/tests/testdata/TestFont1 (UFO2).ufo/groups.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/groups.plist rename to tests/testdata/TestFont1 (UFO2).ufo/groups.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/kerning.plist b/tests/testdata/TestFont1 (UFO2).ufo/kerning.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/kerning.plist rename to tests/testdata/TestFont1 (UFO2).ufo/kerning.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/lib.plist b/tests/testdata/TestFont1 (UFO2).ufo/lib.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/lib.plist rename to tests/testdata/TestFont1 (UFO2).ufo/lib.plist diff --git a/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/metainfo.plist b/tests/testdata/TestFont1 (UFO2).ufo/metainfo.plist similarity index 100% rename from Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/metainfo.plist rename to tests/testdata/TestFont1 (UFO2).ufo/metainfo.plist diff --git a/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt b/tests/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt similarity index 100% rename from Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt rename to tests/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt diff --git a/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt b/tests/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt similarity index 100% rename from Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt rename to tests/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt diff --git a/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt b/tests/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt similarity index 100% rename from Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt rename to tests/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt diff --git a/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/metainfo.plist b/tests/testdata/UFO3-Read Data.ufo/metainfo.plist similarity index 100% rename from Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/metainfo.plist rename to tests/testdata/UFO3-Read Data.ufo/metainfo.plist diff --git a/Lib/ufoLib/test/testdata/test.plist b/tests/testdata/test.plist similarity index 100% rename from Lib/ufoLib/test/testdata/test.plist rename to tests/testdata/test.plist From fa68a5dc94f3cb895f6c3eb6d2c222fe6bc1a7aa Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 13:18:43 +0100 Subject: [PATCH 59/71] MANIFEST.in: add tests modules and data files to sdist --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index f00def8..65f2e8c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include README.md notes.txt LICENSE.txt include Documentation/Makefile recursive-include Documentation *.py *.rst -recursive-include Lib/ufoLib/test/testdata *.plist *.glif *.fea *.txt +recursive-include tests *.py +recursive-include tests/testdata *.plist *.glif *.fea *.txt include requirements.txt extra_requirements.txt .coveragerc tox.ini From c40bfd3a0d8a1e13cf27358dbe1563a590f121a0 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 13:31:56 +0100 Subject: [PATCH 60/71] setup.cfg: ignore deprecation warnings that we ourselves raise in tests --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.cfg b/setup.cfg index 52567ce..bbe0beb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,10 @@ testpaths = ufoLib tests/ doctest_optionflags = ALLOW_UNICODE ALLOW_BYTES +filterwarnings = + ignore:readPlist:DeprecationWarning:test_plistlib + ignore:writePlist:DeprecationWarning:test_plistlib + ignore:some_function:DeprecationWarning:utils addopts = -r a --doctest-modules From 3de05dde2f9865952e8b0d62fc71c176fe8eee83 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 13:44:34 +0100 Subject: [PATCH 61/71] remove runTests helper, just use pytest as runner from now on --- tests/testSupport.py | 30 ------------------------------ tests/test_GLIF1.py | 5 ----- tests/test_GLIF2.py | 5 ----- tests/test_UFO1.py | 5 ----- tests/test_UFO2.py | 5 ----- tests/test_UFO3.py | 4 ---- tests/test_UFOConversion.py | 6 ------ tests/test_glifLib.py | 8 -------- 8 files changed, 68 deletions(-) diff --git a/tests/testSupport.py b/tests/testSupport.py index 7c04ae5..4b4a541 100755 --- a/tests/testSupport.py +++ b/tests/testSupport.py @@ -1,9 +1,7 @@ """Miscellaneous helpers for our test suite.""" from __future__ import absolute_import, unicode_literals -import sys import os -import unittest try: basestring @@ -21,34 +19,6 @@ def getDemoFontGlyphSetPath(): return os.path.join(getDemoFontPath(), "glyphs") -def _gatherTestCasesFromCallerByMagic(): - # UGLY magic: fetch TestClass subclasses from the globals of our - # caller's caller. - frame = sys._getframe(2) - return _gatherTestCasesFromDict(frame.f_globals) - - -def _gatherTestCasesFromDict(d): - testCases = [] - for ob in list(d.values()): - if isinstance(ob, type) and issubclass(ob, unittest.TestCase): - testCases.append(ob) - return testCases - - -def runTests(testCases=None, verbosity=1): - """Run a series of tests.""" - if testCases is None: - testCases = _gatherTestCasesFromCallerByMagic() - loader = unittest.TestLoader() - suites = [] - for testCase in testCases: - suites.append(loader.loadTestsFromTestCase(testCase)) - - testRunner = unittest.TextTestRunner(verbosity=verbosity) - testSuite = unittest.TestSuite(suites) - testRunner.run(testSuite) - # GLIF test tools class Glyph(object): diff --git a/tests/test_GLIF1.py b/tests/test_GLIF1.py index 4db1b41..b4371c9 100644 --- a/tests/test_GLIF1.py +++ b/tests/test_GLIF1.py @@ -1335,8 +1335,3 @@ def testOpenContourLooseOffCurves_illegal(self): pointPen.endPath() """ self.assertRaises(GlifLibError, self.pyToGLIF, py) - - -if __name__ == "__main__": - from ufoLib.test.testSupport import runTests - runTests() diff --git a/tests/test_GLIF2.py b/tests/test_GLIF2.py index 565f1bd..3db4931 100644 --- a/tests/test_GLIF2.py +++ b/tests/test_GLIF2.py @@ -2370,8 +2370,3 @@ def testIdentifierConflict_anchor_anchor(self): """ self.assertRaises(GlifLibError, self.pyToGLIF, py) self.assertRaises(GlifLibError, self.glifToPy, glif) - - -if __name__ == "__main__": - from ufoLib.test.testSupport import runTests - runTests() diff --git a/tests/test_UFO1.py b/tests/test_UFO1.py index f4a5dc3..2fb688c 100644 --- a/tests/test_UFO1.py +++ b/tests/test_UFO1.py @@ -150,8 +150,3 @@ def testWidthNameConversion(self): writer.writeInfo(infoObject) writtenData = self.readPlist() self.assertEqual(writtenData["widthName"], old) - - -if __name__ == "__main__": - from ufoLib.test.testSupport import runTests - runTests() diff --git a/tests/test_UFO2.py b/tests/test_UFO2.py index 1c4439b..c44d657 100644 --- a/tests/test_UFO2.py +++ b/tests/test_UFO2.py @@ -1412,8 +1412,3 @@ def testPostscriptWrite(self): infoObject.macintoshFONDName = 123 writer = UFOWriter(self.dstDir, formatVersion=2) self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject) - - -if __name__ == "__main__": - from ufoLib.test.testSupport import runTests - runTests() diff --git a/tests/test_UFO3.py b/tests/test_UFO3.py index 916e8ff..f59b136 100644 --- a/tests/test_UFO3.py +++ b/tests/test_UFO3.py @@ -4684,7 +4684,3 @@ def testColor(self): info.color = "0, 0, 0, 2" glyphSet = self.makeGlyphSet() self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info) - -if __name__ == "__main__": - from ufoLib.test.testSupport import runTests - runTests() diff --git a/tests/test_UFOConversion.py b/tests/test_UFOConversion.py index c569a96..8342373 100644 --- a/tests/test_UFOConversion.py +++ b/tests/test_UFOConversion.py @@ -345,9 +345,3 @@ def testWrite(self): writtenKerning = plistlib.load(f) self.assertEqual(writtenKerning, self.expectedWrittenKerning) self.tearDownUFO() - - - -if __name__ == "__main__": - from ufoLib.test.testSupport import runTests - runTests() diff --git a/tests/test_glifLib.py b/tests/test_glifLib.py index a107e4f..499082f 100644 --- a/tests/test_glifLib.py +++ b/tests/test_glifLib.py @@ -167,11 +167,3 @@ def testRoundTrip(self): def testXmlDeclaration(self): s = writeGlyphToString("a", _Glyph()) self.assertTrue(s.startswith(_XML_DECLARATION.decode("utf-8"))) - - -if __name__ == "__main__": - from ufoLib.test.testSupport import runTests - import sys - if len(sys.argv) > 1 and os.path.isdir(sys.argv[-1]): - GLYPHSETDIR = sys.argv.pop() - runTests() From 20c3c1b227de34c4c28e1d3b25c85fd3647304ff Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 14:03:49 +0100 Subject: [PATCH 62/71] move datetimeAsTimestamp func to utils.py module --- Lib/ufoLib/utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Lib/ufoLib/utils.py b/Lib/ufoLib/utils.py index 7741e29..e8dbc89 100644 --- a/Lib/ufoLib/utils.py +++ b/Lib/ufoLib/utils.py @@ -4,6 +4,36 @@ from __future__ import absolute_import, unicode_literals import warnings import functools +from datetime import datetime + + +if hasattr(datetime, "timestamp"): # python >= 3.3 + + def datetimeAsTimestamp(dt): + return dt.timestamp() + +else: + from datetime import tzinfo, timedelta + + ZERO = timedelta(0) + + class UTC(tzinfo): + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + + utc = UTC() + + EPOCH = datetime.fromtimestamp(0, tz=utc) + + def datetimeAsTimestamp(dt): + return (dt - EPOCH).total_seconds() def deprecated(msg=""): From 6ec8b72b6a431f75fdedd1bfa4a29fdf403d1547 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 14:13:28 +0100 Subject: [PATCH 63/71] remove _common module, put shared methods in ufoLib.__init__ and moved the import ufoLib.glifLib outside from global module scope to avoid circular import issues --- Lib/ufoLib/__init__.py | 136 +++++++++++++++++++++++++++++++++++------ Lib/ufoLib/_common.py | 127 -------------------------------------- Lib/ufoLib/glifLib.py | 6 +- 3 files changed, 122 insertions(+), 147 deletions(-) delete mode 100644 Lib/ufoLib/_common.py diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 52ad00b..52855eb 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -13,11 +13,12 @@ import fs.tempfs import fs.tools from fontTools.misc.py23 import basestring, unicode, tounicode -from ufoLib.glifLib import GlyphSet +from ufoLib import plistlib from ufoLib.validators import * from ufoLib.filenames import userNameToFileName from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning from ufoLib.errors import UFOLibError +from ufoLib.utils import datetimeAsTimestamp """ A library for importing .ufo files and their descendants. @@ -67,9 +68,9 @@ __version__ = "2.4.0.dev0" -# ---------- -# File Names -# ---------- +# --------- +# Constants +# --------- DEFAULT_GLYPHS_DIRNAME = "glyphs" DATA_DIRNAME = "data" @@ -93,6 +94,104 @@ class UFOFileStructure(enum.Enum): PACKAGE = "package" +# -------------- +# Shared methods +# -------------- + + +def _getFileModificationTime(self, path): + """ + Returns the modification time for the file at the given path, as a + floating point number giving the number of seconds since the epoch. + The path must be relative to the UFO path. + Returns None if the file does not exist. + """ + try: + dt = self.fs.getinfo(path, namespaces=["details"]).modified + except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound): + return None + else: + return datetimeAsTimestamp(dt) + + +def _readBytesFromPath(self, path): + """ + Returns the bytes in the file at the given path. + The path must be relative to the UFO's filesystem root. + Returns None if the file does not exist. + """ + try: + return self.fs.getbytes(path) + except fs.errors.ResourceNotFound: + return None + + +def _getPlist(self, fileName, default=None): + """ + Read a property list relative to the UFO filesystem's root. + Raises UFOLibError if the file is missing and default is None, + otherwise default is returned. + + The errors that could be raised during the reading of a plist are + unpredictable and/or too large to list, so, a blind try: except: + is done. If an exception occurs, a UFOLibError will be raised. + """ + try: + with self.fs.open(fileName, "rb") as f: + return plistlib.load(f) + except fs.errors.ResourceNotFound: + if default is None: + raise UFOLibError( + "'%s' is missing on %s. This file is required" + % (fileName, self.fs) + ) + else: + return default + except Exception as e: + # TODO(anthrotype): try to narrow this down a little + raise UFOLibError( + "'%s' could not be read on %s: %s" % (fileName, self.fs, e) + ) + + +def _writePlist(self, fileName, obj): + """ + Write a property list to a file relative to the UFO filesystem's root. + + Do this sort of atomically, making it harder to corrupt existing files, + for example when plistlib encounters an error halfway during write. + This also checks to see if text matches the text that is already in the + file at path. If so, the file is not rewritten so that the modification + date is preserved. + + The errors that could be raised during the writing of a plist are + unpredictable and/or too large to list, so, a blind try: except: is done. + If an exception occurs, a UFOLibError will be raised. + """ + if self._havePreviousFile: + try: + data = plistlib.dumps(obj) + except Exception as e: + raise UFOLibError( + "'%s' could not be written on %s because " + "the data is not properly formatted: %s" + % (fileName, self.fs, e) + ) + if self.fs.exists(fileName) and data == self.fs.getbytes(fileName): + return + self.fs.setbytes(fileName, data) + else: + with self.fs.openbin(fileName, mode="w") as fp: + try: + plistlib.dump(obj, fp) + except Exception as e: + raise UFOLibError( + "'%s' could not be written on %s because " + "the data is not properly formatted: %s" + % (fileName, self.fs, e) + ) + + def _sniffFileStructure(ufo_path): """Return UFOFileStructure.ZIP if the UFO at path 'ufo_path' (basestring) is a zip file, else return UFOFileStructure.PACKAGE if 'ufo_path' is a @@ -111,6 +210,7 @@ def _sniffFileStructure(ufo_path): else: raise UFOLibError("No such file or directory: '%s'" % ufo_path) + # ---------- # UFO Reader # ---------- @@ -262,12 +362,9 @@ def _upConvertKerning(self, validate): # support methods - from ufoLib._common import ( - _getPlist, - getFileModificationTime, - readBytesFromPath, - ) - + _getPlist = _getPlist + getFileModificationTime = _getFileModificationTime + readBytesFromPath = _readBytesFromPath sniffFileStructure = staticmethod(_sniffFileStructure) def getReadFileForPath(self, path, encoding=None): @@ -550,6 +647,8 @@ def getGlyphSet(self, layerName=None, validateRead=None, validateWrite=None): ``validateWrte`` will validate the written data, by default it is set to the class's validate value, can be overridden. """ + from ufoLib.glifLib import GlyphSet + if validateRead is None: validateRead = self._validate if validateWrite is None: @@ -877,13 +976,10 @@ def _get_fileStructure(self): # support methods for file system interaction - from ufoLib._common import ( - _getPlist, - _writePlist, - readBytesFromPath, - getFileModificationTime, - ) - + _getPlist = _getPlist + _writePlist = _writePlist + readBytesFromPath = _readBytesFromPath + getFileModificationTime = _getFileModificationTime sniffFileStructure = staticmethod(_sniffFileStructure) def copyFromReader(self, reader, sourcePath, destPath): @@ -1308,6 +1404,8 @@ def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc raise AssertionError(self.formatVersion) def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): + from ufoLib.glifLib import GlyphSet + glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True), return GlyphSet( glyphSubFS, @@ -1318,6 +1416,8 @@ def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFil ) def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFileNameFunc=None): + from ufoLib.glifLib import GlyphSet + glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True) return GlyphSet( glyphSubFS, @@ -1328,6 +1428,8 @@ def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFil ) def _getGlyphSetFormatVersion3(self, validateRead, validateWrite, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None): + from ufoLib.glifLib import GlyphSet + # if the default flag is on, make sure that the default in the file # matches the default being written. also make sure that this layer # name is not already linked to a non-default layer. diff --git a/Lib/ufoLib/_common.py b/Lib/ufoLib/_common.py deleted file mode 100644 index 8802aef..0000000 --- a/Lib/ufoLib/_common.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Private support methods shared by UFOReader, UFOWriter or glifLib.GlyphSet. -""" -from __future__ import absolute_import, unicode_literals -from ufoLib import plistlib -from ufoLib.errors import UFOLibError -import fs.errors -from datetime import datetime - - -if hasattr(datetime, "timestamp"): # python >= 3.3 - - def _timestamp(dt): - return dt.timestamp() - -else: - from datetime import tzinfo, timedelta - - ZERO = timedelta(0) - - class UTC(tzinfo): - - def utcoffset(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return ZERO - - utc = UTC() - - EPOCH = datetime.fromtimestamp(0, tz=utc) - - def _timestamp(dt): - return (dt - EPOCH).total_seconds() - - -def getFileModificationTime(self, path): - """ - Returns the modification time for the file at the given path, as a - floating point number giving the number of seconds since the epoch. - The path must be relative to the UFO path. - Returns None if the file does not exist. - """ - try: - dt = self.fs.getinfo(path, namespaces=["details"]).modified - except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound): - return None - else: - return _timestamp(dt) - - -def readBytesFromPath(self, path): - """ - Returns the bytes in the file at the given path. - The path must be relative to the UFO's filesystem root. - Returns None if the file does not exist. - """ - try: - return self.fs.getbytes(path) - except fs.errors.ResourceNotFound: - return None - - -def _getPlist(self, fileName, default=None): - """ - Read a property list relative to the UFO filesystem's root. - Raises UFOLibError if the file is missing and default is None, - otherwise default is returned. - - The errors that could be raised during the reading of a plist are - unpredictable and/or too large to list, so, a blind try: except: - is done. If an exception occurs, a UFOLibError will be raised. - """ - try: - with self.fs.open(fileName, "rb") as f: - return plistlib.load(f) - except fs.errors.ResourceNotFound: - if default is None: - raise UFOLibError( - "'%s' is missing on %s. This file is required" - % (fileName, self.fs) - ) - else: - return default - except Exception as e: - # TODO(anthrotype): try to narrow this down a little - raise UFOLibError( - "'%s' could not be read on %s: %s" % (fileName, self.fs, e) - ) - - -_msg = ( - "'%s' could not be written on %s because " - "the data is not properly formatted: %s" -) - - -def _writePlist(self, fileName, obj): - """ - Write a property list to a file relative to the UFO filesystem's root. - - Do this sort of atomically, making it harder to corrupt existing files, - for example when plistlib encounters an error halfway during write. - This also checks to see if text matches the text that is already in the - file at path. If so, the file is not rewritten so that the modification - date is preserved. - - The errors that could be raised during the writing of a plist are - unpredictable and/or too large to list, so, a blind try: except: is done. - If an exception occurs, a UFOLibError will be raised. - """ - if self._havePreviousFile: - try: - data = plistlib.dumps(obj) - except Exception as e: - raise UFOLibError(_msg % (fileName, self.fs, e)) - if self.fs.exists(fileName) and data == self.fs.getbytes(fileName): - return - self.fs.setbytes(fileName, data) - else: - with self.fs.openbin(fileName, mode="w") as fp: - try: - plistlib.dump(obj, fp) - except Exception as e: - raise UFOLibError(_msg % (fileName, self.fs, e)) diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index bae6552..82b8782 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -170,7 +170,7 @@ def __init__( self.rebuildContents() # here we reuse the same methods from UFOReader/UFOWriter - from ufoLib._common import _getPlist, _writePlist, getFileModificationTime + from ufoLib import _getPlist, _writePlist, _getFileModificationTime def rebuildContents(self, validateRead=None): """ @@ -299,12 +299,12 @@ def getGLIF(self, glyphName): fileName = self.contents.get(glyphName) if glyphName not in self._glifCache or ( fileName is not None - and self.getFileModificationTime(fileName) != self._glifCache[glyphName][1] + and self._getFileModificationTime(fileName) != self._glifCache[glyphName][1] ): if fileName is None or not self.fs.exists(fileName): raise KeyError(glyphName) data = self.fs.getbytes(fileName) - self._glifCache[glyphName] = (data, self.getFileModificationTime(fileName)) + self._glifCache[glyphName] = (data, self._getFileModificationTime(fileName)) return self._glifCache[glyphName][0] def getGLIFModificationTime(self, glyphName): From 7ef045c88f71ef50608cd27d4b1daa8ad2d13e8c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 14:20:52 +0100 Subject: [PATCH 64/71] minor [skip ci] --- Lib/ufoLib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 52855eb..260331a 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -888,7 +888,7 @@ def __init__( path = filesystem.getsyspath("/") except fs.errors.NoSysPath: # network or in-memory FS may not map to the local one - path = str(filesystem) + path = unicode(filesystem) else: path = path.rstrip("/") # if passed an FS object, always default to 'package' structure From 032aa7b6ace6d76237f5b65fa4c98e3bbafb0add Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 15:12:19 +0100 Subject: [PATCH 65/71] minor: diff noise, docstrings, etc. [skip ci] --- Lib/ufoLib/__init__.py | 4 +++- tests/test_glifLib.py | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 260331a..f4f6eb6 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -95,7 +95,7 @@ class UFOFileStructure(enum.Enum): # -------------- -# Shared methods +# Shared Methods # -------------- @@ -1052,6 +1052,8 @@ def removePath(self, path, force=False, removeEmptyParents=True): must be relative to the UFO. Raises UFOLibError if the path doesn't exist. If force=True, ignore non-existent paths. + If the directory where 'path' is located becomes empty, it will + be automatically removed, unless 'removeEmptyParents' is False. """ try: self.fs.remove(path) diff --git a/tests/test_glifLib.py b/tests/test_glifLib.py index 499082f..3120534 100644 --- a/tests/test_glifLib.py +++ b/tests/test_glifLib.py @@ -87,12 +87,7 @@ def testCustomFileNamingScheme(self): def myGlyphNameToFileName(glyphName, glyphSet): return "prefix" + glyphNameToFileName(glyphName, glyphSet) src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True) - dst = GlyphSet( - self.dstDir, - glyphNameToFileNameFunc=myGlyphNameToFileName, - validateRead=True, - validateWrite=True, - ) + dst = GlyphSet(self.dstDir, myGlyphNameToFileName, validateRead=True, validateWrite=True) for glyphName in src.keys(): g = src[glyphName] g.drawPoints(None) # load attrs From 7321c2d91fd1386ce2989111e5e8665066fd1dce Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 15:42:58 +0100 Subject: [PATCH 66/71] tests: add UFO3 test font --- MANIFEST.in | 2 +- .../data/com.github.fonttools.ttx/CUST.ttx | 10 + .../TestFont1 (UFO3).ufo/fontinfo.plist | 338 ++++++++++++++++++ .../TestFont1 (UFO3).ufo/glyphs/_notdef.glif | 18 + .../TestFont1 (UFO3).ufo/glyphs/a.glif | 12 + .../TestFont1 (UFO3).ufo/glyphs/b.glif | 13 + .../TestFont1 (UFO3).ufo/glyphs/c.glif | 16 + .../glyphs/contents.plist | 34 ++ .../TestFont1 (UFO3).ufo/glyphs/d.glif | 21 ++ .../TestFont1 (UFO3).ufo/glyphs/e.glif | 21 ++ .../TestFont1 (UFO3).ufo/glyphs/f.glif | 21 ++ .../TestFont1 (UFO3).ufo/glyphs/g.glif | 8 + .../TestFont1 (UFO3).ufo/glyphs/h.glif | 9 + .../TestFont1 (UFO3).ufo/glyphs/i.glif | 17 + .../TestFont1 (UFO3).ufo/glyphs/j.glif | 17 + .../TestFont1 (UFO3).ufo/glyphs/k.glif | 9 + .../TestFont1 (UFO3).ufo/glyphs/l.glif | 9 + .../TestFont1 (UFO3).ufo/glyphs/space.glif | 7 + .../TestFont1 (UFO3).ufo/kerning.plist | 20 ++ .../TestFont1 (UFO3).ufo/layercontents.plist | 10 + tests/testdata/TestFont1 (UFO3).ufo/lib.plist | 25 ++ .../TestFont1 (UFO3).ufo/metainfo.plist | 10 + 22 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/fontinfo.plist create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/kerning.plist create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/layercontents.plist create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/lib.plist create mode 100644 tests/testdata/TestFont1 (UFO3).ufo/metainfo.plist diff --git a/MANIFEST.in b/MANIFEST.in index 65f2e8c..59b65bd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,6 @@ include Documentation/Makefile recursive-include Documentation *.py *.rst recursive-include tests *.py -recursive-include tests/testdata *.plist *.glif *.fea *.txt +recursive-include tests/testdata *.plist *.glif *.fea *.txt *.ttx include requirements.txt extra_requirements.txt .coveragerc tox.ini diff --git a/tests/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx b/tests/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx new file mode 100644 index 0000000..51847fd --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx @@ -0,0 +1,10 @@ + + + + + + 0001beef + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/fontinfo.plist b/tests/testdata/TestFont1 (UFO3).ufo/fontinfo.plist new file mode 100644 index 0000000..78dd88e --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/fontinfo.plist @@ -0,0 +1,338 @@ + + + + + ascender + 750 + capHeight + 750 + copyright + Copyright © Some Foundry. + descender + -250 + familyName + Some Font (Family Name) + guidelines + + + x + 250 + + + x + -20 + + + x + 30 + + + y + 500 + + + y + -200 + + + y + 700 + + + angle + 135 + x + 0 + y + 0 + + + angle + 45 + x + 0 + y + 700 + + + angle + 135 + x + 20 + y + 0 + + + italicAngle + -12.5 + macintoshFONDFamilyID + 15000 + macintoshFONDName + SomeFont Regular (FOND Name) + note + A note. + openTypeGaspRangeRecords + + + rangeGaspBehavior + + 1 + 3 + + rangeMaxPPEM + 7 + + + rangeGaspBehavior + + 0 + 1 + 2 + 3 + + rangeMaxPPEM + 65535 + + + openTypeHeadCreated + 2000/01/01 00:00:00 + openTypeHeadFlags + + 0 + 1 + + openTypeHeadLowestRecPPEM + 10 + openTypeHheaAscender + 750 + openTypeHheaCaretOffset + 0 + openTypeHheaCaretSlopeRise + 1 + openTypeHheaCaretSlopeRun + 0 + openTypeHheaDescender + -250 + openTypeHheaLineGap + 200 + openTypeNameCompatibleFullName + Some Font Regular (Compatible Full Name) + openTypeNameDescription + Some Font by Some Designer for Some Foundry. + openTypeNameDesigner + Some Designer + openTypeNameDesignerURL + http://somedesigner.com + openTypeNameLicense + License info for Some Foundry. + openTypeNameLicenseURL + http://somefoundry.com/license + openTypeNameManufacturer + Some Foundry + openTypeNameManufacturerURL + http://somefoundry.com + openTypeNamePreferredFamilyName + Some Font (Preferred Family Name) + openTypeNamePreferredSubfamilyName + Regular (Preferred Subfamily Name) + openTypeNameRecords + + + encodingID + 0 + languageID + 0 + nameID + 3 + platformID + 1 + string + Unique Font Identifier + + + encodingID + 1 + languageID + 1033 + nameID + 8 + platformID + 3 + string + Some Foundry (Manufacturer Name) + + + openTypeNameSampleText + Sample Text for Some Font. + openTypeNameUniqueID + OpenType name Table Unique ID + openTypeNameVersion + OpenType name Table Version + openTypeNameWWSFamilyName + Some Font (WWS Family Name) + openTypeNameWWSSubfamilyName + Regular (WWS Subfamily Name) + openTypeOS2CodePageRanges + + 0 + 1 + + openTypeOS2FamilyClass + + 1 + 1 + + openTypeOS2Panose + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + openTypeOS2Selection + + 3 + + openTypeOS2StrikeoutPosition + 300 + openTypeOS2StrikeoutSize + 20 + openTypeOS2SubscriptXOffset + 0 + openTypeOS2SubscriptXSize + 200 + openTypeOS2SubscriptYOffset + -100 + openTypeOS2SubscriptYSize + 400 + openTypeOS2SuperscriptXOffset + 0 + openTypeOS2SuperscriptXSize + 200 + openTypeOS2SuperscriptYOffset + 200 + openTypeOS2SuperscriptYSize + 400 + openTypeOS2Type + + openTypeOS2TypoAscender + 750 + openTypeOS2TypoDescender + -250 + openTypeOS2TypoLineGap + 200 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + SOME + openTypeOS2WeightClass + 500 + openTypeOS2WidthClass + 5 + openTypeOS2WinAscent + 750 + openTypeOS2WinDescent + 250 + openTypeVheaCaretOffset + 0 + openTypeVheaCaretSlopeRise + 0 + openTypeVheaCaretSlopeRun + 1 + openTypeVheaVertTypoAscender + 750 + openTypeVheaVertTypoDescender + -250 + openTypeVheaVertTypoLineGap + 200 + postscriptBlueFuzz + 1 + postscriptBlueScale + 0.039625 + postscriptBlueShift + 7 + postscriptBlueValues + + 500 + 510 + + postscriptDefaultCharacter + .notdef + postscriptDefaultWidthX + 400 + postscriptFamilyBlues + + 500 + 510 + + postscriptFamilyOtherBlues + + -250 + -260 + + postscriptFontName + SomeFont-Regular (Postscript Font Name) + postscriptForceBold + + postscriptFullName + Some Font-Regular (Postscript Full Name) + postscriptIsFixedPitch + + postscriptNominalWidthX + 400 + postscriptOtherBlues + + -250 + -260 + + postscriptSlantAngle + -12.5 + postscriptStemSnapH + + 100 + 120 + + postscriptStemSnapV + + 80 + 90 + + postscriptUnderlinePosition + -200 + postscriptUnderlineThickness + 20 + postscriptUniqueID + 4000000 + postscriptWeightName + Medium + postscriptWindowsCharacterSet + 1 + styleMapFamilyName + Some Font Regular (Style Map Family Name) + styleMapStyleName + regular + styleName + Regular (Style Name) + trademark + Trademark Some Foundry + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 0 + xHeight + 500 + year + 2008 + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..630ec6b --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif new file mode 100644 index 0000000..a751d87 --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif new file mode 100644 index 0000000..54066a2 --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif new file mode 100644 index 0000000..7abf451 --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist new file mode 100644 index 0000000..f730b93 --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist @@ -0,0 +1,34 @@ + + + + + .notdef + _notdef.glif + a + a.glif + b + b.glif + c + c.glif + d + d.glif + e + e.glif + f + f.glif + g + g.glif + h + h.glif + i + i.glif + j + j.glif + k + k.glif + l + l.glif + space + space.glif + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif new file mode 100644 index 0000000..09b619b --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif new file mode 100644 index 0000000..52abd0a --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif new file mode 100644 index 0000000..2c13b95 --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif new file mode 100644 index 0000000..fdbe8ac --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif new file mode 100644 index 0000000..561563f --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif new file mode 100644 index 0000000..84ef89b --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif new file mode 100644 index 0000000..550ee9b --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif new file mode 100644 index 0000000..c09ac3a --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif new file mode 100644 index 0000000..f71c34f --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif new file mode 100644 index 0000000..eaa0d16 --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/kerning.plist b/tests/testdata/TestFont1 (UFO3).ufo/kerning.plist new file mode 100644 index 0000000..1dd116e --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/kerning.plist @@ -0,0 +1,20 @@ + + + + + a + + a + 5 + b + -10 + space + 1 + + b + + a + -7 + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/layercontents.plist b/tests/testdata/TestFont1 (UFO3).ufo/layercontents.plist new file mode 100644 index 0000000..963df40 --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/lib.plist b/tests/testdata/TestFont1 (UFO3).ufo/lib.plist new file mode 100644 index 0000000..f257b3a --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/lib.plist @@ -0,0 +1,25 @@ + + + + + public.glyphOrder + + .notdef + glyph1 + glyph2 + space + a + b + c + d + e + f + g + h + i + j + k + l + + + diff --git a/tests/testdata/TestFont1 (UFO3).ufo/metainfo.plist b/tests/testdata/TestFont1 (UFO3).ufo/metainfo.plist new file mode 100644 index 0000000..8e836fb --- /dev/null +++ b/tests/testdata/TestFont1 (UFO3).ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + From 3bb859fe907af04e9a6d554ddac0c4ab170f09e0 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 15:46:22 +0100 Subject: [PATCH 67/71] tests: add *.ufoz test file Compressed using `zip` utility $ cd tests/testdata $ zip -9 -r "TestFont1 (UFO3).ufoz" "TestFont1 (UFO3).ufo" --- MANIFEST.in | 2 +- tests/testdata/TestFont1 (UFO3).ufoz | Bin 0 -> 10577 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/testdata/TestFont1 (UFO3).ufoz diff --git a/MANIFEST.in b/MANIFEST.in index 59b65bd..03ee2cb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,6 @@ include Documentation/Makefile recursive-include Documentation *.py *.rst recursive-include tests *.py -recursive-include tests/testdata *.plist *.glif *.fea *.txt *.ttx +recursive-include tests/testdata *.plist *.glif *.fea *.txt *.ttx *ufoz include requirements.txt extra_requirements.txt .coveragerc tox.ini diff --git a/tests/testdata/TestFont1 (UFO3).ufoz b/tests/testdata/TestFont1 (UFO3).ufoz new file mode 100644 index 0000000000000000000000000000000000000000..d78d494f0eb4a5872593431b58e69f4474414f65 GIT binary patch literal 10577 zcmbW7bzGENx5tMLi2)HQX_O8DX{5VhkPxMF=pm#VMH!@|OGE_`q!ETjX^`$zO6io6 zJDm6U9>#l!dxy`%Fn`SYKI`}Fz4i*!6j4yI0VgkNZ+Ydv{`mXXH2?)b%gh-fV{ZrH zqGklisB$y0yO`T^fV8jxD2G)reX?qpzKi=601|3r1O@v2-zJH;4NGv9||1vqK>67s7}D$NGsQ*=s_J6BZRFL1?q0Xqt#@yN7RRp@-$E~Iz*P+;{+S0CA2ExX5>@86% zEc;r>#N^o4#iGur(#C=n$OO&iYRp6@pvLhHaPw#vex1=!`{0O&i>J=0 zzzY916SFcJHZ#CqS;ksg!C0O)=uNsI{1qRQn1~+PCmU7LjX?_FX_cXx0SdAI^#K3z z|6WEs#sS>-RAc~v@+|+sR>tfOU@K?Hc`kZK)u}Xa0;N{>u)li3Vy;T$v5XL9Ye>;W zQu4np9gVMeGD{-yi}FqO^RVJE^P|s258m(19DrNmc2ca81X2n-6(Dr_W{F>uS6bt? zBH5+tR|}VOyf_9Lb$ih?mR{_cOIu>EEhYB@bQ2~SjX<|#uL^WJeDkK1a@k~jL9+^3rz!$BV9mM%!aSfn# zcmXHkdVLxZGN>ZeJjI(Ee3XkOT;2m3bvG5C zM|F=blkOiAaZ=e`iOTg3?FjhFUPVtMr*4JP@`zkkh?;#yMxl-%kjIZ6s=}m$ZtPAi zreN|7yI|EbF`PZIY}>Lg{;jr?S;$cR{R6Qpnn3c+4At*H@c?Zxts&~lB`L+~r8yjx z@7_A;t?3V7HLO+savuLgtte7;`w+=DYP$0AD(nw^(u2z6+0X%i{F?v(-C6pa>}M-G zbNfG}ISERC1dk5Aht4DXK;P9jU)K9P0pylF#gdEk^u-DOwT<+)RM$L_3`~b_wJ9&~ zeIG1Wb-jIO$nTr4sS{rVz2DZsvUQfha^9l1k1NwYmM}HtL7!z=J36wtC!Wz$)$q#U z503_Zj`{W;e091Ve-!_KCM`-VeR2HLf`=cOas&UaH+G1lK&4b~cL}d#R0C(89ogFg zqWWV?5>-`IHi{1G;Z$;EoM%kCUQ@=rQKrgRmIC!wa$H;oP{AWU%xwzerwid;2bk|f z@xX?7YpDd@VKujuYLVTKkDd&3-=WfY9DbB9My#lg_H~b?Isywdu5=Oqj>$sWWb?*- zF-ZztkwJ=s1Zql1XY); zT|i}GHQw!tXJ`9_`mUP3fTbxZb2qOLhr;0dhSHvblz0w>TZ8*Ay;g0yjY`wgGr?}E zc#KcMcHe>THXhUU`|OhG9jmsFKeF=NeWt{tbCtlKsi13@XVtN`coOWUr?I$wKs0Tv zudo&{>)_-}D*Nh(BBIuN4 zXYZBK17-=C)q-+g5XQOOu4b*1GY3EXkRRaIejh@Fi;?=nM4XZlM^fR6bF(eKvLhq1 zHJy#NcCnU<7Y+B$eH#3C{HiZDTLO?|og=QBcX|d4h^53pU_>3`%W`!weXRcUkf z!y2^$v`%Fxv60aPQbS|!U^A%q-!2MgJpIm6q0SKZ=$S5mQz_=hiPR@i*Wz;+^~8i` zIWaoxNMEo9mIAcYfJ|9B(~@>oCEL%n+KDw3G*Xm|H6O2P7q}G_t0_G&=u=VL*FJbwr%JL0I^3o_%}i@bW;>yso#W ztIaU?kC6r20Z-6;Ie6r%INQ=bk@;Hg_qfJDMzW3&@ta}BhOpzxPg2P7^u21P#ykz( zC9xE7mOkvu-eV@rEfpy#ZF|c}pgVdk*`S%2G!S#`z%P^h2&6qq5EOUNAIq_Pc-Azq#)=g&E?S=O*we_u*Ugs5SjjDZN@B`<6AH;-?NIlLVaQogs=of1f8+iaV90d-!z|pwD<$Au%f2MG zqJ!yT=b?9#C>$;Ky{Vs~O`7bz3EE^y%e2M77Yf5bqQ2&dxEuS?t;BaNeucr_jeY; zB3VVsrt33>I-KhyIs1AQA$wMRRe!&NsPDsZ|Z+V8nkh642Hp+O4 z7z(IJ-*Shk(6kvj(>r%670HG3)Af{pA)z3w_=rdAOxWa!T{=Ca@rJlnkrl{-ZL$=% zlmH$d3UiP{ek#aFj%4E(lGcnt?GZHn;FpokaYxNb_WDoq zLqf}cFaSTPyWl4^?O7+0jhU03m7T>O2hqRecqz{nuheq_gI15O*{m#%1TqrRjWA8nHsm{#+8LHI8eJrg3NSB9%)yTCGOHf(*$cLOx0^aH{LthzqzwZcdLvss#fl zZ6gI*Y4v@pa!*0}$-)l*L%fiORbPEQdZ)XF-WdP<$!|A~gs7Ntg%I8_Trpm_VrQP+ zwq_8c%bndIXopH8Cr;3+H+HnA3GjMQp>CaPq;^g^p#eSgNM2D&Up6~qX*605=s-UD z)n3N-?q{Tl@H94Maz~mjwD|0d0hJ>einhuv1+dG@&caZGz57dbxUS}m(6T)v3|eWJ zqaEjC-30MSDoRC6oW`RnzqI9^FO7_zpOCCXYphcr@IXPEUeF4y?;nHI$u}VsLS!w41zXh*B7guKTK_;D!`bEQmf4@BM)kqRzYd$8@IS10 zp=AAY0G&a~o(344mCZ9iD0+9|2&ePot?#ZYa4<{lTbrPyw!2U$$MS*9_ffnkR8a|L{9o3WC^Axuq1f)u2iKhw4%KPr=E zEq$Pv5R19NF%99U+4#0;@oL%0*K3KGF5uuwnBfuQ$K5N4N;-u3P}abeq<|}VW)btZ zk`L|dA*N>L7wQ=sS%>|Xo}$t%-lfndLd71VSHbvd;QoNuGdaCw4?!fDFP8{6;y8R0 zgDgxmqs86qP1MDRZc~#vzr%Tf{Gh987EhOKtuS>YEFE0z2Yjzt0nixAi&Uw-*5e0c z@2BP(eAV+h_t3d9-tB;7mKmv_YcXJWq91)b*)vujQo5==>tcf|cpJO?Q?)zcRA!Ri zq$|4~+fE(5sPyqoHvtvCG*J@&_w4h_FKb$YQaI*F#-G(KuCgYxH>m`2APOX?0ovJt z3v^OAU$oRrFO()6zSM%Kfg!7+XbGMKuUZrRJV$R?Qq?ko2+>Ts4JNB|U$fbkj*PZ{ zOG@*NCz#it%+L*A9U;Rax+>PHRXXrGa&6z~kzn1&$5Op)l24U}S+IE1`Gj_|qMwTq z(9pMfuq$V&y@T}b-&U<^2My-M*8b#fWZ@-U4qok3My46I7+1;bl^Hil0}b#Ch3n64 z)>(gjzM9Tdr;Bd6Zq&)82>58W=96QU-2Zy}UEl`LSL_=qcPW<_?k?$P{7D?kdWX-u zUXD;KZ%nU<3a-%TRFck3@9rRv?yXuq%p;Q#;PEC3it}AQni<1k+FunY1o`vYQdEeF zE^@QAY`_&=zeGH4v48 zqowY#vDE4AD)Qn$bM!;YR-nsUF=0nTbmx0IxkO$ts`?vCMPk|=WBoWTHbfNuv3d3- z_ttQ9YvcPqz2f4_y;GW#&BlK#jYI#Zp11_RV4F2{|?AX^H|r7_fZi?rrL4(%XcPz^|RAa%13Xpx82izTv;sF%W^cS zVszNNxKT05YYMrFhSuwfhud;@TP%=ganflVh=VpgSV+;n!IY#p)y=C*n&QiOrTUMP znpE?K+Z0^QflH`qbqO`Qq455L*F4LegHH8l+w@lJ9UN<-=@=rG$oc_jv#SPDRhH}% znw3Jm!^7DlDWm8zqf{xk!WTouK4|^yQO{cc@gbtDyp0aTt9cJ4VZa)KdAHhL`Ad1d zFtXZf3fybN)<1F*N7JIrRPu02K8FM2@9Uxn5U1)|giC-6^(XN&98Kqi?ghV@3iZh@ zUx8%;4ryNy8qGMj%@}`A*-*Kep`fr15rB+M?$Uv^M5Fo8^t!2F(?eDUD-qXjPB5y^8!MVyOye5v%GG5piB z*RnG7W;PvhyW%ax^+r`k0ZeZg^nzO5RB#C+;?V%v%(tY2Lu8UFrd?Q3cVWr`JcL*0 zEW^W1%O5@IAPkmcuc@NKu#w|v*PJ%^A>Rw&=r2;CY2TI5{mf}z4FcusI*oW7kAXkB z@5)v(h7W`mPW^ERT#35j{su0?Cj7c6##vsdgKZbri4JIAQZ#iY^d*9UlF0gl?8`{V zSoDPan8?ASeAFY^Ni}A7+hF$(@Q6iJ1|n;}wE}tiRdlVA zsCn#`vaY)~&>~N1utizp%8U*Kxz_7AOlUu%<5VX3+%%+gr}wvo1-H5Ygky+hw&voVo(AnhsB}6mw^Hb z0Kj}f>UQ~do=cCqBD_}$nA-gn7QZ{UrA+@L{zs5_cffd{Y7av}M`B49?!b{}F9^84 zvh1|CLI{oikzZtzW=>)|nD;ZKe73zfr?>RI_Mw{ar(LfUsg0Z7!^HM`(CtoW>TGBe zxW-HveQ2wS8(-n1i6h$_n#UJdW168$fgQAy z7;5oi#kV?I>Y2Q4Y+4}j&Ro>v(#;Vsxp;gSa-> zm~w%+jJX84Okg0G@mWVuvP|$)SUqom4Yoe2K6V<=>!m~BR7gEu9kkx$Av1Vj@UVs! zt4&3rd<9JCje;ojOd#m?GJKs~CUv_w)d-JLJc<4xyUE5po?%@4^ij&LBI2F^R6HGI zastVQWgEH23euE95%Zh#llVR`9$uV6A(l+E5Gqvh?_)#?amY0_qDLZ>nh|l^L)z&@ zGT6d2$x4H!+SH%=P^NUbOa%Y#R3Z zY>pdFtUTe?FbCy&dYlna$YvTZm1OI5wcGN*G}Q9|bTdtan=SMp+QfxRxV(lMe=Z`* zCXv~HuDMGu7bIS?LIHZj^pc(b`@6gyP9FQ5ZBfqR4FGAF3CwO zyLrJTc6Kl_xwteC2^T-<$4l>ud@|ipVBm7>esSa07Zjdlxun_{F>zRxT4v?0ug}E2 zjyqc)dU%=|$v7;Yh%H4$j9Gb&jrQV_(`gOXGy%XEJ^T>^1ohm?fgNNvJji0YHF~b7 z{>Im#`xWmVbkDEN=HwDXD-U&S$+B{Nz4e@^60q+br{`?gG-4pS=#p%bB4-Lb!WDo~bi)9t=91#8~Y*Ln8|)QvB3(A31@*$%!>?ZC6iEp~<{g~iW_&!w8!mz*#P1n6}Y*x)+hP4SO5De#+*I~w-~fi z{4+FqT~O$0HKOm08MeG#aKEcBvEofG^gTGTgD`-aD0o*C&3X6cFEutXHxY3Wzsy(d z3!i9;y$;eTY^M9LS45~n8-lM#RMSQC@MWos!7|0@wJWrTFw2uwrwPJ@rd5yf^cUxO8+Oj+S&w4N;gd>nSWC@56WIoeEs7fKdlaWg zEbivz@aTyoy3KwUXDMz>k;~>zK?^F-Ul#sJpPBG&)GWyiVs>;*-?CkF;RTG}Z0Sw1 z>5~21{;*%I(`jYR!uw?C5d#A23)?pyt)U`Hb(eYUI2tZh)g_jR#f4IdKu>l7k<@PT z%>3%LK1SA&!c9~UKkUg;tRn_BpuIg!<9PcwGjpk`pLphEGS_ux7ImnBd>JladQqxf z$>5UKpvCGcw1HH5vLH7S7?iNfCcfB z_cNy7X#bgh|1oGf&omj}v}x6|O#eLz|G(SkMzEiGuKjbS;^%??X^SGx#Q)g1N29O`*Iw_M(}CQU7RyGf3}6s1O593ra;&QF1Tla z{@Y1CkMPfON9?1XcBSnC!tbu? z2RluZCpe$>yDN4%4q|ucG$)Dhe9rG)(d9UZeUsCi#jEFYes@tW$3g5doaQiGJD>Br z<8V0+Vhw+q!$y2Q=XXVaISyi>e44{~{d~@!W%K2Di1p%WUIP5XzsB#sS@8arm9K;&sG-sRae9q~L<#H^LPk+ed2^67s7A12SW literal 0 HcmV?d00001 From 57bc0264f5ba1e9cf60af88786c0680f431e7902 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 17:16:55 +0100 Subject: [PATCH 68/71] don't attempt to remove empty optional files if writing new UFO from scratch and don't strip trailing '/', keep input path as is. It should not be used by anybody, anyway. --- Lib/ufoLib/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index f4f6eb6..9b4ac57 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -276,8 +276,6 @@ def __init__(self, path, validate=True): except fs.errors.NoSysPath: # network or in-memory FS may not map to the local one path = unicode(filesystem) - else: - path = path.rstrip("/") # when user passed an already initialized fs instance, it is her # responsibility to close it, thus UFOReader.close/__exit__ are no-op self._shouldClose = False @@ -889,8 +887,6 @@ def __init__( except fs.errors.NoSysPath: # network or in-memory FS may not map to the local one path = unicode(filesystem) - else: - path = path.rstrip("/") # if passed an FS object, always default to 'package' structure self._fileStructure = UFOFileStructure.PACKAGE # if FS contains a "metainfo.plist", we consider it non-empty @@ -912,7 +908,7 @@ def __init__( # if the file already exists, get the format version. # this will be needed for up and down conversion. previousFormatVersion = None - if havePreviousFile: + if self._havePreviousFile: metaInfo = self._getPlist(METAINFO_FILENAME) previousFormatVersion = metaInfo.get("formatVersion") try: @@ -1263,7 +1259,7 @@ def writeKerning(self, kerning, validate=None): kerningDict[left][right] = value if kerningDict: self._writePlist(KERNING_FILENAME, kerningDict) - else: + elif self._havePreviousFile: self.removePath(KERNING_FILENAME, force=True, removeEmptyParents=False) # lib.plist @@ -1284,7 +1280,7 @@ def writeLib(self, libDict, validate=None): raise UFOLibError(message) if libDict: self._writePlist(LIB_FILENAME, libDict) - else: + elif self._havePreviousFile: self.removePath(LIB_FILENAME, force=True, removeEmptyParents=False) # features.fea @@ -1303,7 +1299,7 @@ def writeFeatures(self, features, validate=None): raise UFOLibError("The features are not text.") if features: self.writeBytesToPath(FEATURES_FILENAME, features.encode("utf8")) - else: + elif self._havePreviousFile: self.removePath(FEATURES_FILENAME, force=True, removeEmptyParents=False) # glyph sets & layers From 37e3f3bcb3b4cb3d7ac7631d91b8e5bdf7c0a1fe Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 17:32:15 +0100 Subject: [PATCH 69/71] Add tests/test_UFOZ.py we'll add more tests later --- tests/test_UFOZ.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/test_UFOZ.py diff --git a/tests/test_UFOZ.py b/tests/test_UFOZ.py new file mode 100644 index 0000000..d009e53 --- /dev/null +++ b/tests/test_UFOZ.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +from fontTools.misc.py23 import tostr +from ufoLib import UFOReader, UFOWriter, UFOFileStructure +from ufoLib.errors import UFOLibError, GlifLibError +from ufoLib import plistlib +import sys +import os +import fs.osfs +import fs.tempfs +import fs.memoryfs +import fs.copy +import pytest +import warnings + + +TESTDATA = fs.osfs.OSFS( + os.path.join(os.path.dirname(__file__), "testdata") +) +TEST_UFO3 = "TestFont1 (UFO3).ufo" +TEST_UFOZ = "TestFont1 (UFO3).ufoz" + + +@pytest.fixture(params=[TEST_UFO3, TEST_UFOZ]) +def testufo(request): + name = request.param + with fs.tempfs.TempFS() as tmp: + if TESTDATA.isdir(name): + fs.copy.copy_dir(TESTDATA, name, tmp, name) + else: + fs.copy.copy_file(TESTDATA, name, tmp, name) + yield tmp.getsyspath(name) + + +@pytest.fixture +def testufoz(): + with fs.tempfs.TempFS() as tmp: + fs.copy.copy_file(TESTDATA, TEST_UFOZ, tmp, TEST_UFOZ) + yield tmp.getsyspath(TEST_UFOZ) + + +class TestUFOZ(object): + + def test_read(self, testufoz): + with UFOReader(testufoz) as reader: + assert reader.fileStructure == UFOFileStructure.ZIP + assert reader.formatVersion == 3 + + def test_write(self, testufoz): + with UFOWriter(testufoz, structure="zip") as writer: + writer.writeLib({"hello world": 123}) + with UFOReader(testufoz) as reader: + assert reader.readLib() == {"hello world": 123} + + +def test_pathlike(testufo): + + class PathLike(object): + + def __init__(self, s): + self._path = s + + def __fspath__(self): + return tostr(self._path, sys.getfilesystemencoding()) + + path = PathLike(testufo) + + with UFOReader(path) as reader: + assert reader._path == path.__fspath__() + + with UFOWriter(path) as writer: + assert writer._path == path.__fspath__() + + +def test_path_attribute_deprecated(testufo): + with UFOWriter(testufo) as writer: + with pytest.warns(DeprecationWarning, match="The 'path' attribute"): + writer.path + + +@pytest.fixture +def memufo(): + m = fs.memoryfs.MemoryFS() + fs.copy.copy_dir(TESTDATA, TEST_UFO3, m, "/") + return m + + +class TestMemoryFS(object): + + def test_init_reader(self, memufo): + with UFOReader(memufo) as reader: + assert reader.formatVersion == 3 + assert reader.fileStructure == UFOFileStructure.PACKAGE + + def test_init_writer(self): + m = fs.memoryfs.MemoryFS() + with UFOWriter(m) as writer: + assert m.exists("metainfo.plist") + assert writer._path == "" From 81e0855cc7fa9fd4ca5e2fa8c93a4546530a1b30 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 18:20:10 +0100 Subject: [PATCH 70/71] must decode incoming user paths on py2, pyfilesystem2 requires unicode paths with this the defcon test suite passes on both py2 and py3. I think i'm done for now. --- Lib/ufoLib/__init__.py | 23 ++++++++++++++++------- Lib/ufoLib/utils.py | 6 ++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 9b4ac57..77dafc0 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, unicode_literals +import sys import os from copy import deepcopy import zipfile @@ -18,7 +19,7 @@ from ufoLib.filenames import userNameToFileName from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning from ufoLib.errors import UFOLibError -from ufoLib.utils import datetimeAsTimestamp +from ufoLib.utils import datetimeAsTimestamp, fsdecode """ A library for importing .ufo files and their descendants. @@ -107,7 +108,7 @@ def _getFileModificationTime(self, path): Returns None if the file does not exist. """ try: - dt = self.fs.getinfo(path, namespaces=["details"]).modified + dt = self.fs.getinfo(fsdecode(path), namespaces=["details"]).modified except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound): return None else: @@ -121,7 +122,7 @@ def _readBytesFromPath(self, path): Returns None if the file does not exist. """ try: - return self.fs.getbytes(path) + return self.fs.getbytes(fsdecode(path)) except fs.errors.ResourceNotFound: return None @@ -375,6 +376,7 @@ def getReadFileForPath(self, path, encoding=None): Note: The caller is responsible for closing the open file. """ + path = fsdecode(path) try: if encoding is None: return self.fs.openbin(path) @@ -760,6 +762,7 @@ def readImage(self, fileName, validate=None): except AttributeError: # in case readImage is called before getImageDirectoryListing imagesFS = self.fs.opendir(IMAGES_DIRNAME) + fileName = fsdecode(fileName) data = imagesFS.getbytes(fileName) if data is None: raise UFOLibError("No image file named %s." % fileName) @@ -986,6 +989,8 @@ def copyFromReader(self, reader, sourcePath, destPath): """ if not isinstance(reader, UFOReader): raise UFOLibError("The reader must be an instance of UFOReader.") + sourcePath = fsdecode(sourcePath) + destPath = fsdecode(destPath) if not reader.fs.exists(sourcePath): raise UFOLibError("The reader does not have data located at \"%s\"." % sourcePath) if self.fs.exists(destPath): @@ -1005,6 +1010,7 @@ def writeBytesToPath(self, path, data): so that the modification date is preserved. If needed, the directory tree for the given path will be built. """ + path = fsdecode(path) if self._havePreviousFile: if self.fs.isfile(path) and data == self.fs.getbytes(path): return @@ -1026,6 +1032,7 @@ def getFileObjectForPath(self, path, mode="w", encoding=None): Note: The caller is responsible for closing the open file. """ + path = fsdecode(path) try: return self.fs.open(path, mode=mode, encoding=encoding) except fs.errors.ResourceNotFound as e: @@ -1051,6 +1058,7 @@ def removePath(self, path, force=False, removeEmptyParents=True): If the directory where 'path' is located becomes empty, it will be automatically removed, unless 'removeEmptyParents' is False. """ + path = fsdecode(path) try: self.fs.remove(path) except fs.errors.FileExpected: @@ -1076,7 +1084,7 @@ def setModificationTime(self): This is never called automatically. It is up to the caller to call this when finished working on the UFO. """ - path = self.path + path = self._path if path is not None: os.utime(path, None) @@ -1534,6 +1542,7 @@ def writeImage(self, fileName, data, validate=None): validate = self._validate if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) + fileName = fsdecode(fileName) if validate: valid, error = pngValidator(data=data) if not valid: @@ -1547,7 +1556,7 @@ def removeImage(self, fileName, validate=None): # XXX remove unused 'validate'? """ if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - self.removePath("%s/%s" % (IMAGES_DIRNAME, fileName)) + self.removePath("%s/%s" % (IMAGES_DIRNAME, fsdecode(fileName))) def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None): """ @@ -1559,8 +1568,8 @@ def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=Non validate = self._validate if self._formatVersion < 3: raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) - sourcePath = "%s/%s" % (IMAGES_DIRNAME, sourceFileName) - destPath = "%s/%s" % (IMAGES_DIRNAME, destFileName) + sourcePath = "%s/%s" % (IMAGES_DIRNAME, fsdecode(sourceFileName)) + destPath = "%s/%s" % (IMAGES_DIRNAME, fsdecode(destFileName)) self.copyFromReader(reader, sourcePath, destPath) def close(self): diff --git a/Lib/ufoLib/utils.py b/Lib/ufoLib/utils.py index e8dbc89..5044dd1 100644 --- a/Lib/ufoLib/utils.py +++ b/Lib/ufoLib/utils.py @@ -2,9 +2,11 @@ It's not considered part of the public ufoLib API. """ from __future__ import absolute_import, unicode_literals +import sys import warnings import functools from datetime import datetime +from fontTools.misc.py23 import tounicode if hasattr(datetime, "timestamp"): # python >= 3.3 @@ -64,6 +66,10 @@ def wrapper(*args, **kwargs): return deprecated_decorator +def fsdecode(path, encoding=sys.getfilesystemencoding()): + return tounicode(path, encoding=encoding) + + if __name__ == "__main__": import doctest From 7101e62863951cdebe21ec3dee406bc3f5158754 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 9 Oct 2018 18:33:14 +0100 Subject: [PATCH 71/71] =?UTF-8?q?Bump=20version:=202.4.0.dev0=20=E2=86=92?= =?UTF-8?q?=203.0.0.dev0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/ufoLib/__init__.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 77dafc0..f08eec2 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -66,7 +66,7 @@ "convertFontInfoValueForAttributeFromVersion2ToVersion1" ] -__version__ = "2.4.0.dev0" +__version__ = "3.0.0.dev0" # --------- diff --git a/setup.cfg b/setup.cfg index bbe0beb..8c6be1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.4.0.dev0 +current_version = 3.0.0.dev0 commit = True tag = False tag_name = v{new_version} diff --git a/setup.py b/setup.py index a07888b..6db65e8 100755 --- a/setup.py +++ b/setup.py @@ -147,7 +147,7 @@ def run(self): setup_params = dict( name="ufoLib", - version="2.4.0.dev0", + version="3.0.0.dev0", description="A low-level UFO reader and writer.", author="Just van Rossum, Tal Leming, Erik van Blokland, others", author_email="info@robofab.com",