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
*.egg-info
*.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
sudo: required
python:
- "2.7"
- "3.4"
- "3.5"
before_install:
- git clone --depth=1 https://github.com/behdad/fonttools.git
sudo: false
matrix:
include:
- python: 2.7
env: TOXENV=py27-fs
- python: 2.7
env: TOXENV=py27-nofs
- python: 3.5
env: TOXENV=py35-fs
- python: 3.5
env: TOXENV=py35-nofs
- python: 3.6
env: TOXENV=py36-fs
- python: 3.6
env:
- TOXENV=py36-nofs
- BUILD_DIST=true
install:
- cd fonttools; python setup.py install; cd ..
- python setup.py install
script:
- python -c "import fontTools"
- python setup.py test
- pip install --upgrade pip setuptools wheel tox
script: tox
before_deploy:
- pip wheel --no-deps -w dist .
- 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
import codecs
from copy import deepcopy
from fontTools.misc.py23 import basestring, unicode
from ufoLib.filesystem import FileSystem
from ufoLib.glifLib import GlyphSet
from ufoLib.validators import *
@ -41,11 +42,6 @@ fontinfo.plist values between the possible format versions.
convertFontInfoValueForAttributeFromVersion3ToVersion2
"""
try:
basestring
except NameError:
basestring = str
__all__ = [
"makeUFOPath"
"UFOLibError",
@ -61,6 +57,8 @@ __all__ = [
"convertFontInfoValueForAttributeFromVersion2ToVersion1"
]
__version__ = "2.1.1.dev0"
@ -173,6 +171,11 @@ class UFOReader(object):
self._upConvertedKerningData["groups"] = groups
self._upConvertedKerningData["groupRenameMaps"] = conversionMaps
# support methods
def getFileModificationTime(self, path):
return self.fileSystem.getFileModificationTime(path)
# metainfo.plist
def readMetaInfo(self):
@ -431,13 +434,14 @@ class UFOReader(object):
if not self.fileSystem.isDirectory(IMAGES_DIRNAME):
raise UFOLibError("The UFO contains an \"images\" file instead of a directory.")
result = []
for fileName in self.fileSystem.listDirectory(path):
if self.fileSystem.isDirectory(fileName):
for fileName in self.fileSystem.listDirectory(IMAGES_DIRNAME):
path = self.fileSystem.joinPath(IMAGES_DIRNAME, fileName)
if self.fileSystem.isDirectory(path):
# silently skip this as version control
# systems often have hidden directories
continue
# XXX this is sending a path to the validator. that won't work in the abstracted filesystem.
valid, error = pngValidator(path=p)
with self.fileSystem.open(path, mode='rb') as fp:
valid, error = pngValidator(fileObj=fp)
if valid:
result.append(fileName)
return result
@ -927,9 +931,9 @@ class UFOWriter(object):
# not caching this could be slightly expensive,
# but caching it will be cumbersome
existing = [d.lower() for d in list(self.layerContents.values())]
if not isinstance(layerName, basestring):
if not isinstance(layerName, unicode):
try:
layerName = str(layerName)
layerName = unicode(layerName)
except UnicodeDecodeError:
raise UFOLibError("The specified layer name is not a Unicode string.")
directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.")
@ -1024,8 +1028,8 @@ class UFOWriter(object):
"""
if self._formatVersion < 3:
raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
sourcePath = reader.joinPath(IMAGES_DIRNAME, sourceFileName)
destPath = self.joinPath(IMAGES_DIRNAME, destFileName)
sourcePath = self.fileSystem.joinPath(IMAGES_DIRNAME, sourceFileName)
destPath = self.fileSystem.joinPath(IMAGES_DIRNAME, destFileName)
self.copyFromReader(reader, sourcePath, destPath)
@ -1037,10 +1041,12 @@ def makeUFOPath(path):
"""
Return a .ufo pathname.
>>> makeUFOPath("/directory/something.ext")
'/directory/something.ufo'
>>> makeUFOPath("/directory/something.another.thing.ext")
'/directory/something.another.thing.ufo'
>>> makeUFOPath("directory/something.ext") == (
... os.path.join('directory', 'something.ufo'))
True
>>> makeUFOPath("directory/something.another.thing.ext") == (
... os.path.join('directory', 'something.another.thing.ufo'))
True
"""
dir, name = os.path.split(path)
name = ".".join([".".join(name.split(".")[:-1]), "ufo"])

View File

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

View File

