Merge pull request #81 from anthrotype/ufo4

update "Add ZIP support to UFO"
This commit is contained in:
Cosimo Lupo 2017-07-21 17:16:10 +01:00 committed by GitHub
commit 879fce2e7c
59 changed files with 768 additions and 259 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@ dist/
.DS_Store .DS_Store
*.egg-info *.egg-info
*.py[cod] *.py[cod]
.eggs/
.tox/

5
.pyup.yml Normal file
View File

@ -0,0 +1,5 @@
# controls the frequency of updates (undocumented beta feature)
schedule: every week
# do not pin dependencies unless they have explicit version specifiers
pin: False

View File

@ -1,14 +1,53 @@
language: python language: python
sudo: required sudo: false
python: matrix:
- "2.7" include:
- "3.4" - python: 2.7
- "3.5" env: TOXENV=py27-fs
before_install: - python: 2.7
- git clone --depth=1 https://github.com/behdad/fonttools.git 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: install:
- cd fonttools; python setup.py install; cd .. - pip install --upgrade pip setuptools wheel tox
- python setup.py install script: tox
script: before_deploy:
- python -c "import fontTools" - pip wheel --no-deps -w dist .
- python setup.py test - python setup.py sdist
- export WHL=$(ls dist/ufoLib*.whl)
- export ZIP=$(ls dist/ufoLib*.zip)
deploy:
# deploy to Github Releases on tags
- provider: releases
api_key:
secure: ZwNCLnAvI2ftcH35Hk+jJ4cNWBRKqgjzyO3hdQT8Kbh2RyhKkpe7Dv+e3/ac2iaCkYPoGXGmrMVjNm6ny1HM5xuyXbIQuh34nmY1de4mdU4aTyWzGZ+E1SoajjDZVi5OhkqIm/FD/o5czIY8tv7YzwhVME/d5PHrBJ9vq91wu++Vx9hy/pocuS1YdBa53iFXDtF66zA8Jyw/qdallHEmN7ZMwasozW2X20Ry5rhFzgmx9oQ7R2v3jIUU0AMVJIY60Q2UwhI0XJIeTXQY4pxKgNU+0k4UQCRCCNbQgqcRvoVy8o5m9ofWtkVMmQM1c5UB+wD8IGJccVM608+/pbB3fHLk5TZOHKWRP1WyAWtQA29yDktPFdjLCYnfCt3oj10cTIs+Iphu7F9vAWt8g7fuyBYlqaqdjC2J2WcOesJzLAkalAPa/vat2T30xKNUmx6eV1Nu3X2BtuBy1gn6IcDZ+szySpls6ZM1oaOMmu2cRbwAQtszeKqIsJyx6atogeIeWTKsnC9QV1A/TR9Ku9L6YPF6bxmreW7DbKX0hoNmJL+VUiW8DealIgP/4tloI7VqRvbfL8AZp78RfWGK7ZzCK+QLI7Xt8cSu33HvRBWgWlsjIdA7Qw+vb8UtqVQPKOpPw+jraybSUBQ3bSjb4Zhi8sOpDhYnw1EXIZ4RdRqqVvE=
file:
- "${ZIP}"
- "${WHL}"
skip_cleanup: true
on:
repo: unified-font-object/ufoLib
tags: true
all_branches: true
condition: "$BUILD_DIST == true"
# deploy to PyPI on tags
- provider: pypi
server: https://upload.pypi.org/legacy/
on:
repo: unified-font-object/ufoLib
tags: true
all_branches: true
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=
distributions: sdist bdist_wheel

View File

@ -3,6 +3,7 @@ import shutil
from io import StringIO, BytesIO, open from io import StringIO, BytesIO, open
import codecs import codecs
from copy import deepcopy from copy import deepcopy
from fontTools.misc.py23 import basestring, unicode
from ufoLib.filesystem import FileSystem from ufoLib.filesystem import FileSystem
from ufoLib.glifLib import GlyphSet from ufoLib.glifLib import GlyphSet
from ufoLib.validators import * from ufoLib.validators import *
@ -41,11 +42,6 @@ fontinfo.plist values between the possible format versions.
convertFontInfoValueForAttributeFromVersion3ToVersion2 convertFontInfoValueForAttributeFromVersion3ToVersion2
""" """
try:
basestring
except NameError:
basestring = str
__all__ = [ __all__ = [
"makeUFOPath" "makeUFOPath"
"UFOLibError", "UFOLibError",
@ -61,6 +57,8 @@ __all__ = [
"convertFontInfoValueForAttributeFromVersion2ToVersion1" "convertFontInfoValueForAttributeFromVersion2ToVersion1"
] ]
__version__ = "2.1.1.dev0"
@ -173,6 +171,11 @@ class UFOReader(object):
self._upConvertedKerningData["groups"] = groups self._upConvertedKerningData["groups"] = groups
self._upConvertedKerningData["groupRenameMaps"] = conversionMaps self._upConvertedKerningData["groupRenameMaps"] = conversionMaps
# support methods
def getFileModificationTime(self, path):
return self.fileSystem.getFileModificationTime(path)
# metainfo.plist # metainfo.plist
def readMetaInfo(self): def readMetaInfo(self):
@ -431,13 +434,14 @@ class UFOReader(object):
if not self.fileSystem.isDirectory(IMAGES_DIRNAME): if not self.fileSystem.isDirectory(IMAGES_DIRNAME):
raise UFOLibError("The UFO contains an \"images\" file instead of a directory.") raise UFOLibError("The UFO contains an \"images\" file instead of a directory.")
result = [] result = []
for fileName in self.fileSystem.listDirectory(path): for fileName in self.fileSystem.listDirectory(IMAGES_DIRNAME):
if self.fileSystem.isDirectory(fileName): path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName)
if self.fileSystem.isDirectory(path):
# silently skip this as version control # silently skip this as version control
# systems often have hidden directories # systems often have hidden directories
continue continue
# XXX this is sending a path to the validator. that won't work in the abstracted filesystem. with self.fileSystem.open(path, mode='rb') as fp:
valid, error = pngValidator(path=p) valid, error = pngValidator(fileObj=fp)
if valid: if valid:
result.append(fileName) result.append(fileName)
return result return result
@ -927,9 +931,9 @@ class UFOWriter(object):
# not caching this could be slightly expensive, # not caching this could be slightly expensive,
# but caching it will be cumbersome # but caching it will be cumbersome
existing = [d.lower() for d in list(self.layerContents.values())] existing = [d.lower() for d in list(self.layerContents.values())]
if not isinstance(layerName, basestring): if not isinstance(layerName, unicode):
try: try:
layerName = str(layerName) layerName = unicode(layerName)
except UnicodeDecodeError: except UnicodeDecodeError:
raise UFOLibError("The specified layer name is not a Unicode string.") raise UFOLibError("The specified layer name is not a Unicode string.")
directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.") directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.")
@ -1024,8 +1028,8 @@ class UFOWriter(object):
""" """
if self._formatVersion < 3: if self._formatVersion < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion) raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
sourcePath = reader.joinPath(IMAGES_DIRNAME, sourceFileName) sourcePath = self.fileSystem.joinPath(IMAGES_DIRNAME, sourceFileName)
destPath = self.joinPath(IMAGES_DIRNAME, destFileName) destPath = self.fileSystem.joinPath(IMAGES_DIRNAME, destFileName)
self.copyFromReader(reader, sourcePath, destPath) self.copyFromReader(reader, sourcePath, destPath)
@ -1037,10 +1041,12 @@ def makeUFOPath(path):
""" """
Return a .ufo pathname. Return a .ufo pathname.
>>> makeUFOPath("/directory/something.ext") >>> makeUFOPath("directory/something.ext") == (
'/directory/something.ufo' ... os.path.join('directory', 'something.ufo'))
>>> makeUFOPath("/directory/something.another.thing.ext") True
'/directory/something.another.thing.ufo' >>> makeUFOPath("directory/something.another.thing.ext") == (
... os.path.join('directory', 'something.another.thing.ufo'))
True
""" """
dir, name = os.path.split(path) dir, name = os.path.split(path)
name = ".".join([".".join(name.split(".")[:-1]), "ufo"]) name = ".".join([".".join(name.split(".")[:-1]), "ufo"])

