diff --git a/.gitignore b/.gitignore index 4da73de8d..7faed8c14 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ dist/ .DS_Store *.egg-info *.py[cod] +.eggs/ +.tox/ diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 000000000..ed0ac860b --- /dev/null +++ b/.pyup.yml @@ -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 diff --git a/.travis.yml b/.travis.yml index 84cba53c7..d2f926a14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Lib/ufoLib/__init__.py b/Lib/ufoLib/__init__.py index 18e18c546..f37ebd4e6 100755 --- a/Lib/ufoLib/__init__.py +++ b/Lib/ufoLib/__init__.py @@ -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"]) diff --git a/Lib/ufoLib/converters.py b/Lib/ufoLib/converters.py index be8ee7772..81b2a2586 100644 --- a/Lib/ufoLib/converters.py +++ b/Lib/ufoLib/converters.py @@ -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. diff --git a/Lib/ufoLib/filenames.py b/Lib/ufoLib/filenames.py index b6e310c9a..876f6b389 100644 --- a/Lib/ufoLib/filenames.py +++ b/Lib/ufoLib/filenames.py @@ -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) diff --git a/Lib/ufoLib/filesystem.py b/Lib/ufoLib/filesystem.py index ab92dfecc..ab2b72340 100644 --- a/Lib/ufoLib/filesystem.py +++ b/Lib/ufoLib/filesystem.py @@ -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 = "" 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 diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index 1b9789bac..2fa7d613f 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -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. diff --git a/Lib/ufoLib/pointPen.py b/Lib/ufoLib/pointPen.py index 8bc6fe96d..805949175 100644 --- a/Lib/ufoLib/pointPen.py +++ b/Lib/ufoLib/pointPen.py @@ -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) diff --git a/Lib/ufoLib/test/testSupport.py b/Lib/ufoLib/test/testSupport.py index bd364c8d6..2f02cd1e7 100755 --- a/Lib/ufoLib/test/testSupport.py +++ b/Lib/ufoLib/test/testSupport.py @@ -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(): diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index 8e5dedc84..87f82b258 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -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)) diff --git a/Lib/ufoLib/test/test_UFOConversion.py b/Lib/ufoLib/test/test_UFOConversion.py index 16e1de16d..f0efa4c5b 100644 --- a/Lib/ufoLib/test/test_UFOConversion.py +++ b/Lib/ufoLib/test/test_UFOConversion.py @@ -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 diff --git a/Lib/ufoLib/test/test_filenames.py b/Lib/ufoLib/test/test_filenames.py new file mode 100644 index 000000000..b57854bf5 --- /dev/null +++ b/Lib/ufoLib/test/test_filenames.py @@ -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' + ) diff --git a/Data/DemoFont.ufo/fontinfo.plist b/Lib/ufoLib/test/testdata/DemoFont.ufo/fontinfo.plist similarity index 100% rename from Data/DemoFont.ufo/fontinfo.plist rename to Lib/ufoLib/test/testdata/DemoFont.ufo/fontinfo.plist diff --git a/Data/DemoFont.ufo/glyphs/A_.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/A_.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/A_.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/A_.glif diff --git a/Data/DemoFont.ufo/glyphs/B_.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/B_.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/B_.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/B_.glif diff --git a/Data/DemoFont.ufo/glyphs/F_.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F_.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/F_.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F_.glif diff --git a/Data/DemoFont.ufo/glyphs/F__A__B_.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F__A__B_.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/F__A__B_.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/F__A__B_.glif diff --git a/Data/DemoFont.ufo/glyphs/G_.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/G_.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/G_.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/G_.glif diff --git a/Data/DemoFont.ufo/glyphs/O_.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/O_.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/O_.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/O_.glif diff --git a/Data/DemoFont.ufo/glyphs/R_.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/R_.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/R_.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/R_.glif diff --git a/Data/DemoFont.ufo/glyphs/a#condensed_bold.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#condensed_bold.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/a#condensed_bold.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#condensed_bold.glif diff --git a/Data/DemoFont.ufo/glyphs/a#condensed_light.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#condensed_light.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/a#condensed_light.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#condensed_light.glif diff --git a/Data/DemoFont.ufo/glyphs/a#wide_bold.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#wide_bold.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/a#wide_bold.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#wide_bold.glif diff --git a/Data/DemoFont.ufo/glyphs/a#wide_light.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#wide_light.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/a#wide_light.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a#wide_light.glif diff --git a/Data/DemoFont.ufo/glyphs/a.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/a.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/a.glif diff --git a/Data/DemoFont.ufo/glyphs/contents.plist b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/contents.plist similarity index 100% rename from Data/DemoFont.ufo/glyphs/contents.plist rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/contents.plist diff --git a/Data/DemoFont.ufo/glyphs/testglyph1.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/testglyph1.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.glif diff --git a/Data/DemoFont.ufo/glyphs/testglyph1.reversed.glif b/Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif similarity index 100% rename from Data/DemoFont.ufo/glyphs/testglyph1.reversed.glif rename to Lib/ufoLib/test/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif diff --git a/Data/DemoFont.ufo/metainfo.plist b/Lib/ufoLib/test/testdata/DemoFont.ufo/metainfo.plist similarity index 100% rename from Data/DemoFont.ufo/metainfo.plist rename to Lib/ufoLib/test/testdata/DemoFont.ufo/metainfo.plist diff --git a/TestData/TestFont1 (UFO1).ufo/fontinfo.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/fontinfo.plist similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/fontinfo.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/fontinfo.plist diff --git a/TestData/TestFont1 (UFO1).ufo/glyphs/A_.glif b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/glyphs/A_.glif rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif diff --git a/TestData/TestFont1 (UFO1).ufo/glyphs/B_.glif b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/glyphs/B_.glif rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif diff --git a/TestData/TestFont1 (UFO1).ufo/glyphs/contents.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/glyphs/contents.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist diff --git a/TestData/TestFont1 (UFO1).ufo/groups.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/groups.plist similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/groups.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/groups.plist diff --git a/TestData/TestFont1 (UFO1).ufo/kerning.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/kerning.plist similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/kerning.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/kerning.plist diff --git a/TestData/TestFont1 (UFO1).ufo/lib.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/lib.plist similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/lib.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/lib.plist diff --git a/TestData/TestFont1 (UFO1).ufo/metainfo.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/metainfo.plist similarity index 100% rename from TestData/TestFont1 (UFO1).ufo/metainfo.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO1).ufo/metainfo.plist diff --git a/TestData/TestFont1 (UFO2).ufo/features.fea b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/features.fea similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/features.fea rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/features.fea diff --git a/TestData/TestFont1 (UFO2).ufo/fontinfo.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/fontinfo.plist similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/fontinfo.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/fontinfo.plist diff --git a/TestData/TestFont1 (UFO2).ufo/glyphs/A_.glif b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/glyphs/A_.glif rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif diff --git a/TestData/TestFont1 (UFO2).ufo/glyphs/B_.glif b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/glyphs/B_.glif rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif diff --git a/TestData/TestFont1 (UFO2).ufo/glyphs/contents.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/glyphs/contents.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist diff --git a/TestData/TestFont1 (UFO2).ufo/groups.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/groups.plist similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/groups.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/groups.plist diff --git a/TestData/TestFont1 (UFO2).ufo/kerning.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/kerning.plist similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/kerning.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/kerning.plist diff --git a/TestData/TestFont1 (UFO2).ufo/lib.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/lib.plist similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/lib.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/lib.plist diff --git a/TestData/TestFont1 (UFO2).ufo/metainfo.plist b/Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/metainfo.plist similarity index 100% rename from TestData/TestFont1 (UFO2).ufo/metainfo.plist rename to Lib/ufoLib/test/testdata/TestFont1 (UFO2).ufo/metainfo.plist diff --git a/TestData/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt b/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt similarity index 100% rename from TestData/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt rename to Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt diff --git a/TestData/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt b/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt similarity index 100% rename from TestData/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt rename to Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt diff --git a/TestData/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt b/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt similarity index 100% rename from TestData/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt rename to Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt diff --git a/TestData/UFO3-Read Data.ufo/metainfo.plist b/Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/metainfo.plist similarity index 100% rename from TestData/UFO3-Read Data.ufo/metainfo.plist rename to Lib/ufoLib/test/testdata/UFO3-Read Data.ufo/metainfo.plist diff --git a/Lib/ufoLib/validators.py b/Lib/ufoLib/validators.py index a2377c91b..b453615d7 100644 --- a/Lib/ufoLib/validators.py +++ b/Lib/ufoLib/validators.py @@ -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): diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..aff919126 --- /dev/null +++ b/MANIFEST.in @@ -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 diff --git a/README.md b/README.md index 78ecccb8b..9a9dc4a6f 100644 --- a/README.md +++ b/README.md @@ -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 ------ diff --git a/appveyor.yml b/appveyor.yml index af00b2c5f..a4078899d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -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" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..1f61c3241 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +fonttools==3.13.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..2a8e34e20 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,50 @@ +[bumpversion] +current_version = 2.1.1.dev0 +commit = True +tag = False +tag_name = v{new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\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 + diff --git a/setup.py b/setup.py index bfd33a758..9fc3005d3 100755 --- a/setup.py +++ b/setup.py @@ -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) diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..11e9bbc50 --- /dev/null +++ b/tox.ini @@ -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}