@ -2,11 +2,9 @@
User name to file name conversion.
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 += [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
of all existing file names.
>>> userNameToFileName(u"a")
u'a'
>>> userNameToFileName(u"A")
u'A_'
>>> userNameToFileName(u"AE")
u'A_E_'
>>> userNameToFileName(u"Ae")
u'A_e'
>>> userNameToFileName(u"ae")
u'ae'
>>> userNameToFileName(u"aE")
u'aE_'
>>> userNameToFileName(u"a.alt")
u'a.alt'
>>> userNameToFileName(u"A.alt")
u'A_.alt'
>>> userNameToFileName(u"A.Alt")
u'A_.A_lt'
>>> userNameToFileName(u"A.aLt")
u'A_.aL_t'
>>> userNameToFileName(u"A.alT")
u'A_.alT_'
>>> userNameToFileName(u"T_H")
u'T__H_'
>>> userNameToFileName(u"T_h")
u'T__h'
>>> userNameToFileName(u"t_h")
u't_h'
>>> userNameToFileName(u"F_F_I")
u'F__F__I_'
>>> userNameToFileName(u"f_f_i")
u'f_f_i'
>>> userNameToFileName(u"Aacute_V.swash")
u'A_acute_V_.swash'
>>> userNameToFileName(u".notdef")
u'_notdef'
>>> userNameToFileName(u"con")
u'_con'
>>> userNameToFileName(u"CON")
u'C_O_N_'
>>> userNameToFileName(u"con.alt")
u'_con.alt'
>>> userNameToFileName(u"alt.con")
u'alt._con'
>>> userNameToFileName("a") == "a"
True
>>> userNameToFileName("A") == "A_"
True
>>> userNameToFileName("AE") == "A_E_"
True
>>> userNameToFileName("Ae") == "A_e"
True
>>> userNameToFileName("ae") == "ae"
True
>>> userNameToFileName("aE") == "aE_"
True
>>> userNameToFileName("a.alt") == "a.alt"
True
>>> userNameToFileName("A.alt") == "A_.alt"
True
>>> userNameToFileName("A.Alt") == "A_.A_lt"
True
>>> userNameToFileName("A.aLt") == "A_.aL_t"
True
>>> userNameToFileName(u"A.alT") == "A_.alT_"
True
>>> userNameToFileName("T_H") == "T__H_"
True
>>> userNameToFileName("T_h") == "T__h"
True
>>> userNameToFileName("t_h") == "t_h"
True
>>> userNameToFileName("F_F_I") == "F__F__I_"
True
>>> userNameToFileName("f_f_i") == "f_f_i"
True
>>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash"
True
>>> userNameToFileName(".notdef") == "_notdef"
True
>>> userNameToFileName("con") == "_con"
True
>>> userNameToFileName("CON") == "C_O_N_"
True
>>> userNameToFileName("con.alt") == "_con.alt"
True
>>> userNameToFileName("alt.con") == "alt._con"
True
"""
# 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
prefixLength = len(prefix)
suffixLength = len(suffix)
@ -118,20 +117,23 @@ def handleClash1(userName, existing=[], prefix="", suffix=""):
>>> e = list(existing)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix)
'00000.AAAAA000000000000001.0000000000'
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000001.0000000000')
True
>>> e = list(existing)
>>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix)
'00000.AAAAA000000000000002.0000000000'
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000002.0000000000')
True
>>> e = list(existing)
>>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix)
'00000.AAAAA000000000000001.0000000000'
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000001.0000000000')
True
"""
# 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
@ -170,18 +172,21 @@ def handleClash2(existing=[], prefix="", suffix=""):
>>> existing = [prefix + str(i) + suffix for i in range(100)]
>>> e = list(existing)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix)
'00000.100.0000000000'
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.100.0000000000')
True
>>> e = list(existing)
>>> e.remove(prefix + "1" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix)
'00000.1.0000000000'
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.1.0000000000')
True
>>> e = list(existing)
>>> e.remove(prefix + "2" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix)
'00000.2.0000000000'
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.2.0000000000')
True
"""
# calculate the longest possible string
maxLength = maxFileNameLength - len(prefix) - len(suffix)

View File

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

View File

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

View File

@ -23,7 +23,7 @@ class AbstractPointPen(object):
Baseclass for all PointPens.
"""
def beginPath(self):
def beginPath(self, identifier=None, **kwargs):
"""Start a new sub path."""
raise NotImplementedError
@ -31,11 +31,13 @@ class AbstractPointPen(object):
"""End the current sub path."""
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."""
raise NotImplementedError
def addComponent(self, baseGlyphName, transformation):
def addComponent(self, baseGlyphName, transformation, identifier=None,
**kwargs):
"""Add a sub glyph."""
raise NotImplementedError
@ -190,7 +192,7 @@ class PointToSegmentPen(BasePointToSegmentPen):
else:
pen.endPath()
def addComponent(self, glyphName, transform):
def addComponent(self, glyphName, transform, **kwargs):
self.pen.addComponent(glyphName, transform)
@ -320,3 +322,85 @@ class GuessSmoothPointPen(AbstractPointPen):
def addComponent(self, glyphName, transformation):
assert self._points is None
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():
"""Return the path to Data/DemoFont.ufo/."""
import ufoLib
root = os.path.dirname(os.path.dirname(os.path.dirname(ufoLib.__file__)))
return os.path.join(root, "Data", "DemoFont.ufo")
testdata = os.path.join(os.path.dirname(__file__), "testdata")
return os.path.join(testdata, "DemoFont.ufo")
def getDemoFontGlyphSetPath():

View File

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

View File

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

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
from io import open
try:
from collections.abc import Mapping # python >= 3.3
except ImportError:
from collections import Mapping
# -------
# Python 2 or 3
# -------
@ -21,7 +26,7 @@ def isDictEnough(value):
Some objects will likely come in that aren't
dicts but are dict-ish enough.
"""
if isinstance(value, dict):
if isinstance(value, Mapping):
return True
attrs = ("keys", "values", "items")
for attr in attrs:
@ -75,7 +80,7 @@ def genericDictValidator(value, prototype):
Generic. (Added at version 3.)
"""
# not a dict
if not isinstance(value, dict):
if not isinstance(value, Mapping):
return False
# missing required keys
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.')
"""
bogusFormatMessage = "The kerning data is not in the correct format."
if not isinstance(data, dict):
if not isinstance(data, Mapping):
return False, bogusFormatMessage
for first, secondDict in list(data.items()):
if not isinstance(first, basestring):
return False, bogusFormatMessage
elif not isinstance(secondDict, dict):
elif not isinstance(secondDict, Mapping):
return False, bogusFormatMessage
for second, value in list(secondDict.items()):
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)
[![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)
[![PyPI](https://img.shields.io/pypi/v/ufoLib.svg)](https://pypi.org/project/ufoLib/)
ufoLib
------

View File

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

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

228
setup.py
View File

@ -1,57 +1,191 @@
#! /usr/bin/env python
import sys
from setuptools import setup, find_packages, Command
from distutils import log
import os, sys
try:
from setuptools import setup
extra_kwargs = {
"test_suite": "ufoLib.test"
}
except ImportError:
from distutils.core import setup
extra_kwargs = {}
class bump_version(Command):
try:
import fontTools
except ImportError:
print("*** Warning: ufoLib needs FontTools for some operations, see:")
print(" https://github.com/behdad/fonttools")
description = "increment the package version and commit the changes"
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 = """\
ufoLib reads and writes Unified Font Object (UFO) files. UFO is a file format
that stores fonts source files.
ufoLib reads and writes Unified Font Object (UFO) files.
UFO is a file format that stores fonts source files.
http://unifiedfontobject.org
"""
setup(
name = "ufoLib",
version = "1.2",
description = "A low-level UFO reader and writer.",
author = "Just van Rossum, Tal Leming, Erik van Blokland, others",
author_email = "info@robofab.com",
maintainer = "Just van Rossum, Tal Leming, Erik van Blokland",
maintainer_email = "info@robofab.com",
url = "http://unifiedfontobject.org",
license = "OpenSource, BSD-style",
platforms = ["Any"],
long_description = long_description,
setup_params = dict(
name="ufoLib",
version="2.1.1.dev0",
description="A low-level UFO reader and writer.",
author="Just van Rossum, Tal Leming, Erik van Blokland, others",
author_email="info@robofab.com",
maintainer="Just van Rossum, Tal Leming, Erik van Blokland",
maintainer_email="info@robofab.com",
url="https://github.com/unified-font-object/ufoLib",
license="OpenSource, BSD-style",
platforms=["Any"],
long_description=long_description,
package_dir={'': 'Lib'},
packages=find_packages('Lib'),
include_package_data=True,
setup_requires=pytest_runner + wheel + bump2version,
tests_require=[
'pytest>=3.0.2',
],
install_requires=[
"fonttools>=3.10.0",
],
cmdclass={
"release": release,
"bump_version": bump_version,
},
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console",
"Environment :: Other Environment",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Multimedia :: Graphics",
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
],
)
packages = [
"ufoLib",
],
package_dir = {'': 'Lib'},
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Environment :: Other Environment",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Multimedia :: Graphics",
"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}