View File

@ -254,14 +254,9 @@ def test():
>>> groups == expected >>> groups == expected
True True
>>> kerningDict = {} >>> from .validators import kerningValidator
>>> for first, seconds in kerning.items(): >>> kerningValidator(kerning)
... for s, value in seconds.items(): (True, None)
... key = (first, s)
... kerningDict[key] = value
>>> from validators import kerningValidator
>>> kerningValidator(kerningDict, groups)
(True, [])
Mixture of known prefixes and groups without prefixes. Mixture of known prefixes and groups without prefixes.

View File

@ -2,11 +2,9 @@
User name to file name conversion. User name to file name conversion.
This was taken form the UFO 3 spec. This was taken form the UFO 3 spec.
""" """
from __future__ import unicode_literals
from fontTools.misc.py23 import basestring, unicode
try:
basestring
except NameError:
basestring = str
illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ") illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ")
illegalCharacters += [chr(i) for i in range(1, 32)] illegalCharacters += [chr(i) for i in range(1, 32)]
@ -25,53 +23,54 @@ def userNameToFileName(userName, existing=[], prefix="", suffix=""):
existing should be a case-insensitive list existing should be a case-insensitive list
of all existing file names. of all existing file names.
>>> userNameToFileName(u"a") >>> userNameToFileName("a") == "a"
u'a' True
>>> userNameToFileName(u"A") >>> userNameToFileName("A") == "A_"
u'A_' True
>>> userNameToFileName(u"AE") >>> userNameToFileName("AE") == "A_E_"
u'A_E_' True
>>> userNameToFileName(u"Ae") >>> userNameToFileName("Ae") == "A_e"
u'A_e' True
>>> userNameToFileName(u"ae") >>> userNameToFileName("ae") == "ae"
u'ae' True
>>> userNameToFileName(u"aE") >>> userNameToFileName("aE") == "aE_"
u'aE_' True
>>> userNameToFileName(u"a.alt") >>> userNameToFileName("a.alt") == "a.alt"
u'a.alt' True
>>> userNameToFileName(u"A.alt") >>> userNameToFileName("A.alt") == "A_.alt"
u'A_.alt' True
>>> userNameToFileName(u"A.Alt") >>> userNameToFileName("A.Alt") == "A_.A_lt"
u'A_.A_lt' True
>>> userNameToFileName(u"A.aLt") >>> userNameToFileName("A.aLt") == "A_.aL_t"
u'A_.aL_t' True
>>> userNameToFileName(u"A.alT") >>> userNameToFileName(u"A.alT") == "A_.alT_"
u'A_.alT_' True
>>> userNameToFileName(u"T_H") >>> userNameToFileName("T_H") == "T__H_"
u'T__H_' True
>>> userNameToFileName(u"T_h") >>> userNameToFileName("T_h") == "T__h"
u'T__h' True
>>> userNameToFileName(u"t_h") >>> userNameToFileName("t_h") == "t_h"
u't_h' True
>>> userNameToFileName(u"F_F_I") >>> userNameToFileName("F_F_I") == "F__F__I_"
u'F__F__I_' True
>>> userNameToFileName(u"f_f_i") >>> userNameToFileName("f_f_i") == "f_f_i"
u'f_f_i' True
>>> userNameToFileName(u"Aacute_V.swash") >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash"
u'A_acute_V_.swash' True
>>> userNameToFileName(u".notdef") >>> userNameToFileName(".notdef") == "_notdef"
u'_notdef' True
>>> userNameToFileName(u"con") >>> userNameToFileName("con") == "_con"
u'_con' True
>>> userNameToFileName(u"CON") >>> userNameToFileName("CON") == "C_O_N_"
u'C_O_N_' True
>>> userNameToFileName(u"con.alt") >>> userNameToFileName("con.alt") == "_con.alt"
u'_con.alt' True
>>> userNameToFileName(u"alt.con") >>> userNameToFileName("alt.con") == "alt._con"
u'alt._con' True
""" """
# the incoming name must be a unicode string # the incoming name must be a unicode string
assert isinstance(userName, basestring), "The value for userName must be a unicode string." if not isinstance(userName, unicode):
raise ValueError("The value for userName must be a unicode string.")
# establish the prefix and suffix lengths # establish the prefix and suffix lengths
prefixLength = len(prefix) prefixLength = len(prefix)
suffixLength = len(suffix) suffixLength = len(suffix)
@ -118,20 +117,23 @@ def handleClash1(userName, existing=[], prefix="", suffix=""):
>>> e = list(existing) >>> e = list(existing)
>>> handleClash1(userName="A" * 5, existing=e, >>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) ... prefix=prefix, suffix=suffix) == (
'00000.AAAAA000000000000001.0000000000' ... '00000.AAAAA000000000000001.0000000000')
True
>>> e = list(existing) >>> e = list(existing)
>>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e, >>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) ... prefix=prefix, suffix=suffix) == (
'00000.AAAAA000000000000002.0000000000' ... '00000.AAAAA000000000000002.0000000000')
True
>>> e = list(existing) >>> e = list(existing)
>>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e, >>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) ... prefix=prefix, suffix=suffix) == (
'00000.AAAAA000000000000001.0000000000' ... '00000.AAAAA000000000000001.0000000000')
True
""" """
# if the prefix length + user name length + suffix length + 15 is at # if the prefix length + user name length + suffix length + 15 is at
# or past the maximum length, silce 15 characters off of the user name # or past the maximum length, silce 15 characters off of the user name
@ -170,18 +172,21 @@ def handleClash2(existing=[], prefix="", suffix=""):
>>> existing = [prefix + str(i) + suffix for i in range(100)] >>> existing = [prefix + str(i) + suffix for i in range(100)]
>>> e = list(existing) >>> e = list(existing)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
'00000.100.0000000000' ... '00000.100.0000000000')
True
>>> e = list(existing) >>> e = list(existing)
>>> e.remove(prefix + "1" + suffix) >>> e.remove(prefix + "1" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
'00000.1.0000000000' ... '00000.1.0000000000')
True
>>> e = list(existing) >>> e = list(existing)
>>> e.remove(prefix + "2" + suffix) >>> e.remove(prefix + "2" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
'00000.2.0000000000' ... '00000.2.0000000000')
True
""" """
# calculate the longest possible string # calculate the longest possible string
maxLength = maxFileNameLength - len(prefix) - len(suffix) maxLength = maxFileNameLength - len(prefix) - len(suffix)

