Significant work on /data.
git-svn-id: http://svn.robofab.com/branches/ufo3k@269 b5fa9d6c-a76f-4ffd-b3cb-f825fc41095c
This commit is contained in:
parent
597066a7fc
commit
7302683af3
@ -1,7 +1,11 @@
|
||||
#! /usr/local/bin/apppython
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
import tempfile
|
||||
import codecs
|
||||
from plistlib import writePlist, readPlist
|
||||
from robofab.ufoLib import UFOReader, UFOWriter, UFOLibError, \
|
||||
convertUFOFormatVersion1ToFormatVersion2, convertUFOFormatVersion2ToFormatVersion1
|
||||
@ -1551,7 +1555,128 @@ class WriteFontInfoVersion2TestCase(unittest.TestCase):
|
||||
self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
|
||||
|
||||
|
||||
class UFO3ReadDataTestCase(unittest.TestCase):
|
||||
|
||||
def getFontPath(self):
|
||||
import robofab
|
||||
path = os.path.dirname(robofab.__file__)
|
||||
path = os.path.dirname(path)
|
||||
path = os.path.dirname(path)
|
||||
path = os.path.join(path, "TestData", "UFO3-Read Data.ufo")
|
||||
return path
|
||||
|
||||
def testUFOReaderDataDirectoryListing(self):
|
||||
reader = UFOReader(self.getFontPath())
|
||||
found = reader.getDataDirectoryListing()
|
||||
expected = [
|
||||
'data/org.unifiedfontobject.directory/bar/lol.txt',
|
||||
'data/org.unifiedfontobject.directory/foo.txt',
|
||||
'data/org.unifiedfontobject.file.txt'
|
||||
]
|
||||
self.assertEqual(found, expected)
|
||||
|
||||
def testUFOReaderBytesFromPath(self):
|
||||
reader = UFOReader(self.getFontPath())
|
||||
found = reader.readBytesFromPath("data/org.unifiedfontobject.file.txt")
|
||||
expected = "file.txt"
|
||||
self.assertEqual(found, expected)
|
||||
found = reader.readBytesFromPath("data/org.unifiedfontobject.directory/bar/lol.txt")
|
||||
expected = "lol.txt"
|
||||
self.assertEqual(found, expected)
|
||||
found = reader.readBytesFromPath("data/org.unifiedfontobject.doesNotExist")
|
||||
expected = None
|
||||
self.assertEqual(found, expected)
|
||||
|
||||
def testUFOReaderReadFileFromPath(self):
|
||||
reader = UFOReader(self.getFontPath())
|
||||
fileObject = reader.getReadFileForPath("data/org.unifiedfontobject.file.txt")
|
||||
self.assertNotEqual(fileObject, None)
|
||||
hasRead = hasattr(fileObject, "read")
|
||||
self.assertEqual(hasRead, True)
|
||||
fileObject.close()
|
||||
fileObject = reader.getReadFileForPath("data/org.unifiedfontobject.doesNotExist")
|
||||
self.assertEqual(fileObject, None)
|
||||
|
||||
class UFO3WriteDataTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.dstDir = tempfile.mktemp()
|
||||
os.mkdir(self.dstDir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.dstDir)
|
||||
|
||||
def testUFOWriterWriteBytesToPath(self):
|
||||
# basic file
|
||||
path = "data/org.unifiedfontobject.writebytesbasicfile.txt"
|
||||
bytes = "test"
|
||||
writer = UFOWriter(self.dstDir)
|
||||
writer.writeBytesToPath(path, bytes)
|
||||
path = os.path.join(self.dstDir, path)
|
||||
self.assertEqual(os.path.exists(path), True)
|
||||
f = open(path, "rb")
|
||||
written = f.read()
|
||||
f.close()
|
||||
self.assertEqual(bytes, written)
|
||||
# basic file with unicode text
|
||||
path = "data/org.unifiedfontobject.writebytesbasicunicodefile.txt"
|
||||
bytes = u"tëßt"
|
||||
writer = UFOWriter(self.dstDir)
|
||||
writer.writeBytesToPath(path, bytes, encoding="utf8")
|
||||
path = os.path.join(self.dstDir, path)
|
||||
self.assertEqual(os.path.exists(path), True)
|
||||
f = codecs.open(path, "rb")
|
||||
written = f.read().decode("utf8")
|
||||
f.close()
|
||||
self.assertEqual(bytes, written)
|
||||
# basic directory
|
||||
path = "data/org.unifiedfontobject.writebytesdirectory/level1/level2/file.txt"
|
||||
bytes = "test"
|
||||
writer = UFOWriter(self.dstDir)
|
||||
writer.writeBytesToPath(path, bytes)
|
||||
path = os.path.join(self.dstDir, path)
|
||||
self.assertEqual(os.path.exists(path), True)
|
||||
f = open(path, "rb")
|
||||
written = f.read()
|
||||
f.close()
|
||||
self.assertEqual(bytes, written)
|
||||
|
||||
def testUFOWriterWriteFileToPath(self):
|
||||
# basic file
|
||||
path = "data/org.unifiedfontobject.getwritefile.txt"
|
||||
writer = UFOWriter(self.dstDir)
|
||||
fileObject = writer.getFileObjectForPath(path)
|
||||
self.assertNotEqual(fileObject, None)
|
||||
hasRead = hasattr(fileObject, "read")
|
||||
self.assertEqual(hasRead, True)
|
||||
fileObject.close()
|
||||
|
||||
def testUFOWriterRemoveFile(self):
|
||||
path1 = "data/org.unifiedfontobject.removefile/level1/level2/file1.txt"
|
||||
path2 = "data/org.unifiedfontobject.removefile/level1/level2/file2.txt"
|
||||
path3 = "data/org.unifiedfontobject.removefile/level1/file3.txt"
|
||||
writer = UFOWriter(self.dstDir)
|
||||
writer.writeBytesToPath(path1, "test")
|
||||
writer.writeBytesToPath(path2, "test")
|
||||
writer.writeBytesToPath(path3, "test")
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path1)), True)
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path2)), True)
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), True)
|
||||
writer.removeFileForPath(path1)
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path1)), False)
|
||||
self.assertEqual(os.path.exists(os.path.dirname(os.path.join(self.dstDir, path1))), True)
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path2)), True)
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), True)
|
||||
writer.removeFileForPath(path2)
|
||||
self.assertEqual(os.path.exists(os.path.dirname(os.path.join(self.dstDir, path1))), False)
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path2)), False)
|
||||
self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), True)
|
||||
writer.removeFileForPath(path3)
|
||||
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")
|
||||
|
||||
class ConversionFunctionsTestCase(unittest.TestCase):
|
||||
|
||||
|
@ -34,6 +34,7 @@ import os
|
||||
import shutil
|
||||
from cStringIO import StringIO
|
||||
import calendar
|
||||
import codecs
|
||||
from robofab.plistlib import readPlist, writePlist
|
||||
from robofab.glifLib import GlyphSet, READ_MODE, WRITE_MODE
|
||||
|
||||
@ -133,8 +134,8 @@ def convertUFOFormatVersion1ToFormatVersion2(inPath, outPath=None):
|
||||
# write the info manually
|
||||
writePlistAtomically(infoData, infoPath)
|
||||
# copy the glyph tree
|
||||
inGlyphs = os.path.join(inPath, GLYPHS_DIRNAME)
|
||||
outGlyphs = os.path.join(outPath, GLYPHS_DIRNAME)
|
||||
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)
|
||||
|
||||
@ -189,8 +190,8 @@ def convertUFOFormatVersion2ToFormatVersion1(inPath, outPath=None):
|
||||
# write the info manually
|
||||
writePlistAtomically(infoData, infoPath)
|
||||
# copy the glyph tree
|
||||
inGlyphs = os.path.join(inPath, GLYPHS_DIRNAME)
|
||||
outGlyphs = os.path.join(outPath, GLYPHS_DIRNAME)
|
||||
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)
|
||||
|
||||
@ -208,35 +209,53 @@ class UFOReader(object):
|
||||
self._path = path
|
||||
self.readMetaInfo()
|
||||
|
||||
# properties
|
||||
|
||||
def _get_formatVersion(self):
|
||||
return self._formatVersion
|
||||
|
||||
formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.")
|
||||
|
||||
# support methods
|
||||
|
||||
def _checkForFile(self, path):
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def readDataFromPath(self, path):
|
||||
def readBytesFromPath(self, path, encoding=None):
|
||||
"""
|
||||
Reads the data from the file at the given path.
|
||||
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.
|
||||
"""
|
||||
# XXX this is likely less efficient than allowing
|
||||
# the caller to read files directly. However a future
|
||||
# version of the UFO may not be a package with readable
|
||||
# files. (ie, it could be a zip.)
|
||||
path = os.path.join(self._path, path)
|
||||
if not self._checkForFile(path):
|
||||
return None
|
||||
f = open(path, "rb")
|
||||
f = codecs.open(path, READ_MODE, 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.
|
||||
"""
|
||||
path = os.path.join(self._path, path)
|
||||
if not self._checkForFile(path):
|
||||
return None
|
||||
f = codecs.open(path, READ_MODE, encoding=encoding)
|
||||
return f
|
||||
|
||||
# metainfo.plist
|
||||
|
||||
def readMetaInfo(self):
|
||||
"""
|
||||
Read metainfo.plist. Only used for internal operations.
|
||||
@ -253,6 +272,8 @@ class UFOReader(object):
|
||||
raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path))
|
||||
self._formatVersion = formatVersion
|
||||
|
||||
# groups.plist
|
||||
|
||||
def readGroups(self):
|
||||
"""
|
||||
Read groups.plist. Returns a dict.
|
||||
@ -262,6 +283,8 @@ class UFOReader(object):
|
||||
return {}
|
||||
return readPlist(path)
|
||||
|
||||
# fontinfo.plist
|
||||
|
||||
def readInfo(self, info):
|
||||
"""
|
||||
Read fontinfo.plist. It requires an object that allows
|
||||
@ -301,6 +324,8 @@ class UFOReader(object):
|
||||
except AttributeError:
|
||||
raise UFOLibError("The supplied info object does not support setting a necessary attribute (%s)." % attr)
|
||||
|
||||
# kerning.plist
|
||||
|
||||
def readKerning(self):
|
||||
"""
|
||||
Read kerning.plist. Returns a dict.
|
||||
@ -316,6 +341,8 @@ class UFOReader(object):
|
||||
kerning[left, right] = value
|
||||
return kerning
|
||||
|
||||
# lib.plist
|
||||
|
||||
def readLib(self):
|
||||
"""
|
||||
Read lib.plist. Returns a dict.
|
||||
@ -325,6 +352,8 @@ class UFOReader(object):
|
||||
return {}
|
||||
return readPlist(path)
|
||||
|
||||
# features.fea
|
||||
|
||||
def readFeatures(self):
|
||||
"""
|
||||
Read features.fea. Returns a string.
|
||||
@ -337,6 +366,8 @@ class UFOReader(object):
|
||||
f.close()
|
||||
return text
|
||||
|
||||
# glyph sets & layers
|
||||
|
||||
def _readLayerContents(self):
|
||||
"""
|
||||
Private utility for reading layercontents.plist.
|
||||
@ -364,7 +395,7 @@ class UFOReader(object):
|
||||
# public.foreground seems like the logical name but it needs to be discussed.
|
||||
layerContents = self._readLayerContents()
|
||||
for layerName, layerDirectory in layerContents:
|
||||
if layerDirectory == DEFAULT_GLYPHS_DIRNAME:
|
||||
if layerDirectory == DEFAULT_DEFAULT_GLYPHS_DIRNAME:
|
||||
return layerName
|
||||
# The default layer is not defined in the UFO.
|
||||
# XXX Should this error be raised when layercontents.plist is first read?
|
||||
@ -396,7 +427,7 @@ class UFOReader(object):
|
||||
Return a dictionary that maps unicode values (ints) to
|
||||
lists of glyph names.
|
||||
"""
|
||||
glyphsPath = os.path.join(self._path, GLYPHS_DIRNAME)
|
||||
glyphsPath = os.path.join(self._path, DEFAULT_GLYPHS_DIRNAME)
|
||||
glyphSet = GlyphSet(glyphsPath)
|
||||
allUnicodes = glyphSet.getUnicodes()
|
||||
cmap = {}
|
||||
@ -408,6 +439,8 @@ class UFOReader(object):
|
||||
cmap[code] = [glyphName]
|
||||
return cmap
|
||||
|
||||
# /data
|
||||
|
||||
def getDataDirectoryListing(self, maxDepth=100):
|
||||
"""
|
||||
Returns a list of all files and directories
|
||||
@ -467,7 +500,7 @@ class UFOWriter(object):
|
||||
self._removePath(p)
|
||||
# remove glyphs/layerinfo.plist
|
||||
# XXX should glifLib handle this one?
|
||||
p = os.path.join(path, GLYPHS_DIRNAME, LAYERINFO_FILENAME)
|
||||
p = os.path.join(path, DEFAULT_GLYPHS_DIRNAME, LAYERINFO_FILENAME)
|
||||
self._removePath(p)
|
||||
# remove /images
|
||||
p = os.path.join(path, IMAGES_DIRNAME)
|
||||
@ -479,6 +512,11 @@ class UFOWriter(object):
|
||||
# remove features.fea
|
||||
p = os.path.join(path, FEATURES_FILENAME)
|
||||
self._removePath(p)
|
||||
# read the existing layer contents
|
||||
if formatVersion >= 3:
|
||||
p = os.path.join(layercontents)
|
||||
|
||||
# properties
|
||||
|
||||
def _get_formatVersion(self):
|
||||
return self._formatVersion
|
||||
@ -490,12 +528,14 @@ class UFOWriter(object):
|
||||
|
||||
fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.")
|
||||
|
||||
# support methods
|
||||
|
||||
def _removePath(self, path):
|
||||
if os.path.exists(p):
|
||||
if os.path.isdir(p):
|
||||
shutil.rmtree(p)
|
||||
if os.path.exists(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
os.remove(p)
|
||||
os.remove(path)
|
||||
|
||||
def _makeDirectory(self, subDirectory=None):
|
||||
path = self._path
|
||||
@ -505,6 +545,86 @@ class UFOWriter(object):
|
||||
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 _removeEmptyDirectoriesForPath(self, directory):
|
||||
absoluteDirectory = os.path.join(self._path, directory)
|
||||
if not len(os.listdir(absoluteDirectory)):
|
||||
shutil.rmtree(absoluteDirectory)
|
||||
else:
|
||||
return
|
||||
directory = os.path.dirname(directory)
|
||||
if directory:
|
||||
self._removeEmptyDirectoriesForPath(directory)
|
||||
|
||||
def writeBytesToPath(self, path, bytes, 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 self._formatVersion < 2:
|
||||
raise UFOLibError("The data directory is not allowed in UFO Format Version %d." % self.formatVersion)
|
||||
self._buildDirectoryTree(path)
|
||||
path = os.path.join(self._path, path)
|
||||
writeFileAtomically(bytes, path, encoding=encoding)
|
||||
|
||||
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.
|
||||
"""
|
||||
if self._formatVersion < 2:
|
||||
raise UFOLibError("The data directory is not allowed in UFO Format Version %d." % self.formatVersion)
|
||||
self._buildDirectoryTree(path)
|
||||
return codecs.open(path, WRITE_MODE, 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
|
||||
originalPath = path
|
||||
path = os.path.join(self._path, path)
|
||||
if not os.path.exists(path):
|
||||
raise UFOLibError("The file %s does not exist." % path)
|
||||
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))
|
||||
|
||||
# metainfo.plist
|
||||
|
||||
def _writeMetaInfo(self):
|
||||
self._makeDirectory()
|
||||
path = os.path.join(self._path, METAINFO_FILENAME)
|
||||
@ -514,6 +634,8 @@ class UFOWriter(object):
|
||||
)
|
||||
writePlistAtomically(metaInfo, path)
|
||||
|
||||
# groups.plist
|
||||
|
||||
def writeGroups(self, groups):
|
||||
"""
|
||||
Write groups.plist. This method requires a
|
||||
@ -529,6 +651,8 @@ class UFOWriter(object):
|
||||
elif os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
# fontinfo.plist
|
||||
|
||||
def writeInfo(self, info):
|
||||
"""
|
||||
Write info.plist. This method requires an object
|
||||
@ -557,6 +681,8 @@ class UFOWriter(object):
|
||||
# write file
|
||||
writePlistAtomically(infoData, path)
|
||||
|
||||
# kerning.plist
|
||||
|
||||
def writeKerning(self, kerning):
|
||||
"""
|
||||
Write kerning.plist. This method requires a
|
||||
@ -575,6 +701,8 @@ class UFOWriter(object):
|
||||
elif os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
# lib.plist
|
||||
|
||||
def writeLib(self, libDict):
|
||||
"""
|
||||
Write lib.plist. This method requires a
|
||||
@ -587,6 +715,8 @@ class UFOWriter(object):
|
||||
elif os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
# features.fea
|
||||
|
||||
def writeFeatures(self, features):
|
||||
"""
|
||||
Write features.fea. This method requires a
|
||||
@ -598,12 +728,14 @@ class UFOWriter(object):
|
||||
path = os.path.join(self._path, FEATURES_FILENAME)
|
||||
writeFileAtomically(features, path)
|
||||
|
||||
def makeGlyphPath(self):
|
||||
# glyph sets & layers
|
||||
|
||||
def makeGlyphPath(self, layerName):
|
||||
"""
|
||||
Make the glyphs directory in the .ufo.
|
||||
Returns the path of the directory created.
|
||||
"""
|
||||
glyphDir = self._makeDirectory(GLYPHS_DIRNAME)
|
||||
glyphDir = self._makeDirectory(DEFAULT_GLYPHS_DIRNAME)
|
||||
return glyphDir
|
||||
|
||||
def getGlyphSet(self, glyphNameToFileNameFunc=None):
|
||||
@ -644,14 +776,16 @@ def writePlistAtomically(obj, path):
|
||||
data = f.getvalue()
|
||||
writeFileAtomically(data, path)
|
||||
|
||||
def writeFileAtomically(text, path):
|
||||
"""Write text into a file at path. Do this sort of atomically
|
||||
def writeFileAtomically(text, path, encoding=None):
|
||||
"""
|
||||
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."""
|
||||
is preserved. An encoding may be passed if needed.
|
||||
"""
|
||||
if os.path.exists(path):
|
||||
f = open(path, READ_MODE)
|
||||
f = codecs.open(path, READ_MODE, encoding=encoding)
|
||||
oldText = f.read()
|
||||
f.close()
|
||||
if text == oldText:
|
||||
@ -660,7 +794,7 @@ def writeFileAtomically(text, path):
|
||||
if not text:
|
||||
os.remove(path)
|
||||
if text:
|
||||
f = open(path, WRITE_MODE)
|
||||
f = codecs.open(path, WRITE_MODE, encoding=encoding)
|
||||
f.write(text)
|
||||
f.close()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user