Merge pull request #81 from anthrotype/ufo4
update "Add ZIP support to UFO"
This commit is contained in:
commit
879fce2e7c
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,3 +7,5 @@ dist/
|
||||
.DS_Store
|
||||
*.egg-info
|
||||
*.py[cod]
|
||||
.eggs/
|
||||
.tox/
|
||||
|
5
.pyup.yml
Normal file
5
.pyup.yml
Normal 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
|
63
.travis.yml
63
.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
|
||||
|
@ -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"])
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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():
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
98
Lib/ufoLib/test/test_filenames.py
Normal file
98
Lib/ufoLib/test/test_filenames.py
Normal 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'
|
||||
)
|
@ -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
8
MANIFEST.in
Normal 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
|
@ -1,6 +1,7 @@
|
||||
[](https://travis-ci.org/unified-font-object/ufoLib)
|
||||
[](https://ci.appveyor.com/project/adrientetar/ufolib)
|
||||

|
||||
[](https://pypi.org/project/ufoLib/)
|
||||
|
||||
ufoLib
|
||||
------
|
||||
|
79
appveyor.yml
79
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"
|
||||
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
fonttools==3.13.1
|
50
setup.cfg
Normal file
50
setup.cfg
Normal 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
228
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)
|
||||
|
17
tox.ini
Normal file
17
tox.ini
Normal 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}
|
Loading…
x
Reference in New Issue
Block a user