View File

@ -1,13 +1,15 @@
import os import os
import sys
import shutil import shutil
from io import StringIO, BytesIO, open from io import StringIO, BytesIO, open
import zipfile import zipfile
from fontTools.misc.py23 import tounicode
haveFS = False haveFS = False
try: try:
import fs import fs
from fs.osfs import OSFS from fs.osfs import OSFS
from fs.zipfs import ZipFS, ZipOpenError from fs.zipfs import ZipFS
haveFS = True haveFS = True
except ImportError: except ImportError:
pass pass
@ -20,6 +22,9 @@ try:
except NameError: except NameError:
basestring = str basestring = str
_SYS_FS_ENCODING = sys.getfilesystemencoding()
def sniffFileStructure(path): def sniffFileStructure(path):
if zipfile.is_zipfile(path): if zipfile.is_zipfile(path):
return "zip" return "zip"
@ -32,7 +37,7 @@ class FileSystem(object):
def __init__(self, path, mode="r", structure=None): 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. mode can be r or w.
@ -41,12 +46,12 @@ class FileSystem(object):
package: package structure package: package structure
zip: zipped package zip: zipped package
mode and structure are both ignored if a mode and structure are both ignored if a FileSystem
fs file system object is given for path. object is given for path.
""" """
self._root = None self._root = None
self._path = "<data stream>"
if isinstance(path, basestring): if isinstance(path, basestring):
path = tounicode(path, encoding=_SYS_FS_ENCODING)
self._path = path self._path = path
if mode == "w": if mode == "w":
if os.path.exists(path): if os.path.exists(path):
@ -58,7 +63,7 @@ class FileSystem(object):
structure = existingStructure structure = existingStructure
elif mode == "r": elif mode == "r":
if not os.path.exists(path): 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) structure = sniffFileStructure(path)
if structure == "package": if structure == "package":
if mode == "w" and not os.path.exists(path): if mode == "w" and not os.path.exists(path):
@ -67,14 +72,22 @@ class FileSystem(object):
elif structure == "zip": elif structure == "zip":
if not haveFS: if not haveFS:
raise UFOLibError("The fs module is required for reading and writing UFO ZIP.") 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("") roots = path.listdir("")
if not roots: if not roots:
self._root = "contents" self._root = u"contents"
path.makedir(self._root)
elif len(roots) > 1: elif len(roots) > 1:
raise UFOLibError("The UFO contains more than one root.") raise UFOLibError("The UFO contains more than one root.")
else: else:
self._root = roots[0] 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 self._fs = path
def close(self): def close(self):
@ -94,6 +107,7 @@ class FileSystem(object):
""" """
def _fsRootPath(self, path): def _fsRootPath(self, path):
path = tounicode(path, encoding=_SYS_FS_ENCODING)
if self._root is None: if self._root is None:
return path return path
return self.joinPath(self._root, path) return self.joinPath(self._root, path)
@ -119,18 +133,17 @@ class FileSystem(object):
path = self._fsRootPath(path) path = self._fsRootPath(path)
self._fs.makedir(path) self._fs.makedir(path)
def _fsRemoveDirectory(self, path): def _fsRemoveTree(self, path):
path = self._fsRootPath(path) path = self._fsRootPath(path)
self._fs.removedir(path, force=True) self._fs.removetree(path)
def _fsMove(self, path1, path2): def _fsMove(self, path1, path2):
if self.isDirectory(path1):
meth = self._fs.movedir
else:
meth = self._fs.move
path1 = self._fsRootPath(path1) path1 = self._fsRootPath(path1)
path2 = self._fsRootPath(path2) 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): def _fsExists(self, path):
path = self._fsRootPath(path) path = self._fsRootPath(path)
@ -147,23 +160,35 @@ class FileSystem(object):
def _fsGetFileModificationTime(self, path): def _fsGetFileModificationTime(self, path):
path = self._fsRootPath(path) path = self._fsRootPath(path)
info = self._fs.getinfo(path) info = self._fs.getinfo(path)
return info["modified_time"] return info.modified
# ----------------- # -----------------
# Path Manipulation # Path Manipulation
# ----------------- # -----------------
def joinPath(self, *parts): def joinPath(self, *parts):
if haveFS:
return fs.path.join(*parts) return fs.path.join(*parts)
else:
return os.path.join(*parts)
def splitPath(self, path): def splitPath(self, path):
if haveFS:
return fs.path.split(path) return fs.path.split(path)
else:
return os.path.split(path)
def directoryName(self, path): def directoryName(self, path):
if haveFS:
return fs.path.dirname(path) return fs.path.dirname(path)
else:
return os.path.dirname(path)
def relativePath(self, path, start): def relativePath(self, path, start):
if haveFS:
return fs.relativefrom(path, start) return fs.relativefrom(path, start)
else:
return os.path.relpath(path, start)
# --------- # ---------
# Existence # Existence
@ -179,8 +204,9 @@ class FileSystem(object):
return self._listDirectory(path, recurse=recurse, relativeTo=path) return self._listDirectory(path, recurse=recurse, relativeTo=path)
def _listDirectory(self, path, recurse=False, relativeTo=None, depth=0, maxDepth=100): def _listDirectory(self, path, recurse=False, relativeTo=None, depth=0, maxDepth=100):
if not relativeTo.endswith("/"): sep = os.sep
relativeTo += "/" if not relativeTo.endswith(sep):
relativeTo += sep
if depth > maxDepth: if depth > maxDepth:
raise UFOLibError("Maximum recusion depth reached.") raise UFOLibError("Maximum recusion depth reached.")
result = [] result = []
@ -190,6 +216,9 @@ class FileSystem(object):
result += self._listDirectory(p, recurse=True, relativeTo=relativeTo, depth=depth+1, maxDepth=maxDepth) result += self._listDirectory(p, recurse=True, relativeTo=relativeTo, depth=depth+1, maxDepth=maxDepth)
else: else:
p = p[len(relativeTo):] p = p[len(relativeTo):]
if sep != "/":
# replace '\\' with '/'
p = p.replace(sep, "/")
result.append(p) result.append(p)
return result return result
@ -319,7 +348,7 @@ class FileSystem(object):
raise UFOLibError("The file %s does not exist." % path) raise UFOLibError("The file %s does not exist." % path)
else: else:
if self.isDirectory(path): if self.isDirectory(path):
self._fsRemoveDirectory(path) self._fsRemoveTree(path)
else: else:
self._fsRemove(path) self._fsRemove(path)
directory = self.directoryName(path) directory = self.directoryName(path)
@ -330,7 +359,7 @@ class FileSystem(object):
if not self.exists(directory): if not self.exists(directory):
return return
if not len(self._fsListDirectory(directory)): if not len(self._fsListDirectory(directory)):
self._fsRemoveDirectory(directory) self._fsRemoveTree(directory)
else: else:
return return
directory = self.directoryName(directory) directory = self.directoryName(directory)
@ -372,8 +401,8 @@ class FileSystem(object):
try: try:
with self.open(path, "rb") as f: with self.open(path, "rb") as f:
return readPlist(f) return readPlist(f)
except: except Exception as e:
raise UFOLibError("The file %s could not be read." % path) raise UFOLibError("The file %s could not be read: %s" % (path, str(e)))
def writePlist(self, path, obj): def writePlist(self, path, obj):
""" """
@ -409,18 +438,6 @@ class _NOFS(object):
def _absPath(self, path): def _absPath(self, path):
return os.path.join(self._path, 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): def close(self):
pass pass
@ -436,7 +453,7 @@ class _NOFS(object):
path = self._absPath(path) path = self._absPath(path)
os.mkdir(path) os.mkdir(path)
def removedir(self, path): def removetree(self, path):
path = self._absPath(path) path = self._absPath(path)
shutil.rmtree(path) shutil.rmtree(path)
@ -445,9 +462,34 @@ class _NOFS(object):
path2 = self._absPath(path2) path2 = self._absPath(path2)
os.move(path1, path2) os.move(path1, path2)
def movedir(self, path1, path2): def movedir(self, path1, path2, create=False):
path1 = self._absPath(path1) path1 = self._absPath(path1)
path2 = self._absPath(path2) 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) shutil.move(path1, path2)
def exists(self, path): def exists(self, path):
@ -463,10 +505,11 @@ class _NOFS(object):
return os.listdir(path) return os.listdir(path)
def getinfo(self, path): def getinfo(self, path):
from fontTools.misc.py23 import SimpleNamespace
path = self._absPath(path) path = self._absPath(path)
stat = os.stat(path) stat = os.stat(path)
info = dict( info = SimpleNamespace(
modified_time=stat.st_mtime modified=stat.st_mtime
) )
return info return info

