Significant work on /data.

git-svn-id: http://svn.robofab.com/branches/ufo3k@269 b5fa9d6c-a76f-4ffd-b3cb-f825fc41095c
This commit is contained in:
Tal Leming 2011-09-12 17:49:34 +00:00
parent 597066a7fc
commit 7302683af3
2 changed files with 284 additions and 25 deletions

View File

@ -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):

View File

@ -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()