Merge pull request #1344 from anthrotype/ufo-reader-and-writer

make UFOWriter a subclass of UFOReader, use mixins for shared methods
This commit is contained in:
Cosimo Lupo 2018-10-25 17:12:27 +01:00 committed by GitHub
commit 10e10a2c82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 110 additions and 155 deletions

View File

@ -5,6 +5,7 @@ from copy import deepcopy
import logging
import zipfile
import enum
from collections import OrderedDict
import fs
import fs.base
import fs.subfs
@ -57,6 +58,7 @@ __all__ = [
"UFOLibError",
"UFOReader",
"UFOWriter",
"UFOReaderWriter",
"UFOFileStructure",
"fontInfoAttributesVersion1",
"fontInfoAttributesVersion2",
@ -105,104 +107,92 @@ class UFOFileStructure(enum.Enum):
# --------------
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(fsdecode(path), namespaces=["details"]).modified
except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound):
return None
else:
return datetimeAsTimestamp(dt)
class _UFOBaseIO(object):
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(fsdecode(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:
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:
data = plistlib.dumps(obj)
dt = self.fs.getinfo(fsdecode(path), namespaces=["details"]).modified
except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound):
return None
else:
return datetimeAsTimestamp(dt)
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 written on %s because "
"the data is not properly formatted: %s"
% (fileName, self.fs, e)
"'%s' could not be read on %s: %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:
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:
plistlib.dump(obj, fp)
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)
)
# ----------
# UFO Reader
# ----------
class UFOReader(object):
class UFOReader(_UFOBaseIO):
"""
Read the various components of the .ufo.
@ -280,6 +270,18 @@ class UFOReader(object):
# 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 of the UFO (DEPRECATED).")
def _get_formatVersion(self):
return self._formatVersion
@ -291,7 +293,7 @@ class UFOReader(object):
fileStructure = property(
_get_fileStructure,
doc=(
"The current file structure of the UFO: "
"The file structure of the UFO: "
"either UFOFileStructure.ZIP or UFOFileStructure.PACKAGE"
)
)
@ -347,9 +349,16 @@ class UFOReader(object):
# support methods
_getPlist = _getPlist
getFileModificationTime = _getFileModificationTime
readBytesFromPath = _readBytesFromPath
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(fsdecode(path))
except fs.errors.ResourceNotFound:
return None
def getReadFileForPath(self, path, encoding=None):
"""
@ -796,7 +805,7 @@ class UFOReader(object):
# UFO Writer
# ----------
class UFOWriter(object):
class UFOWriter(UFOReader):
"""
Write the various components of the .ufo.
@ -948,7 +957,7 @@ class UFOWriter(object):
self.layerContents = {}
if previousFormatVersion is not None and previousFormatVersion >= 3:
# already exists
self._readLayerContents(validate=validate)
self.layerContents = OrderedDict(self._readLayerContents(validate))
else:
# previous < 3
# imply the layer contents
@ -959,46 +968,13 @@ class UFOWriter(object):
# 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 (DEPRECATED).")
def _get_formatVersion(self):
return self._formatVersion
formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is set into metainfo.plist during __init__.")
def _get_fileCreator(self):
return self._fileCreator
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
_getPlist = _getPlist
_writePlist = _writePlist
readBytesFromPath = _readBytesFromPath
getFileModificationTime = _getFileModificationTime
def copyFromReader(self, reader, sourcePath, destPath):
"""
Copy the sourcePath in the provided UFOReader to destPath
@ -1334,25 +1310,6 @@ class UFOWriter(object):
# glyph sets & layers
def _readLayerContents(self, validate):
"""
Rebuild the layer contents list by checking what glyph sets
are available on disk.
``validate`` will validate the data.
"""
# read the file on disk
raw = self._getPlist(LAYERCONTENTS_FILENAME)
contents = {}
if validate:
valid, error = layerContentsValidator(raw, self.fs)
if not valid:
raise UFOLibError(error)
for entry in raw:
layerName, directoryName = entry
contents[layerName] = directoryName
self.layerContents = contents
def writeLayerContents(self, layerOrder=None, validate=None):
"""
Write the layercontents.plist file. This method *must* be called
@ -1414,7 +1371,7 @@ class UFOWriter(object):
raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion)
# locate a layer name when None has been given
if layerName is None and defaultLayer:
for existingLayerName, directory in list(self.layerContents.items()):
for existingLayerName, directory in self.layerContents.items():
if directory == DEFAULT_GLYPHS_DIRNAME:
layerName = existingLayerName
if layerName is None:
@ -1462,10 +1419,13 @@ class UFOWriter(object):
# matches the default being written. also make sure that this layer
# name is not already linked to a non-default layer.
if defaultLayer:
for existingLayerName, directory in list(self.layerContents.items()):
for existingLayerName, directory in self.layerContents.items():
if directory == DEFAULT_GLYPHS_DIRNAME:
if existingLayerName != layerName:
raise UFOLibError("Another layer is already mapped to the default directory.")
raise UFOLibError(
"Another layer ('%s') is already mapped to the default directory."
% existingLayerName
)
elif existingLayerName == layerName:
raise UFOLibError("The layer name is already mapped to a non-default layer.")
# get an existing directory name
@ -1478,7 +1438,7 @@ class UFOWriter(object):
else:
# not caching this could be slightly expensive,
# but caching it will be cumbersome
existing = [d.lower() for d in list(self.layerContents.values())]
existing = {d.lower() for d in self.layerContents.values()}
if not isinstance(layerName, unicode):
try:
layerName = unicode(layerName)
@ -1526,14 +1486,14 @@ class UFOWriter(object):
if newLayerName in self.layerContents:
raise UFOLibError("A layer named %s already exists." % newLayerName)
# make sure the default layer doesn't already exist
if defaultLayer and DEFAULT_GLYPHS_DIRNAME in list(self.layerContents.values()):
if defaultLayer and DEFAULT_GLYPHS_DIRNAME in self.layerContents.values():
raise UFOLibError("A default layer already exists.")
# get the paths
oldDirectory = self._findDirectoryForLayerName(layerName)
if defaultLayer:
newDirectory = DEFAULT_GLYPHS_DIRNAME
else:
existing = [name.lower() for name in list(self.layerContents.values())]
existing = {name.lower() for name in self.layerContents.values()}
newDirectory = userNameToFileName(newLayerName, existing=existing, prefix="glyphs.")
# update the internal mapping
del self.layerContents[layerName]
@ -1614,14 +1574,11 @@ class UFOWriter(object):
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()
super(UFOWriter, self).close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self.close()
# just an alias, makes it more explicit
UFOReaderWriter = UFOWriter
# ----------------

View File

@ -34,6 +34,7 @@ from fontTools.ufoLib.validators import (
glyphLibValidator,
)
from fontTools.misc import etree
from fontTools.ufoLib import _UFOBaseIO
from fontTools.ufoLib.utils import integerTypes, numberTypes
@ -88,7 +89,7 @@ class Glyph(object):
# Glyph Set
# ---------
class GlyphSet(object):
class GlyphSet(_UFOBaseIO):
"""
GlyphSet manages a set of .glif files inside one directory.
@ -169,9 +170,6 @@ class GlyphSet(object):
self.rebuildContents()
# here we reuse the same methods from UFOReader/UFOWriter
from fontTools.ufoLib import _getPlist, _writePlist, _getFileModificationTime
def rebuildContents(self, validateRead=None):
"""
Rebuild the contents dict by loading contents.plist.
@ -308,7 +306,7 @@ class GlyphSet(object):
Raises KeyError if the glyphName is not in contents.plist.
"""
fileName = self.contents[glyphName]
return self._getFileModificationTime(fileName)
return self.getFileModificationTime(fileName)
# reading/writing API