View File

@ -15,7 +15,7 @@ from __future__ import unicode_literals
import os import os
from io import BytesIO, open from io import BytesIO, open
from warnings import warn from warnings import warn
from fontTools.misc.py23 import tobytes from fontTools.misc.py23 import tobytes, unicode
from ufoLib.filesystem import FileSystem from ufoLib.filesystem import FileSystem
from ufoLib.plistlib import PlistWriter, readPlist, writePlist from ufoLib.plistlib import PlistWriter, readPlist, writePlist
from ufoLib.plistFromETree import readPlistFromTree from ufoLib.plistFromETree import readPlistFromTree
@ -207,7 +207,7 @@ class GlyphSet(object):
value = getattr(info, attr) value = getattr(info, attr)
except AttributeError: except AttributeError:
raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr) raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
if value is None: if value is None or (attr == 'lib' and not value):
continue continue
infoData[attr] = value infoData[attr] = value
# validate # validate
@ -452,9 +452,9 @@ def glyphNameToFileName(glyphName, glyphSet):
existing = [name.lower() for name in list(glyphSet.contents.values())] existing = [name.lower() for name in list(glyphSet.contents.values())]
else: else:
existing = [] existing = []
if not isinstance(glyphName, basestring): if not isinstance(glyphName, unicode):
try: try:
new = str(glyphName) new = unicode(glyphName)
glyphName = new glyphName = new
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
@ -528,10 +528,10 @@ def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, writer=
""" """
if writer is None: if writer is None:
try: try:
from xmlWriter import XMLWriter from fontTools.misc.xmlWriter import XMLWriter
except ImportError: except ImportError:
# try the other location # try the other location
from fontTools.misc.xmlWriter import XMLWriter from xmlWriter import XMLWriter
aFile = BytesIO() aFile = BytesIO()
writer = XMLWriter(aFile, encoding="UTF-8") writer = XMLWriter(aFile, encoding="UTF-8")
else: else:
@ -803,7 +803,7 @@ def _glifTreeFromFile(aFile):
root = ElementTree.parse(aFile).getroot() root = ElementTree.parse(aFile).getroot()
if root.tag != "glyph": if root.tag != "glyph":
raise GlifLibError("The GLIF is not properly formatted.") raise GlifLibError("The GLIF is not properly formatted.")
if root.text.strip() != '': if root.text and root.text.strip() != '':
raise GlifLibError("Invalid GLIF structure.") raise GlifLibError("Invalid GLIF structure.")
return root return root
@ -811,7 +811,7 @@ def _glifTreeFromString(aString):
root = ElementTree.fromstring(aString) root = ElementTree.fromstring(aString)
if root.tag != "glyph": if root.tag != "glyph":
raise GlifLibError("The GLIF is not properly formatted.") raise GlifLibError("The GLIF is not properly formatted.")
if root.text.strip() != '': if root.text and root.text.strip() != '':
raise GlifLibError("Invalid GLIF structure.") raise GlifLibError("Invalid GLIF structure.")
return root return root
@ -847,7 +847,7 @@ def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None):
raise GlifLibError("The outline element occurs more than once.") raise GlifLibError("The outline element occurs more than once.")
if element.attrib: if element.attrib:
raise GlifLibError("The outline element contains unknown attributes.") raise GlifLibError("The outline element contains unknown attributes.")
if element.text.strip() != '': if element.text and element.text.strip() != '':
raise GlifLibError("Invalid outline structure.") raise GlifLibError("Invalid outline structure.")
haveSeenOutline = True haveSeenOutline = True
buildOutlineFormat1(glyphObject, pointPen, element) buildOutlineFormat1(glyphObject, pointPen, element)
@ -897,7 +897,7 @@ def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None):
raise GlifLibError("The outline element occurs more than once.") raise GlifLibError("The outline element occurs more than once.")
if element.attrib: if element.attrib:
raise GlifLibError("The outline element contains unknown attributes.") raise GlifLibError("The outline element contains unknown attributes.")
if element.text.strip() != '': if element.text and element.text.strip() != '':
raise GlifLibError("Invalid outline structure.") raise GlifLibError("Invalid outline structure.")
haveSeenOutline = True haveSeenOutline = True
if pointPen is not None: if pointPen is not None:
@ -1014,11 +1014,6 @@ pointTypeOptions = set(["move", "line", "offcurve", "curve", "qcurve"])
# format 1 # format 1
componentAttributesFormat1 = set(["base", "xScale", "xyScale", "yxScale", "yScale", "xOffset", "yOffset"])
pointAttributesFormat1 = set(["x", "y", "type", "smooth", "name"])
pointSmoothOptions = set(("no", "yes"))
pointTypeOptions = set(["move", "line", "offcurve", "curve", "qcurve"])
def buildOutlineFormat1(glyphObject, pen, outline): def buildOutlineFormat1(glyphObject, pen, outline):
anchors = [] anchors = []
for element in outline: for element in outline:
@ -1286,7 +1281,7 @@ def _number(s):
1 1
>>> _number("1.0") >>> _number("1.0")
1.0 1.0
>>> _number("a") >>> _number("a") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last): Traceback (most recent call last):
... ...
GlifLibError: Could not convert a to an int or float. GlifLibError: Could not convert a to an int or float.

View File

@ -23,7 +23,7 @@ class AbstractPointPen(object):
Baseclass for all PointPens. Baseclass for all PointPens.
""" """
def beginPath(self): def beginPath(self, identifier=None, **kwargs):
"""Start a new sub path.""" """Start a new sub path."""
raise NotImplementedError raise NotImplementedError
@ -31,11 +31,13 @@ class AbstractPointPen(object):
"""End the current sub path.""" """End the current sub path."""
raise NotImplementedError raise NotImplementedError
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): def addPoint(self, pt, segmentType=None, smooth=False, name=None,
identifier=None, **kwargs):
"""Add a point to the current sub path.""" """Add a point to the current sub path."""
raise NotImplementedError raise NotImplementedError
def addComponent(self, baseGlyphName, transformation): def addComponent(self, baseGlyphName, transformation, identifier=None,
**kwargs):
"""Add a sub glyph.""" """Add a sub glyph."""
raise NotImplementedError raise NotImplementedError
@ -190,7 +192,7 @@ class PointToSegmentPen(BasePointToSegmentPen):
else: else:
pen.endPath() pen.endPath()
def addComponent(self, glyphName, transform): def addComponent(self, glyphName, transform, **kwargs):
self.pen.addComponent(glyphName, transform) self.pen.addComponent(glyphName, transform)
@ -320,3 +322,85 @@ class GuessSmoothPointPen(AbstractPointPen):
def addComponent(self, glyphName, transformation): def addComponent(self, glyphName, transformation):
assert self._points is None assert self._points is None
self._outPen.addComponent(glyphName, transformation) self._outPen.addComponent(glyphName, transformation)
class ReverseContourPointPen(AbstractPointPen):
"""
This is a PointPen that passes outline data to another PointPen, but
reversing the winding direction of all contours. Components are simply
passed through unchanged.
Closed contours are reversed in such a way that the first point remains
the first point.
"""
def __init__(self, outputPointPen):
self.pen = outputPointPen
# a place to store the points for the current sub path
self.currentContour = None
def _flushContour(self):
pen = self.pen
contour = self.currentContour
if not contour:
pen.beginPath(identifier=self.currentContourIdentifier)
pen.endPath()
return
closed = contour[0][1] != "move"
if not closed:
lastSegmentType = "move"
else:
# Remove the first point and insert it at the end. When
# the list of points gets reversed, this point will then
# again be at the start. In other words, the following
# will hold:
# for N in range(len(originalContour)):
# originalContour[N] == reversedContour[-N]
contour.append(contour.pop(0))
# Find the first on-curve point.
firstOnCurve = None
for i in range(len(contour)):
if contour[i][1] is not None:
firstOnCurve = i
break
if firstOnCurve is None:
# There are no on-curve points, be basically have to
# do nothing but contour.reverse().
lastSegmentType = None
else:
lastSegmentType = contour[firstOnCurve][1]
contour.reverse()
if not closed:
# Open paths must start with a move, so we simply dump
# all off-curve points leading up to the first on-curve.
while contour[0][1] is None:
contour.pop(0)
pen.beginPath(identifier=self.currentContourIdentifier)
for pt, nextSegmentType, smooth, name, kwargs in contour:
if nextSegmentType is not None:
segmentType = lastSegmentType
lastSegmentType = nextSegmentType
else:
segmentType = None
pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs)
pen.endPath()
def beginPath(self, identifier=None, **kwargs):
assert self.currentContour is None
self.currentContour = []
self.currentContourIdentifier = identifier
self.onCurve = []
def endPath(self):
assert self.currentContour is not None
self._flushContour()
self.currentContour = None
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self.currentContour.append((pt, segmentType, smooth, name, kwargs))
def addComponent(self, glyphName, transform, identifier=None, **kwargs):
assert self.currentContour is None
self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)

View File

@ -12,9 +12,8 @@ except NameError:
def getDemoFontPath(): def getDemoFontPath():
"""Return the path to Data/DemoFont.ufo/.""" """Return the path to Data/DemoFont.ufo/."""
import ufoLib testdata = os.path.join(os.path.dirname(__file__), "testdata")
root = os.path.dirname(os.path.dirname(os.path.dirname(ufoLib.__file__))) return os.path.join(testdata, "DemoFont.ufo")
return os.path.join(root, "Data", "DemoFont.ufo")
def getDemoFontGlyphSetPath(): def getDemoFontGlyphSetPath():

View File

@ -4140,19 +4140,15 @@ class UFO3WriteLayersTestCase(unittest.TestCase):
class UFO3ReadDataTestCase(unittest.TestCase): class UFO3ReadDataTestCase(unittest.TestCase):
def getFontPath(self): def getFontPath(self):
import ufoLib testdata = os.path.join(os.path.dirname(__file__), "testdata")
path = os.path.dirname(ufoLib.__file__) return os.path.join(testdata, "UFO3-Read Data.ufo")
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): def testUFOReaderDataDirectoryListing(self):
reader = UFOReader(self.getFontPath()) reader = UFOReader(self.getFontPath())
found = reader.getDataDirectoryListing() found = reader.getDataDirectoryListing()
expected = [ expected = [
'org.unifiedfontobject.directory%(s)sbar%(s)slol.txt' % {'s': os.sep}, 'org.unifiedfontobject.directory/bar/lol.txt',
'org.unifiedfontobject.directory%(s)sfoo.txt' % {'s': os.sep}, 'org.unifiedfontobject.directory/foo.txt',
'org.unifiedfontobject.file.txt' 'org.unifiedfontobject.file.txt'
] ]
self.assertEqual(set(found), set(expected)) self.assertEqual(set(found), set(expected))

View File

@ -4,7 +4,7 @@ import shutil
import unittest import unittest
import tempfile import tempfile
from io import open from io import open
from ufoLib import convertUFOFormatVersion1ToFormatVersion2, UFOReader, UFOWriter from ufoLib import UFOReader, UFOWriter
from ufoLib.plistlib import readPlist, writePlist from ufoLib.plistlib import readPlist, writePlist
from ufoLib.test.testSupport import expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion from ufoLib.test.testSupport import expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion
@ -30,12 +30,8 @@ class ConversionFunctionsTestCase(unittest.TestCase):
shutil.rmtree(path) shutil.rmtree(path)
def getFontPath(self, fileName): def getFontPath(self, fileName):
import ufoLib testdata = os.path.join(os.path.dirname(__file__), "testdata")
path = os.path.dirname(ufoLib.__file__) return os.path.join(testdata, fileName)
path = os.path.dirname(path)
path = os.path.dirname(path)
path = os.path.join(path, "TestData", fileName)
return path
def compareFileStructures(self, path1, path2, expectedInfoData, testFeatures): def compareFileStructures(self, path1, path2, expectedInfoData, testFeatures):
# result # result
@ -120,13 +116,6 @@ class ConversionFunctionsTestCase(unittest.TestCase):
data2 = readPlist(f) data2 = readPlist(f)
self.assertEqual(data1, data2) 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 # kerning up conversion

View File

@ -0,0 +1,98 @@
from __future__ import unicode_literals
import unittest
from ufoLib.filenames import userNameToFileName, handleClash1, handleClash2
class TestFilenames(unittest.TestCase):
def test_userNameToFileName(self):
self.assertEqual(userNameToFileName("a"), "a")
self.assertEqual(userNameToFileName("A"), "A_")
self.assertEqual(userNameToFileName("AE"), "A_E_")
self.assertEqual(userNameToFileName("Ae"), "A_e")
self.assertEqual(userNameToFileName("ae"), "ae")
self.assertEqual(userNameToFileName("aE"), "aE_")
self.assertEqual(userNameToFileName("a.alt"), "a.alt")
self.assertEqual(userNameToFileName("A.alt"), "A_.alt")
self.assertEqual(userNameToFileName("A.Alt"), "A_.A_lt")
self.assertEqual(userNameToFileName("A.aLt"), "A_.aL_t")
self.assertEqual(userNameToFileName("A.alT"), "A_.alT_")
self.assertEqual(userNameToFileName("T_H"), "T__H_")
self.assertEqual(userNameToFileName("T_h"), "T__h")
self.assertEqual(userNameToFileName("t_h"), "t_h")
self.assertEqual(userNameToFileName("F_F_I"), "F__F__I_")
self.assertEqual(userNameToFileName("f_f_i"), "f_f_i")
self.assertEqual(userNameToFileName("Aacute_V.swash"),
"A_acute_V_.swash")
self.assertEqual(userNameToFileName(".notdef"), "_notdef")
self.assertEqual(userNameToFileName("con"), "_con")
self.assertEqual(userNameToFileName("CON"), "C_O_N_")
self.assertEqual(userNameToFileName("con.alt"), "_con.alt")
self.assertEqual(userNameToFileName("alt.con"), "alt._con")
def test_userNameToFileName_ValueError(self):
with self.assertRaises(ValueError):
userNameToFileName(b"a")
with self.assertRaises(ValueError):
userNameToFileName({"a"})
with self.assertRaises(ValueError):
userNameToFileName(("a",))
with self.assertRaises(ValueError):
userNameToFileName(["a"])
with self.assertRaises(ValueError):
userNameToFileName(["a"])
with self.assertRaises(ValueError):
userNameToFileName(b"\xd8\x00")
def test_handleClash1(self):
prefix = ("0" * 5) + "."
suffix = "." + ("0" * 10)
existing = ["a" * 5]
e = list(existing)
self.assertEqual(
handleClash1(userName="A" * 5, existing=e, prefix=prefix,
suffix=suffix),
'00000.AAAAA000000000000001.0000000000'
)
e = list(existing)
e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
self.assertEqual(
handleClash1(userName="A" * 5, existing=e, prefix=prefix,
suffix=suffix),
'00000.AAAAA000000000000002.0000000000'
)
e = list(existing)
e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
self.assertEqual(
handleClash1(userName="A" * 5, existing=e, prefix=prefix,
suffix=suffix),
'00000.AAAAA000000000000001.0000000000'
)
def test_handleClash2(self):
prefix = ("0" * 5) + "."
suffix = "." + ("0" * 10)
existing = [prefix + str(i) + suffix for i in range(100)]
e = list(existing)
self.assertEqual(
handleClash2(existing=e, prefix=prefix, suffix=suffix),
'00000.100.0000000000'
)
e = list(existing)
e.remove(prefix + "1" + suffix)
self.assertEqual(
handleClash2(existing=e, prefix=prefix, suffix=suffix),
'00000.1.0000000000'
)
e = list(existing)
e.remove(prefix + "2" + suffix)
self.assertEqual(
handleClash2(existing=e, prefix=prefix, suffix=suffix),
'00000.2.0000000000'
)

View File

@ -4,6 +4,11 @@ import os
import calendar import calendar
from io import open from io import open
try:
from collections.abc import Mapping # python >= 3.3
except ImportError:
from collections import Mapping
# ------- # -------
# Python 2 or 3 # Python 2 or 3
# ------- # -------
@ -21,7 +26,7 @@ def isDictEnough(value):
Some objects will likely come in that aren't Some objects will likely come in that aren't
dicts but are dict-ish enough. dicts but are dict-ish enough.
""" """
if isinstance(value, dict): if isinstance(value, Mapping):
return True return True
attrs = ("keys", "values", "items") attrs = ("keys", "values", "items")
for attr in attrs: for attr in attrs:
@ -75,7 +80,7 @@ def genericDictValidator(value, prototype):
Generic. (Added at version 3.) Generic. (Added at version 3.)
""" """
# not a dict # not a dict
if not isinstance(value, dict): if not isinstance(value, Mapping):
return False return False
# missing required keys # missing required keys
for key, (typ, required) in list(prototype.items()): for key, (typ, required) in list(prototype.items()):
@ -907,12 +912,12 @@ def kerningValidator(data):
(False, 'The kerning data is not in the correct format.') (False, 'The kerning data is not in the correct format.')
""" """
bogusFormatMessage = "The kerning data is not in the correct format." bogusFormatMessage = "The kerning data is not in the correct format."
if not isinstance(data, dict): if not isinstance(data, Mapping):
return False, bogusFormatMessage return False, bogusFormatMessage
for first, secondDict in list(data.items()): for first, secondDict in list(data.items()):
if not isinstance(first, basestring): if not isinstance(first, basestring):
return False, bogusFormatMessage return False, bogusFormatMessage
elif not isinstance(secondDict, dict): elif not isinstance(secondDict, Mapping):
return False, bogusFormatMessage return False, bogusFormatMessage
for second, value in list(secondDict.items()): for second, value in list(secondDict.items()):
if not isinstance(second, basestring): if not isinstance(second, basestring):

8
MANIFEST.in Normal file
View File

@ -0,0 +1,8 @@
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
include requirements.txt tox.ini

View File

@ -1,6 +1,7 @@
[![Build Status](https://api.travis-ci.org/unified-font-object/ufoLib.svg)](https://travis-ci.org/unified-font-object/ufoLib) [![Build Status](https://api.travis-ci.org/unified-font-object/ufoLib.svg)](https://travis-ci.org/unified-font-object/ufoLib)
[![AppVeyor Status](https://ci.appveyor.com/api/projects/status/github/unified-font-object/ufoLib?svg=true)](https://ci.appveyor.com/project/adrientetar/ufolib) [![AppVeyor Status](https://ci.appveyor.com/api/projects/status/github/unified-font-object/ufoLib?svg=true)](https://ci.appveyor.com/project/adrientetar/ufolib)
![Python Versions](https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5-blue.svg) ![Python Versions](https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5-blue.svg)
[![PyPI](https://img.shields.io/pypi/v/ufoLib.svg)](https://pypi.org/project/ufoLib/)
ufoLib ufoLib
------ ------

View File

@ -4,26 +4,74 @@ environment:
- PYTHON: "C:\\Python27" - PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7.x" PYTHON_VERSION: "2.7.x"
PYTHON_ARCH: "32" PYTHON_ARCH: "32"
TOXENV: "py27-fs"
TOXPYTHON: "C:\\Python27\\python.exe"
- PYTHON: "C:\\Python34" - PYTHON: "C:\\Python27"
PYTHON_VERSION: "3.4.0" PYTHON_VERSION: "2.7.x"
PYTHON_ARCH: "32" PYTHON_ARCH: "32"
TOXENV: "py27-nofs"
TOXPYTHON: "C:\\Python27\\python.exe"
- PYTHON: "C:\\Python35" - PYTHON: "C:\\Python35"
PYTHON_VERSION: "3.5.0" PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "32" PYTHON_ARCH: "32"
TOXENV: "py35-fs"
TOXPYTHON: "C:\\Python35\\python.exe"
- PYTHON: "C:\\Python35"
PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "32"
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: "C:\\Python27-x64"
PYTHON_VERSION: "2.7.x" PYTHON_VERSION: "2.7.x"
PYTHON_ARCH: "64" PYTHON_ARCH: "64"
TOXENV: "py27-fs"
TOXPYTHON: "C:\\Python27-x64\\python.exe"
- PYTHON: "C:\\Python34-x64" - PYTHON: "C:\\Python27-x64"
PYTHON_VERSION: "3.4.x" PYTHON_VERSION: "2.7.x"
PYTHON_ARCH: "64" PYTHON_ARCH: "64"
TOXENV: "py27-nofs"
TOXPYTHON: "C:\\Python27-x64\\python.exe"
- PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5.x" PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "64" PYTHON_ARCH: "64"
TOXENV: "py35-fs"
TOXPYTHON: "C:\\Python35-x64\\python.exe"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "64"
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: init:
- "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%"
@ -43,24 +91,13 @@ install:
# upgrade pip to avoid out-of-date warnings # upgrade pip to avoid out-of-date warnings
- "pip install --disable-pip-version-check --user --upgrade pip" - "pip install --disable-pip-version-check --user --upgrade pip"
# install wheel to build compiled packages # install/upgrade setuptools and wheel to build packages
# - "pip install --upgrade wheel" - "pip install --upgrade setuptools wheel"
# install requirements # install tox to run test suite in a virtual environment
- "pip install git+https://github.com/behdad/fonttools.git" - "pip install -U tox"
# install
- "python setup.py install"
build: false build: false
test_script: test_script:
- "python setup.py test" - "tox"
# after_test:
# # if tests are successful, create binary packages for the project
# - "pip wheel -w dist ."
# artifacts:
# # archive the generated packages in the ci.appveyor.com build report
# - path: dist\*

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
fonttools==3.13.1

50
setup.cfg Normal file
View File

@ -0,0 +1,50 @@
[bumpversion]
current_version = 2.1.1.dev0
commit = True
tag = False
tag_name = v{new_version}
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<dev>\d+))?
serialize =
{major}.{minor}.{patch}.{release}{dev}
{major}.{minor}.{patch}
[bumpversion:part:release]
optional_value = final
values =
dev
final
[bumpversion:part:dev]
[bumpversion:file:Lib/ufoLib/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
[bumpversion:file:setup.py]
search = version="{current_version}"
replace = version="{new_version}"
[wheel]
universal = 1
[sdist]
formats = zip
[aliases]
test = pytest
[metadata]
license_file = LICENSE.txt
[tool:pytest]
minversion = 3.0.2
testpaths =
Lib/ufoLib
addopts =
# run py.test in verbose mode
-v
# show extra test summary info
-r a
# run doctests in all .py modules
--doctest-modules

204
setup.py
View File

@ -1,46 +1,177 @@
#! /usr/bin/env python #! /usr/bin/env python
import sys
from setuptools import setup, find_packages, Command
from distutils import log
import os, sys
try: class bump_version(Command):
from setuptools import setup
extra_kwargs = {
"test_suite": "ufoLib.test"
}
except ImportError:
from distutils.core import setup
extra_kwargs = {}
try: description = "increment the package version and commit the changes"
import fontTools
except ImportError:
print("*** Warning: ufoLib needs FontTools for some operations, see:")
print(" https://github.com/behdad/fonttools")
user_options = [
("major", None, "bump the first digit, for incompatible API changes"),
("minor", None, "bump the second digit, for new backward-compatible features"),
("patch", None, "bump the third digit, for bug fixes (default)"),
]
def initialize_options(self):
self.minor = False
self.major = False
self.patch = False
def finalize_options(self):
part = None
for attr in ("major", "minor", "patch"):
if getattr(self, attr, False):
if part is None:
part = attr
else:
from distutils.errors import DistutilsOptionError
raise DistutilsOptionError(
"version part options are mutually exclusive")
self.part = part or "patch"
def bumpversion(self, part, **kwargs):
""" Run bumpversion.main() with the specified arguments.
"""
import bumpversion
args = ['--verbose'] if self.verbose > 1 else []
for k, v in kwargs.items():
k = "--{}".format(k.replace("_", "-"))
is_bool = isinstance(v, bool) and v is True
args.extend([k] if is_bool else [k, str(v)])
args.append(part)
log.debug(
"$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args))
bumpversion.main(args)
def run(self):
log.info("bumping '%s' version" % self.part)
self.bumpversion(self.part)
class release(bump_version):
"""Drop the developmental release '.devN' suffix from the package version,
open the default text $EDITOR to write release notes, commit the changes
and generate a git tag.
Release notes can also be set with the -m/--message option, or by reading
from standard input.
"""
description = "tag a new release"
user_options = [
("message=", 'm', "message containing the release notes"),
]
def initialize_options(self):
self.message = None
def finalize_options(self):
import re
current_version = self.distribution.metadata.get_version()
if not re.search(r"\.dev[0-9]+", current_version):
from distutils.errors import DistutilsSetupError
raise DistutilsSetupError(
"current version (%s) has no '.devN' suffix.\n "
"Run 'setup.py bump_version' with any of "
"--major, --minor, --patch options" % current_version)
message = self.message
if message is None:
if sys.stdin.isatty():
# stdin is interactive, use editor to write release notes
message = self.edit_release_notes()
else:
# read release notes from stdin pipe
message = sys.stdin.read()
if not message.strip():
from distutils.errors import DistutilsSetupError
raise DistutilsSetupError("release notes message is empty")
self.message = "v{new_version}\n\n%s" % message
@staticmethod
def edit_release_notes():
"""Use the default text $EDITOR to write release notes.
If $EDITOR is not set, use 'nano'."""
from tempfile import mkstemp
import os
import shlex
import subprocess
text_editor = shlex.split(os.environ.get('EDITOR', 'nano'))
fd, tmp = mkstemp(prefix='bumpversion-')
try:
os.close(fd)
with open(tmp, 'w') as f:
f.write("\n\n# Write release notes.\n"
"# Lines starting with '#' will be ignored.")
subprocess.check_call(text_editor + [tmp])
with open(tmp, 'r') as f:
changes = "".join(
l for l in f.readlines() if not l.startswith('#'))
finally:
os.remove(tmp)
return changes
def run(self):
log.info("stripping developmental release suffix")
# drop '.dev0' suffix, commit with given message and create git tag
self.bumpversion("release",
tag=True,
message="Release {new_version}",
tag_message=self.message)
needs_pytest = {'pytest', 'test'}.intersection(sys.argv)
pytest_runner = ['pytest_runner'] if needs_pytest else []
needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
wheel = ['wheel'] if needs_wheel else []
needs_bump2version = {'release', 'bump_version'}.intersection(sys.argv)
bump2version = ['bump2version'] if needs_bump2version else []
long_description = """\ long_description = """\
ufoLib reads and writes Unified Font Object (UFO) files. UFO is a file format ufoLib reads and writes Unified Font Object (UFO) files.
that stores fonts source files. UFO is a file format that stores fonts source files.
http://unifiedfontobject.org
""" """
setup( setup_params = dict(
name = "ufoLib", name="ufoLib",
version = "1.2", version="2.1.1.dev0",
description = "A low-level UFO reader and writer.", description="A low-level UFO reader and writer.",
author = "Just van Rossum, Tal Leming, Erik van Blokland, others", author="Just van Rossum, Tal Leming, Erik van Blokland, others",
author_email = "info@robofab.com", author_email="info@robofab.com",
maintainer = "Just van Rossum, Tal Leming, Erik van Blokland", maintainer="Just van Rossum, Tal Leming, Erik van Blokland",
maintainer_email = "info@robofab.com", maintainer_email="info@robofab.com",
url = "http://unifiedfontobject.org", url="https://github.com/unified-font-object/ufoLib",
license = "OpenSource, BSD-style", license="OpenSource, BSD-style",
platforms = ["Any"], platforms=["Any"],
long_description = long_description, long_description=long_description,
package_dir={'': 'Lib'},
packages = [ packages=find_packages('Lib'),
"ufoLib", include_package_data=True,
setup_requires=pytest_runner + wheel + bump2version,
tests_require=[
'pytest>=3.0.2',
], ],
package_dir = {'': 'Lib'}, install_requires=[
classifiers = [ "fonttools>=3.10.0",
],
cmdclass={
"release": release,
"bump_version": bump_version,
},
classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Environment :: Console", "Environment :: Console",
"Environment :: Other Environment", "Environment :: Other Environment",
@ -53,5 +184,8 @@ setup(
"Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics",
"Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Graphics Conversion",
], ],
**extra_kwargs )
)
if __name__ == "__main__":
setup(**setup_params)

17
tox.ini Normal file
View File

@ -0,0 +1,17 @@
[tox]
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}
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
pytest {posargs:--pyargs ufoLib}