pax_global_header00006660000000000000000000000064134057520340014515gustar00rootroot0000000000000052 comment=cefbfdb61a5d4e12cc2b856ec1e2fe503a21b12b ufoNormalizer-0.3.6/000077500000000000000000000000001340575203400143575ustar00rootroot00000000000000ufoNormalizer-0.3.6/.coveragerc000066400000000000000000000014521340575203400165020ustar00rootroot00000000000000[run] # measure 'branch' coverage in addition to 'statement' coverage # See: http://coverage.readthedocs.org/en/coverage-4.0.3/branch.html#branch branch = True # list of directories or packages to measure source = ufonormalizer # these are treated as equivalent when combining data [paths] source = src .tox/*/lib/python*/site-packages .tox/pypy*/site-packages [report] # Regexes for lines to exclude from consideration exclude_lines = # keywords to use in inline comments to skip coverage pragma: no cover # don't complain if tests don't hit defensive assertion code raise AssertionError raise NotImplementedError # don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: # ignore source code that can’t be found ignore_errors = True ufoNormalizer-0.3.6/.gitignore000066400000000000000000000013351340575203400163510ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Test data tests/data/*.ufo/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ ufoNormalizer-0.3.6/.travis.yml000066400000000000000000000022661340575203400164760ustar00rootroot00000000000000language: python env: global: - TWINE_USERNAME="anthrotype" - secure: chwl41J38s5RFf3gouXeQzsbkQNDVBWISR/teT6fEOIdt7++T51Qe6lkPbaaz01PmvjF1i5X7Hbyg4JY238un3jhQRnNd4bAnB0rfcGEh1hwEmUV0zFEM1PdPP4ihAcnoi032z0ymN5OA71Qq967vcgM1vcTPL0aJIOi4T5MZZLTDmMcno588ZhMvJ6+kNsWSE9zM/rX3ba8ReB0XccWPLvMiV54VmOk5OtaLhTPIxMy0C3Gf7m5xrND7jnLeFbOw+JoarUQQlYRIGKsXUy5ed0d8JLE8dVHTtzt/zVKWA2wslvxXsRIGZCbW1C6YxXqpjyAhp3Xu7pFKxtPS9bkw3fz87/PXcFlNLe1zX/t1H8aaOTWe6+K6S8GbD95KcCnpHsQUFzvQxHqxJqX7xx+1CSoNalXbf0arnuN9y70JcXW6AnHSZUYzOZalFLp18qj24XHaWuoW7TMP/GdQLDRVDzs1cguC0Lf9W/Cxo/FzOMjUgOTE7iEQWIotfJsY8DIDatjA2rclNz7N7fg2rL9Bi5cPUVyCC5SJtrolfpFz2mNAy06pVlc7fdeeH/40wDm//wr2ZNl1DEiNDCNgfgijnCsobgAIJpT5CsMrgZmAzfxuqhTK8012qwqhVkF69M6aTPmLOTS/WzK1FSA5V1m4whdFWwrQXcqzq9kG2wEa54= install: - pip install coveralls python: - "2.7" - "3.5" - "3.6" script: - coverage run setup.py test after_success: - | if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "unified-font-object/ufoNormalizer" ] && [ "$TRAVIS_PYTHON_VERSION" == "3.6" ]; then pip install --upgrade twine setuptools wheel python setup.py sdist bdist_wheel twine upload dist/*.whl dist/*.zip fi after_success: coveralls ufoNormalizer-0.3.6/LICENSE.txt000066400000000000000000000032021340575203400161770ustar00rootroot00000000000000ufoNormalizer License Agreement Copyright (c) 2015-2017, Tal Leming. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Tal Leming nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Up to date info on ufoNormalizer: https://github.com/unified-font-object/ufoNormalizer This is the BSD license: http://www.opensource.org/licenses/BSD-3-Clause ufoNormalizer-0.3.6/MANIFEST.in000066400000000000000000000002221340575203400161110ustar00rootroot00000000000000include README.md include LICENSE.txt include appveyor.yml include .travis.yml recursive-include tests/ *.py recursive-include tests/data/ *.glif ufoNormalizer-0.3.6/README.md000066400000000000000000000012511340575203400156350ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/unified-font-object/ufoNormalizer.svg)](https://travis-ci.org/unified-font-object/ufoNormalizer) [![Build status](https://ci.appveyor.com/api/projects/status/pc4l0dryn5hevcw4?svg=true)](https://ci.appveyor.com/project/miguelsousa/ufonormalizer) [![Coverage Status](https://coveralls.io/repos/unified-font-object/ufoNormalizer/badge.svg?branch=master&service=github)](https://coveralls.io/github/unified-font-object/ufoNormalizer?branch=master) ![Python Versions](https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5-blue.svg) [![PyPI Version](https://img.shields.io/pypi/v/ufonormalizer.svg)](https://pypi.python.org/pypi/ufonormalizer) ufoNormalizer-0.3.6/appveyor.yml000066400000000000000000000013641340575203400167530ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "32" - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" - PYTHON: "C:\\Python27-x64" PYTHON_VERSION: "2.7.x" PYTHON_ARCH: "64" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" install: # prepend Python to the PATH - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" # check that we have the expected version and architecture for Python - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" build: false test_script: # run the test suite - "python setup.py test" ufoNormalizer-0.3.6/setup.cfg000066400000000000000000000010651340575203400162020ustar00rootroot00000000000000[bumpversion] current_version = 0.3.6 commit = True tag = False tag_name = {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:src/ufonormalizer.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [wheel] universal = 1 [sdist] formats = zip [metadata] license_file = LICENSE.txt ufoNormalizer-0.3.6/setup.py000066400000000000000000000030221340575203400160660ustar00rootroot00000000000000from setuptools import setup from io import open import ast with open('src/ufonormalizer.py', 'r', encoding='utf-8') as f: for line in f: if line.startswith(u'__version__'): version = ast.parse(line).body[0].value.s break else: raise RuntimeError("No __version__ string found!") setup( name="ufonormalizer", version=version, description=("Script to normalize the XML and other data " "inside of a UFO."), author="Tal Leming", author_email="tal@typesupply.com", url="https://github.com/unified-font-object/ufoNormalizer", package_dir={"": "src"}, py_modules=['ufonormalizer'], entry_points={ 'console_scripts': [ "ufonormalizer = ufonormalizer:main", ] }, test_suite="tests", license="OpenSource, BSD-style", platforms=["Any"], classifiers=[ "Development Status :: 5 - Production/Stable", "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", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Text Processing :: Fonts", "Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics :: Graphics Conversion", ], ) ufoNormalizer-0.3.6/src/000077500000000000000000000000001340575203400151465ustar00rootroot00000000000000ufoNormalizer-0.3.6/src/ufonormalizer.py000066400000000000000000001562401340575203400204240ustar00rootroot00000000000000#! /usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals import time import os import shutil from xml.etree import cElementTree as ET import plistlib import textwrap import datetime import glob from collections import OrderedDict from io import open import logging """ - filter out unknown attributes and subelements - add doctests for the image purging - things that need to be improved are marked with "# TO DO" """ __version__ = "0.3.6" description = """ UFO Normalizer (version %s): This tool processes the contents of a UFO and normalizes all possible files to a standard XML formatting, data structure and file naming scheme. """ % __version__ log = logging.getLogger(__name__) def main(args=None): import argparse parser = argparse.ArgumentParser(description=description) parser.add_argument("input", help="Path to a UFO to normalize.", nargs="?") parser.add_argument("-t", "--test", help="Run the normalizer's internal tests.", action="store_true") parser.add_argument("-o", "--output", help="Output path. If not given, the input path will be used.") parser.add_argument("-a", "--all", help="Normalize all files in the UFO. By default, only files modified since the previous normalization will be processed.", action="store_true") parser.add_argument("-v", "--verbose", help="Print more info to console.", action="store_true") parser.add_argument("-q", "--quiet", help="Suppress all non-error messages.", action="store_true") parser.add_argument("--float-precision", type=int, default=DEFAULT_FLOAT_PRECISION, help="Round floats to the specified number of decimal places (default is %d). The value -1 means no rounding (i.e. use built-in repr()." % DEFAULT_FLOAT_PRECISION) parser.add_argument("-m", "--no-mod-times", help="Do not write normalization time stamps.", action="store_true") args = parser.parse_args(args) if args.test: return runTests() if args.verbose and args.quiet: parser.error("--quiet and --verbose options are mutually exclusive.") logLevel = "DEBUG" if args.verbose else "ERROR" if args.quiet else "INFO" logging.basicConfig(level=logLevel, format="%(message)s") if args.input is None: parser.error("No input path was specified.") inputPath = os.path.normpath(args.input) outputPath = args.output onlyModified = not args.all if not os.path.exists(inputPath): parser.error('Input path does not exist: "%s".' % inputPath) if os.path.splitext(inputPath)[-1].lower() != ".ufo": parser.error('Input path is not a UFO: "%s".' % inputPath) if args.float_precision >= 0: floatPrecision = args.float_precision elif args.float_precision == -1: floatPrecision = None else: parser.error("float precision must be >= 0 or -1 (no round).") writeModTimes = not args.no_mod_times message = 'Normalizing "%s".' if not onlyModified: message += " Processing all files." log.info(message, os.path.basename(inputPath)) start = time.time() normalizeUFO(inputPath, outputPath=outputPath, onlyModified=onlyModified, floatPrecision=floatPrecision, writeModTimes=writeModTimes) runtime = time.time() - start log.info("Normalization complete (%.4f seconds).", runtime) # --------- # Internals # --------- modTimeLibKey = "org.unifiedfontobject.normalizer.modTimes" imageReferencesLibKey = "org.unifiedfontobject.normalizer.imageReferences" # Differences between Python 2 and Python 3 # Python 3 does not have long, basestring, unicode try: long except NameError: long = int try: basestring except NameError: basestring = str try: unicode except NameError: unicode = str # Python 3.4 deprecated plistlib.readPlistFromBytes for loads. # Python 2 does not have plistlib.readPlistFromBytes it has # plistlib.readPlistFromString instead. if hasattr(plistlib, "loads"): def _loads(data): return plistlib.loads(data, use_builtin_types=False) def _dumps(plist): return plistlib.dumps(plist) elif hasattr(plistlib, "readPlistFromBytes"): def _loads(data): return plistlib.readPlistFromBytes(tobytes(data)) def _dumps(plist): return plistlib.writePlistToBytes(plist) else: def _loads(data): return plistlib.readPlistFromString(data) def _dumps(plist): return plistlib.writePlistToString(plist) # from fontTools.misc.py23 def tobytes(s, encoding='ascii', errors='strict'): '''no docstring''' if not isinstance(s, bytes): return s.encode(encoding, errors) else: return s def tounicode(s, encoding='ascii', errors='strict'): if not isinstance(s, unicode): return s.decode(encoding, errors) else: return s if str == bytes: tostr = tobytes else: tostr = tounicode class UFONormalizerError(Exception): pass DEFAULT_FLOAT_PRECISION = 10 FLOAT_FORMAT = "%%.%df" % DEFAULT_FLOAT_PRECISION def normalizeUFO(ufoPath, outputPath=None, onlyModified=True, floatPrecision=DEFAULT_FLOAT_PRECISION, writeModTimes=True): global FLOAT_FORMAT if floatPrecision is None: # use repr() and don't round floats FLOAT_FORMAT = None else: # round floats to a fixed number of decimal digits FLOAT_FORMAT = "%%.%df" % floatPrecision # if the output is going to a different location, # duplicate the UFO to the new place and work # on the new file instead of trying to reconstruct # the file one piece at a time. if outputPath is not None and outputPath != ufoPath: duplicateUFO(ufoPath, outputPath) ufoPath = outputPath # get the UFO format version if not subpathExists(ufoPath, "metainfo.plist"): raise UFONormalizerError("Required metainfo.plist file not in %s." % ufoPath) metaInfo = subpathReadPlist(ufoPath, "metainfo.plist") formatVersion = metaInfo.get("formatVersion") if formatVersion is None: raise UFONormalizerError("Required formatVersion value not defined in in metainfo.plist in %s." % ufoPath) try: fV = int(formatVersion) formatVersion = fV except ValueError: raise UFONormalizerError("Required formatVersion value not properly formatted in metainfo.plist in %s." % ufoPath) if formatVersion > 3: raise UFONormalizerError("Unsupported UFO format (%d) in %s." % (formatVersion, ufoPath)) # load the font lib if not subpathExists(ufoPath, "lib.plist"): fontLib = {} else: fontLib = subpathReadPlist(ufoPath, "lib.plist") # get the modification times if onlyModified: modTimes = readModTimes(fontLib) else: modTimes = {} # normalize layers if formatVersion < 3: if subpathExists(ufoPath, "glyphs"): normalizeUFO1And2GlyphsDirectory(ufoPath, modTimes) else: availableImages = readImagesDirectory(ufoPath) referencedImages = set() normalizeGlyphsDirectoryNames(ufoPath) if subpathExists(ufoPath, "layercontents.plist"): layerContents = subpathReadPlist(ufoPath, "layercontents.plist") for layerName, layerDirectory in layerContents: layerReferencedImages = normalizeGlyphsDirectory(ufoPath, layerDirectory, onlyModified=onlyModified, writeModTimes=writeModTimes) referencedImages |= layerReferencedImages imagesToPurge = availableImages - referencedImages purgeImagesDirectory(ufoPath, imagesToPurge) # normalize top level files normalizeMetaInfoPlist(ufoPath, modTimes) if subpathExists(ufoPath, "fontinfo.plist"): normalizeFontInfoPlist(ufoPath, modTimes) if subpathExists(ufoPath, "groups.plist"): normalizeGroupsPlist(ufoPath, modTimes) if subpathExists(ufoPath, "kerning.plist"): normalizeKerningPlist(ufoPath, modTimes) if subpathExists(ufoPath, "layercontents.plist"): normalizeLayerContentsPlist(ufoPath, modTimes) # update the mod time storage, write, normalize if writeModTimes: storeModTimes(fontLib, modTimes) subpathWritePlist(fontLib, ufoPath, "lib.plist") if subpathExists(ufoPath, "lib.plist"): normalizeLibPlist(ufoPath) # ------ # Layers # ------ def normalizeGlyphsDirectoryNames(ufoPath): """ Normalize glyphs directory names following UFO 3 user name to file name convention. """ # INVALID DATA POSSIBILITY: directory for layer name may not exist # INVALID DATA POSSIBILITY: directory may not be stored in layer contents oldLayerMapping = OrderedDict() if subpathExists(ufoPath, "layercontents.plist"): layerContents = subpathReadPlist(ufoPath, "layercontents.plist") for layerName, layerDirectory in layerContents: oldLayerMapping[layerName] = layerDirectory if not oldLayerMapping: return # INVALID DATA POSSIBILITY: no default layer # INVALID DATA POSSIBILITY: public.default used for directory other than "glyphs" newLayerMapping = OrderedDict() newLayerDirectories = set() for layerName, oldLayerDirectory in oldLayerMapping.items(): if oldLayerDirectory == "glyphs": newLayerDirectory = "glyphs" else: newLayerDirectory = userNameToFileName(unicode(layerName), newLayerDirectories, prefix="glyphs.") newLayerDirectories.add(newLayerDirectory.lower()) newLayerMapping[layerName] = newLayerDirectory # don't do a direct rename because an old directory # may have the same name as a new directory. fromTempMapping = {} for index, (layerName, newLayerDirectory) in enumerate(newLayerMapping.items()): oldLayerDirectory = oldLayerMapping[layerName] if newLayerDirectory == oldLayerDirectory: continue log.debug('Normalizing "%s" layer directory name to "%s".', layerName, newLayerDirectory) tempDirectory = "org.unifiedfontobject.normalizer.%d" % index subpathRenameDirectory(ufoPath, oldLayerDirectory, tempDirectory) fromTempMapping[tempDirectory] = newLayerDirectory for tempDirectory, newLayerDirectory in fromTempMapping.items(): subpathRenameDirectory(ufoPath, tempDirectory, newLayerDirectory) # update layercontents.plist newLayerMapping = list(newLayerMapping.items()) subpathWritePlist(newLayerMapping, ufoPath, "layercontents.plist") return newLayerMapping # ------ # Glyphs # ------ def normalizeUFO1And2GlyphsDirectory(ufoPath, modTimes): glyphMapping = normalizeGlyphNames(ufoPath, "glyphs") for fileName in sorted(glyphMapping.values()): location = subpathJoin("glyphs", fileName) if subpathNeedsRefresh(modTimes, ufoPath, location): log.debug('Normalizing "%s".', os.path.join("glyphs", fileName)) normalizeGLIF(ufoPath, "glyphs", fileName) modTimes[location] = subpathGetModTime(ufoPath, "glyphs", fileName) def normalizeGlyphsDirectory(ufoPath, layerDirectory, onlyModified=True, writeModTimes=True): if subpathExists(ufoPath, layerDirectory, "layerinfo.plist"): layerInfo = subpathReadPlist(ufoPath, layerDirectory, "layerinfo.plist") else: layerInfo = {} layerLib = layerInfo.get("lib", {}) imageReferences = {} if onlyModified: stored = readImageReferences(layerLib) if stored is not None: imageReferences = stored else: # we don't know what has a reference so we must check everything onlyModified = False if onlyModified: modTimes = readModTimes(layerLib) else: modTimes = {} glyphMapping = normalizeGlyphNames(ufoPath, layerDirectory) for fileName in glyphMapping.values(): if subpathNeedsRefresh(modTimes, ufoPath, layerDirectory, fileName): imageFileName = normalizeGLIF(ufoPath, layerDirectory, fileName) if imageFileName is not None: imageReferences[fileName] = imageFileName elif fileName in imageReferences: del imageReferences[fileName] modTimes[fileName] = subpathGetModTime(ufoPath, layerDirectory, fileName) if writeModTimes: storeModTimes(layerLib, modTimes) storeImageReferences(layerLib, imageReferences) layerInfo["lib"] = layerLib subpathWritePlist(layerInfo, ufoPath, layerDirectory, "layerinfo.plist") normalizeLayerInfoPlist(ufoPath, layerDirectory) referencedImages = set(imageReferences.values()) return referencedImages def normalizeLayerInfoPlist(ufoPath, layerDirectory): if subpathExists(ufoPath, layerDirectory, "layerinfo.plist"): _normalizePlistFile({}, ufoPath, *[layerDirectory, "layerinfo.plist"], preprocessor=_normalizeLayerInfoColor) def _normalizeLayerInfoColor(obj): """ - Normalize the color if specified. """ if "color" in obj: color = obj.pop("color") color = _normalizeColorString(color) if color is not None: obj["color"] = color def normalizeGlyphNames(ufoPath, layerDirectory): """ Normalize GLIF file names following UFO 3 user name to file name convention. """ # INVALID DATA POSSIBILITY: no contents.plist # INVALID DATA POSSIBILITY: file for glyph name may not exist # INVALID DATA POSSIBILITY: file for glyph may not be stored in contents if not subpathExists(ufoPath, layerDirectory, "contents.plist"): return {} oldGlyphMapping = subpathReadPlist(ufoPath, layerDirectory, "contents.plist") newGlyphMapping = {} newFileNames = set() for glyphName in sorted(oldGlyphMapping.keys()): newFileName = userNameToFileName(unicode(glyphName), newFileNames, suffix=".glif") newFileNames.add(newFileName.lower()) newGlyphMapping[glyphName] = newFileName # don't do a direct rewrite in case an old file has # the same name as a new file. fromTempMapping = {} for index, (glyphName, newFileName) in enumerate(sorted(newGlyphMapping.items())): oldFileName = oldGlyphMapping[glyphName] if newFileName == oldFileName: continue tempFileName = "org.unifiedfontobject.normalizer.%d" % index subpathRenameFile(ufoPath, (layerDirectory, oldFileName), (layerDirectory, tempFileName)) fromTempMapping[tempFileName] = newFileName for tempFileName, newFileName in fromTempMapping.items(): subpathRenameFile(ufoPath, (layerDirectory, tempFileName), (layerDirectory, newFileName)) # update contents.plist subpathWritePlist(newGlyphMapping, ufoPath, layerDirectory, "contents.plist") # normalize contents.plist _normalizePlistFile({}, ufoPath, layerDirectory, "contents.plist", removeEmpty=False) return newGlyphMapping def _test_normalizeGlyphNames(oldGlyphMapping, expectedGlyphMapping): import tempfile directory = tempfile.mkdtemp() layerDirectory = "glyphs" fullLayerDirectory = subpathJoin(directory, layerDirectory) os.mkdir(fullLayerDirectory) for fileName in oldGlyphMapping.values(): subpathWriteFile("", directory, layerDirectory, fileName) assert sorted(os.listdir(fullLayerDirectory)) == sorted(oldGlyphMapping.values()) subpathWritePlist(oldGlyphMapping, directory, layerDirectory, "contents.plist") newGlyphMapping = normalizeGlyphNames(directory, layerDirectory) listing = os.listdir(fullLayerDirectory) listing.remove("contents.plist") assert sorted(listing) == sorted(newGlyphMapping.values()) assert subpathReadPlist(directory, layerDirectory, "contents.plist") == newGlyphMapping shutil.rmtree(directory) return newGlyphMapping == expectedGlyphMapping # --------------- # Top-Level Files # --------------- # These are broken into separate, file specific # functions for clarity and in case file specific # normalization (such as filtering default values) # needs to occur. def _normalizePlistFile(modTimes, ufoPath, *subpath, **kwargs): if subpathNeedsRefresh(modTimes, ufoPath, *subpath): preprocessor = kwargs.get("preprocessor") data = subpathReadPlist(ufoPath, *subpath) if data: log.debug('Normalizing "%s".', os.path.join(*subpath)) text = normalizePropertyList(data, preprocessor=preprocessor) subpathWriteFile(text, ufoPath, *subpath) modTimes[subpath[-1]] = subpathGetModTime(ufoPath, *subpath) elif kwargs.get("removeEmpty", True): # Don't write empty plist files, unless 'removeEmpty' is False log.debug('Removing empty "%s".', os.path.join(*subpath)) subpathRemoveFile(ufoPath, *subpath) if subpath[-1] in modTimes: del modTimes[subpath[-1]] # metainfo.plist def normalizeMetaInfoPlist(ufoPath, modTimes): _normalizePlistFile(modTimes, ufoPath, "metainfo.plist", removeEmpty=False) # fontinfo.plist def normalizeFontInfoPlist(ufoPath, modTimes): _normalizePlistFile(modTimes, ufoPath, "fontinfo.plist", preprocessor=_normalizeFontInfoGuidelines) def _normalizeFontInfoGuidelines(obj): """ - Follow general guideline normalization rules. """ guidelines = obj.get("guidelines") if not guidelines: return normalized = [] for guideline in guidelines: guideline = _normalizeDictGuideline(guideline) if guideline is not None: normalized.append(guideline) obj["guidelines"] = normalized def _normalizeDictGuideline(guideline): """ - Don't write if angle is defined but either x or y are not defined. - Don't write if both x and y are defined but angle is not defined. """ x = guideline.get("x") y = guideline.get("y") angle = guideline.get("angle") name = guideline.get("name") color = guideline.get("color") identifier = guideline.get("identifier") # either x or y must be defined if x is None and y is None: return # if angle is specified, x and y must be specified if (x is None or y is None) and angle is not None: return # if x and y are specified, angle must be specified if (x is not None and y is not None) and angle is None: return # value errors if x is not None: try: x = float(x) except ValueError: return if y is not None: try: y = float(y) except ValueError: return if angle is not None: try: angle = float(angle) except ValueError: return normalized = {} if x is not None: normalized["x"] = x if y is not None: normalized["y"] = y if angle is not None: normalized["angle"] = angle if name is not None: normalized["name"] = name if color is not None: color = _normalizeColorString(color) if color is not None: normalized["color"] = color if identifier is not None: normalized["identifier"] = identifier return normalized # groups.plist def normalizeGroupsPlist(ufoPath, modTimes): _normalizePlistFile(modTimes, ufoPath, "groups.plist") # kerning.plist def normalizeKerningPlist(ufoPath, modTimes): _normalizePlistFile(modTimes, ufoPath, "kerning.plist") # layercontents.plist def normalizeLayerContentsPlist(ufoPath, modTimes): _normalizePlistFile(modTimes, ufoPath, "layercontents.plist", removeEmpty=False) # lib.plist def normalizeLibPlist(ufoPath): _normalizePlistFile({}, ufoPath, "lib.plist") # ----------------- # XML Normalization # ----------------- # Property List def normalizePropertyList(data, preprocessor=None): if preprocessor is not None: preprocessor(data) writer = XMLWriter(isPropertyList=True) writer.beginElement("plist", attrs=dict(version="1.0")) writer.propertyListObject(data) writer.endElement("plist") writer.raw("") return writer.getText() # GLIF def normalizeGLIFString(text, glifPath=None, imageFileRef=[]): tree = ET.fromstring(text) glifVersion = tree.attrib.get("format") if glifVersion is None: msg = "Undefined GLIF format" if glifPath is not None: msg += ": %s" % glifPath raise UFONormalizerError(msg) glifVersion = int(glifVersion) name = tree.attrib.get("name") # start the writer writer = XMLWriter() # grab the top-level elements advance = None unicodes = [] note = None image = None guidelines = [] anchors = [] outline = None lib = None for element in tree: tag = element.tag if tag == "advance": advance = element elif tag == "unicode": unicodes.append(element) elif tag == "note": note = element elif tag == "image": image = element elif tag == "guideline": guidelines.append(element) elif tag == "anchor": anchors.append(element) elif tag == "outline": outline = element elif tag == "lib": lib = element # write the data writer.beginElement("glyph", attrs=dict(name=name, format=glifVersion)) for uni in unicodes: _normalizeGlifUnicode(uni, writer) if advance is not None: _normalizeGlifAdvance(advance, writer) if glifVersion >= 2 and image is not None: imageFileRef[:] = image.attrib.get("fileName") _normalizeGlifImage(image, writer) if outline is not None: if glifVersion == 1: _normalizeGlifOutlineFormat1(outline, writer) else: _normalizeGlifOutlineFormat2(outline, writer) if glifVersion >= 2: for anchor in anchors: _normalizeGlifAnchor(anchor, writer) if glifVersion >= 2: for guideline in guidelines: _normalizeGlifGuideline(guideline, writer) if lib is not None: _normalizeGlifLib(lib, writer) if note is not None: _normalizeGlifNote(note, writer) writer.endElement("glyph") writer.raw("") return writer.getText() def normalizeGLIF(ufoPath, *subpath): """ - Normalize the mark color if specified. TO DO: need doctests The best way to test this is going to be have a GLIF that contains all of the element types. This can be round tripped and compared to make sure that the result matches the expectations. This GLIF doesn't need to contain a robust series of element variations as the testing of those will be handled by the element normalization functions. """ # INVALID DATA POSSIBILITY: format version that can't be converted to int # read and parse glifPath = subpathJoin(ufoPath, *subpath) text = subpathReadFile(ufoPath, *subpath) imageFileRef = [] normalizedText = normalizeGLIFString(text, glifPath, imageFileRef) subpathWriteFile(normalizedText, ufoPath, *subpath) # return the image reference imageFileName = imageFileRef[0] if imageFileRef else None return imageFileName def _normalizeGlifUnicode(element, writer): """ - Don't write unicode element if hex attribute is not defined. - Don't write unicode element if value for hex value is not a proper hex value. - Write hex value as all uppercase, zero padded string. """ v = element.attrib.get("hex") # INVALID DATA POSSIBILITY: no hex value if v: # INVALID DATA POSSIBILITY: invalid hex value try: d = int(v, 16) v = "%04X" % d except ValueError: return else: return writer.simpleElement("unicode", attrs=dict(hex=v)) def _normalizeGlifAdvance(element, writer): """ - Don't write default values (width=0, height=0) - Ignore values that can't be converted to a number. - Don't write an empty element. """ # INVALID DATA POSSIBILITY: value that can't be converted to float w = element.attrib.get("width", "0") h = element.attrib.get("height", "0") try: w = float(w) h = float(h) except ValueError: return attrs = {} # filter out default value (0) if w: attrs["width"] = w if h: attrs["height"] = h if not attrs: return writer.simpleElement("advance", attrs=attrs) def _normalizeGlifImage(element, writer): """ - Don't write if fileName is not defined. """ # INVALID DATA POSSIBILITY: no file name defined # INVALID DATA POSSIBILITY: non-existent file referenced fileName = element.attrib.get("fileName") if not fileName: return attrs = dict( fileName=fileName ) transformation = _normalizeGlifTransformation(element) attrs.update(transformation) color = element.attrib.get("color") if color is not None: attrs["color"] = _normalizeColorString(color) writer.simpleElement("image", attrs=attrs) def _normalizeGlifAnchor(element, writer): """ - Don't write if x or y are not defined. """ # INVALID DATA POSSIBILITY: no x defined # INVALID DATA POSSIBILITY: no y defined # INVALID DATA POSSIBILITY: x or y that can't be converted to float x = element.attrib.get("x") y = element.attrib.get("y") # x or y undefined if not x or not y: return # x or y improperly defined try: x = float(x) y = float(y) except ValueError: return attrs = dict( x=x, y=y ) name = element.attrib.get("name") if name is not None: attrs["name"] = name color = element.attrib.get("color") if color is not None: attrs["color"] = _normalizeColorString(color) identifier = element.attrib.get("identifier") if identifier is not None: attrs["identifier"] = identifier writer.simpleElement("anchor", attrs=attrs) def _normalizeGlifGuideline(element, writer): """ - Follow general guideline normalization rules. """ # INVALID DATA POSSIBILITY: x, y and angle not defined according to the spec # INVALID DATA POSSIBILITY: angle < 0 or > 360 # INVALID DATA POSSIBILITY: x, y or angle that can't be converted to float attrs = "x y angle color name identifier".split(" ") converted = {} for attr in attrs: converted[attr] = element.attrib.get(attr) normalized = _normalizeDictGuideline(converted) if normalized is not None: writer.simpleElement("guideline", attrs=normalized) def _normalizeGlifLib(element, writer): """ - Don't write an empty element. """ if not len(element): return obj = _convertPlistElementToObject(element[0]) if obj: # normalize the mark color if "public.markColor" in obj: color = obj.pop("public.markColor") color = _normalizeColorString(color) if color is not None: obj["public.markColor"] = color writer.beginElement("lib") writer.propertyListObject(obj) writer.endElement("lib") def _normalizeGlifNote(element, writer): """ - Don't write an empty element. """ value = element.text if not value: return if not value.strip(): return writer.beginElement("note") writer.text(value) writer.endElement("note") def _normalizeGlifOutlineFormat1(element, writer): """ - Don't write an empty element. - Don't write an empty contour. - Don't write an empty component. - Retain contour and component order except for implied anchors in < UFO 3. - If the UFO format < 3, move implied anchors to the end. """ if not len(element): return outline = [] anchors = [] for subElement in element: tag = subElement.tag if tag == "contour": contour = _normalizeGlifContourFormat1(subElement) if contour is None: continue if contour["type"] == "contour": outline.append(contour) else: anchors.append(contour) elif tag == "component": component = _normalizeGlifComponentFormat1(subElement) if component is None: continue if component is not None: outline.append(component) if not outline and not anchors: return writer.beginElement("outline") for obj in outline: t = obj.pop("type") if t == "contour": writer.beginElement("contour") for point in obj["points"]: writer.simpleElement("point", attrs=point) writer.endElement("contour") elif t == "component": writer.simpleElement("component", attrs=obj) for anchor in anchors: t = anchor.pop("type") writer.beginElement("contour") attrs = dict( type="move", x=anchor["x"], y=anchor["y"] ) if "name" in anchor: attrs["name"] = anchor["name"] writer.simpleElement("point", attrs=attrs) writer.endElement("contour") writer.endElement("outline") def _normalizeGlifContourFormat1(element): """ - Don't write unknown subelements. """ # INVALID DATA POSSIBILITY: unknown child element # INVALID DATA POSSIBILITY: unknown point type points = [] for subElement in element: tag = subElement.tag if tag != "point": continue attrs = _normalizeGlifPointAttributesFormat1(subElement) if not attrs: return points.append(attrs) if not points: return # anchor if len(points) == 1 and points[0].get("type") == "move": anchor = points[0] anchor["type"] = "anchor" return anchor # contour contour = dict(type="contour", points=points) return contour def _normalizeGlifPointAttributesFormat1(element): """ - Don't write if x or y is undefined. - Don't write default smooth value (no). - Don't write smooth for offcurves. - Don't write default point type attribute (offcurve). - Don't write subelements. - Don't write smooth if undefined. - Don't write unknown point types. """ # INVALID DATA POSSIBILITY: no x defined # INVALID DATA POSSIBILITY: no y defined # INVALID DATA POSSIBILITY: x or y that can't be converted to float # INVALID DATA POSSIBILITY: duplicate attributes x = element.attrib.get("x") y = element.attrib.get("y") if not x or not y: return {} try: x = float(x) y = float(y) except ValueError: return attrs = dict( x=x, y=y ) typ = element.attrib.get("type", "offcurve") if typ not in ("move", "line", "curve", "qcurve", "offcurve"): return {} if typ != "offcurve": attrs["type"] = typ smooth = element.attrib.get("smooth") if smooth == "yes": attrs["smooth"] = "yes" name = element.attrib.get("name") if name is not None: attrs["name"] = name return attrs def _normalizeGlifComponentFormat1(element): """ - Don't write if base is undefined. - Don't write subelements. """ # INVALID DATA POSSIBILITY: no base defined # INVALID DATA POSSIBILITY: unknown child element component = _normalizeGlifComponentAttributesFormat1(element) if not component: return component["type"] = "component" return component def _normalizeGlifComponentAttributesFormat1(element): """ - Don't write if base is not defined. - Don't write default transformation values. """ # INVALID DATA POSSIBILITY: no base defined # INVALID DATA POSSIBILITY: duplicate attributes base = element.attrib.get("base") if not base: return {} attrs = dict( base=element.attrib["base"] ) transformation = _normalizeGlifTransformation(element) attrs.update(transformation) return attrs def _normalizeGlifOutlineFormat2(element, writer): """ - Don't write an empty element. - Don't write an empty contour. - Don't write an empty component. - Retain contour and component order. - Don't write unknown subelements. """ outline = [] for subElement in element: tag = subElement.tag if tag == "contour": contour = _normalizeGlifContourFormat2(subElement) if contour: outline.append(contour) elif tag == "component": component = _normalizeGlifComponentFormat2(subElement) if component: outline.append(component) if not outline: return writer.beginElement("outline") for obj in outline: t = obj.pop("type") if t == "contour": attrs = {} identifier = obj.get("identifier") if identifier is not None: attrs["identifier"] = identifier writer.beginElement("contour", attrs=attrs) for point in obj["points"]: writer.simpleElement("point", attrs=point) writer.endElement("contour") elif t == "component": writer.simpleElement("component", attrs=obj) writer.endElement("outline") def _normalizeGlifContourFormat2(element): """ - Don't write unknown subelements. """ # INVALID DATA POSSIBILITY: unknown child element # INVALID DATA POSSIBILITY: unknown point type points = [] for subElement in element: tag = subElement.tag if tag != "point": continue attrs = _normalizeGlifPointAttributesFormat2(subElement) if not attrs: return points.append(attrs) if not points: return contour = dict(type="contour", points=points) identifier = element.attrib.get("identifier") if identifier is not None: contour["identifier"] = identifier return contour def _normalizeGlifPointAttributesFormat2(element): """ - Follow same rules as Format 1, but allow an identifier attribute. """ attrs = _normalizeGlifPointAttributesFormat1(element) identifier = element.attrib.get("identifier") if identifier is not None: attrs["identifier"] = identifier return attrs def _normalizeGlifComponentFormat2(element): """ - Folow the same rules as Format 1. """ # INVALID DATA POSSIBILITY: no base defined # INVALID DATA POSSIBILITY: unknown child element component = _normalizeGlifComponentAttributesFormat2(element) if not component: return component["type"] = "component" return component def _normalizeGlifComponentAttributesFormat2(element): """ - Follow same rules as Format 1, but allow an identifier attribute. """ attrs = _normalizeGlifComponentAttributesFormat1(element) identifier = element.attrib.get("identifier") if identifier is not None: attrs["identifier"] = identifier return attrs _glifDefaultTransformation = dict( xScale=1, xyScale=0, yxScale=0, yScale=1, xOffset=0, yOffset=0 ) def _normalizeGlifTransformation(element): """ - Don't write default values. """ attrs = {} for attr, default in _glifDefaultTransformation.items(): value = element.attrib.get(attr, default) try: value = float(value) except ValueError: continue if value != default: attrs[attr] = value return attrs def _normalizeColorString(value): """ - Write the string as comma separated numbers, folowing the number normalization rules. """ # INVALID DATA POSSIBILITY: bad color string # INVALID DATA POSSIBILITY: value < 0 or > 1 if value.count(",") != 3: return try: r, g, b, a = (float(i) for i in value.split(",")) except ValueError: return if any(x < 0 or x > 1 for x in (r, g, b, a)): return color = (xmlConvertFloat(i) for i in (r, g, b, a)) return ",".join(color) # Adapted from plistlib.datetime._date_from_string() def _dateFromString(text): import re _dateParser = re.compile(r"(?P\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\d\d))?)?)?)?)?Z") gd = _dateParser.match(text).groupdict() lst = [] for key in ('year', 'month', 'day', 'hour', 'minute', 'second'): val = gd[key] if val is None: break lst.append(int(val)) return datetime.datetime(*lst) def _dateToString(data): return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (data.year, data.month, data.day, data.hour, data.minute, data.second) def _convertPlistElementToObject(element): # INVALID DATA POSSIBILITY: invalid value string obj = None tag = element.tag if tag == "array": obj = [] for subElement in element: obj.append(_convertPlistElementToObject(subElement)) elif tag == "dict": obj = {} key = None for subElement in element: if subElement.tag == "key": key = subElement.text else: obj[key] = _convertPlistElementToObject(subElement) elif tag == "string": if not element.text: return "" return element.text elif tag == "data": if not element.text: return plistlib.Data.fromBase64("") return plistlib.Data.fromBase64(element.text) elif tag == "date": return _dateFromString(element.text) elif tag == "true": return True elif tag == "false": return False elif tag == "real": return float(element.text) elif tag == "integer": return int(element.text) return obj # XML Writer xmlDeclaration = "" plistDocType = "" xmlTextMaxLineLength = 70 xmlIndent = "\t" xmlLineBreak = "\n" xmlAttributeOrder = """ name base format fileName x y angle xScale xyScale yxScale yScale xOffset yOffset type smooth color identifier """.strip().splitlines() d = {} for index, attr in enumerate(xmlAttributeOrder): d[attr] = index xmlAttributeOrder = d class XMLWriter(object): def __init__(self, isPropertyList=False, declaration=xmlDeclaration): self._lines = [] if declaration: self._lines.append(declaration) if isPropertyList: self._lines.append(plistDocType) self._indentLevel = 0 self._stack = [] # text retrieval def getText(self): assert not self._stack return xmlLineBreak.join(self._lines) # writing def raw(self, line): if self._indentLevel: i = xmlIndent * self._indentLevel line = i + line self._lines.append(line) def data(self, text): line = "" % text self.raw(line) def text(self, text): text = text.strip() text = xmlEscapeText(text) paragraphs = [] for paragraph in text.splitlines(): if not paragraph: paragraphs.append("") else: paragraph = textwrap.wrap( paragraph.rstrip(), width=xmlTextMaxLineLength, expand_tabs=False, replace_whitespace=False, drop_whitespace=False, break_long_words=False, break_on_hyphens=False ) paragraphs.extend(paragraph) for line in paragraphs: self.raw(line) def simpleElement(self, tag, attrs={}, value=None): if attrs: attrs = self.attributesToString(attrs) line = "<%s %s" % (tag, attrs) else: line = "<%s" % tag if value is not None: line = "%s>%s" % (line, value, tag) else: line = "%s/>" % line self.raw(line) def beginElement(self, tag, attrs={}): if attrs: attrs = self.attributesToString(attrs) line = "<%s %s>" % (tag, attrs) else: line = "<%s>" % tag self.raw(line) self._stack.append(tag) self._indentLevel += 1 def endElement(self, tag): assert self._stack assert self._stack[-1] == tag del self._stack[-1] self._indentLevel -= 1 line = "" % (tag) self.raw(line) # property list def propertyListObject(self, data): if data is None: return if isinstance(data, (list, tuple)): self._plistArray(data) elif isinstance(data, dict): self._plistDict(data) elif isinstance(data, basestring): self._plistString(data) elif isinstance(data, bool): self._plistBoolean(data) elif isinstance(data, (int, long)): self._plistInt(data) elif isinstance(data, float): dataStr = xmlConvertFloat(data) try: data = int(dataStr) self._plistInt(data) except ValueError: self._plistFloat(data) elif isinstance(data, plistlib.Data): self._plistData(data) elif isinstance(data, datetime.datetime): self._plistDate(data) else: raise UFONormalizerError("Unknown data type in property list: %s" % repr(type(data))) def _plistArray(self, data): self.beginElement("array") for value in data: self.propertyListObject(value) self.endElement("array") def _plistDict(self, data): self.beginElement("dict") for key, value in sorted(data.items()): self.simpleElement("key", value=xmlEscapeText(key)) self.propertyListObject(value) self.endElement("dict") def _plistString(self, data): self.simpleElement("string", value=xmlEscapeText(data)) def _plistBoolean(self, data): if data: self.simpleElement("true") else: self.simpleElement("false") def _plistFloat(self, data): data = xmlConvertFloat(data) self.simpleElement("real", value=data) def _plistInt(self, data): data = xmlConvertInt(data) self.simpleElement("integer", value=data) def _plistDate(self, data): data = _dateToString(data) self.simpleElement("date", value=data) def _plistData(self, data): data = data.asBase64(maxlinelength=xmlTextMaxLineLength) if not data: self.simpleElement("data", value="") else: self.beginElement("data") for line in tostr(data).splitlines(): self.raw(line) self.endElement("data") # support def attributesToString(self, attrs): """ - Sort the known attributes in the preferred order. - Sort unknown attributes in alphabetical order and place them after the known attributes. - Format as space separated name="value". """ sorter = [ (xmlAttributeOrder.get(attr, 100), attr, value) for (attr, value) in attrs.items() ] formatted = [] for index, attr, value in sorted(sorter): attr = xmlEscapeAttribute(attr) value = xmlConvertValue(value) pair = "%s=\"%s\"" % (attr, value) formatted.append(pair) return " ".join(formatted) def xmlEscapeText(text): if text: text = text.replace("&", "&") text = text.replace("<", "<") text = text.replace(">", ">") return text def xmlEscapeAttribute(text): text = xmlEscapeText(text) text = text.replace("\"", """) return text def xmlConvertValue(value): if isinstance(value, float): return xmlConvertFloat(value) elif isinstance(value, int): return xmlConvertInt(value) value = xmlEscapeText(value) return value def xmlConvertFloat(value): if FLOAT_FORMAT is None: string = repr(value) if "e" in string: string = "%.16f" % value else: string = FLOAT_FORMAT % value if "." in string: string = string.rstrip("0") if string[-1] == ".": return xmlConvertInt(int(value)) return string def xmlConvertInt(value): return str(value) # --------------- # Path Operations # --------------- def duplicateUFO(inPath, outPath): """ Duplicate an entire UFO. """ if os.path.exists(outPath): shutil.rmtree(outPath) shutil.copytree(inPath, outPath) def subpathJoin(ufoPath, *subpath): """ Join path parts. """ if not isinstance(subpath, basestring): subpath = os.path.join(*subpath) return os.path.join(ufoPath, subpath) def subpathSplit(path): """ Split path parts. """ return os.path.split(path) def subpathExists(ufoPath, *subpath): """ Get a boolean indicating if a path exists. """ path = subpathJoin(ufoPath, *subpath) return os.path.exists(path) # read def subpathReadFile(ufoPath, *subpath): """ Read the contents of a file. """ path = subpathJoin(ufoPath, *subpath) with open(path, "r", encoding="utf-8") as f: text = f.read() return text def subpathReadPlist(ufoPath, *subpath): """ Read the contents of a property list and convert it into a Python object. """ path = subpathJoin(ufoPath, *subpath) with open(path, "rb") as f: data = f.read() return _loads(data) # write def subpathWriteFile(text, ufoPath, *subpath): """ Write data to a file. This will only modify the file if the file contains data that is different from the new data. """ path = subpathJoin(ufoPath, *subpath) if subpathExists(ufoPath, *subpath): existing = subpathReadFile(ufoPath, *subpath) else: existing = None if text != existing: # always use Unix LF end of lines with open(path, "w", encoding="utf-8", newline="\n") as f: f.write(text) def subpathWritePlist(data, ufoPath, *subpath): """ Write a Python object to a property list. THIS DOES NOT WRITE NORMALIZED OUTPUT. This will only modify the file if the file contains data that is different from the new data. """ data = _dumps(data) path = subpathJoin(ufoPath, *subpath) if subpathExists(ufoPath, *subpath): existing = subpathReadPlist(ufoPath, *subpath) else: existing = None if data != existing: with open(path, "wb") as f: f.write(data) # rename def subpathRenameFile(ufoPath, fromSubpath, toSubpath): """ Rename a file. """ if isinstance(fromSubpath, basestring): fromSubpath = [fromSubpath] if isinstance(toSubpath, basestring): toSubpath = [toSubpath] inPath = subpathJoin(ufoPath, *fromSubpath) outPath = subpathJoin(ufoPath, *toSubpath) os.rename(inPath, outPath) def subpathRenameDirectory(ufoPath, fromSubpath, toSubpath): """ Rename a directory. """ if isinstance(fromSubpath, basestring): fromSubpath = [fromSubpath] if isinstance(toSubpath, basestring): toSubpath = [toSubpath] inPath = subpathJoin(ufoPath, *fromSubpath) outPath = subpathJoin(ufoPath, *toSubpath) shutil.move(inPath, outPath) # remove def subpathRemoveFile(ufoPath, *subpath): """ Remove a file. """ if subpathExists(ufoPath, *subpath): path = subpathJoin(ufoPath, *subpath) os.remove(path) # mod times def subpathGetModTime(ufoPath, *subpath): """ Get the modification time for a file. """ path = subpathJoin(ufoPath, *subpath) return os.path.getmtime(path) def subpathNeedsRefresh(modTimes, ufoPath, *subPath): """ Determine if a file needs to be refreshed. Returns True if the file's latest modification time is different from its previous modification time. """ previous = modTimes.get(subPath[-1]) if previous is None: return True latest = subpathGetModTime(ufoPath, *subPath) return latest != previous # --------------- # Store Mod Times # --------------- def storeModTimes(lib, modTimes): """ Write the file mod times to the lib. """ lines = [ "version: %s" % __version__ ] for fileName, modTime in sorted(modTimes.items()): line = "%.1f %s" % (modTime, fileName) lines.append(line) text = "\n".join(lines) lib[modTimeLibKey] = text def readModTimes(lib): """ Read the file mod times from the lib. """ # TO DO: a version mismatch causing a complete # renomalization of existing files sucks. but, # I haven't been able to come up with a better # solution. maybe we could keep track of what # would need new normalization from version to # version and only trigger it as needed. most # new versions aren't going to require a complete # rerun of everything. text = lib.get(modTimeLibKey) if not text: return {} lines = text.splitlines() version = lines.pop(0).split(":")[-1].strip() if version != __version__: return {} modTimes = {} for line in lines: modTime, fileName = line.split(" ", 1) modTime = float(modTime) modTimes[fileName] = modTime return modTimes # ---------------- # Image Management # ---------------- def readImagesDirectory(ufoPath): """ Get a listing of all images in the images directory. """ pattern = subpathJoin(ufoPath, *["images", "*.png"]) imageNames = [subpathSplit(path)[-1] for path in glob.glob(pattern)] return set(imageNames) def purgeImagesDirectory(ufoPath, toPurge): """ Purge specified images from the images directory. """ for fileName in toPurge: if subpathExists(ufoPath, *["images", fileName]): path = subpathJoin(ufoPath, *["images", fileName]) os.remove(path) def storeImageReferences(lib, imageReferences): """ Store the image references. """ lib[imageReferencesLibKey] = imageReferences def readImageReferences(lib): """ Read the image references. """ references = lib.get(imageReferencesLibKey) return references # ---------------------- # User Name to File Name # ---------------------- # # This was taken directly from the UFO 3 specification. illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ") illegalCharacters += [chr(i) for i in range(1, 32)] illegalCharacters += [chr(0x7F)] reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ") reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ") maxFileNameLength = 255 class NameTranslationError(Exception): pass def userNameToFileName(userName, existing=[], prefix="", suffix=""): """ existing should be a case-insensitive list of all existing file names. """ # the incoming name must be a unicode string assert isinstance(userName, unicode), "The value for userName must be a unicode string." # establish the prefix and suffix lengths prefixLength = len(prefix) suffixLength = len(suffix) # replace an initial period with an _ # if no prefix is to be added if not prefix and userName[0] == ".": userName = "_" + userName[1:] # filter the user name filteredUserName = [] for character in userName: # replace illegal characters with _ if character in illegalCharacters: character = "_" # add _ to all non-lower characters elif character != character.lower(): character += "_" filteredUserName.append(character) userName = "".join(filteredUserName) # clip to 255 sliceLength = maxFileNameLength - prefixLength - suffixLength userName = userName[:sliceLength] # test for illegal files names parts = [] for part in userName.split("."): if part.lower() in reservedFileNames: part = "_" + part parts.append(part) userName = ".".join(parts) # test for clash fullName = prefix + userName + suffix if fullName.lower() in existing: fullName = handleClash1(userName, existing, prefix, suffix) # finished return fullName def handleClash1(userName, existing=[], prefix="", suffix=""): """ existing must be a case-insensitive list of all existing file names. """ # if the prefix length + user name length + suffix length + 15 is at # or past the maximum length, slice 15 characters off of the user name prefixLength = len(prefix) suffixLength = len(suffix) if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: length = (prefixLength + len(userName) + suffixLength + 15) sliceLength = maxFileNameLength - length userName = userName[:sliceLength] finalName = None # try to add numbers to create a unique name counter = 1 while finalName is None: name = userName + str(counter).zfill(15) fullName = prefix + name + suffix if fullName.lower() not in existing: finalName = fullName break else: counter += 1 if counter >= 999999999999999: break # if there is a clash, go to the next fallback if finalName is None: finalName = handleClash2(existing, prefix, suffix) # finished return finalName def handleClash2(existing=[], prefix="", suffix=""): """ existing must be a case-insensitive list of all existing file names. """ # calculate the longest possible string maxLength = maxFileNameLength - len(prefix) - len(suffix) maxValue = int("9" * maxLength) # try to find a number finalName = None counter = 1 while finalName is None: fullName = prefix + str(counter) + suffix if fullName.lower() not in existing: finalName = fullName break else: counter += 1 if counter >= maxValue: break # raise an error if nothing has been found if finalName is None: raise NameTranslationError("No unique name could be found.") # finished return finalName # ------- # Testing # ------- def _runProfile(outPath): normalizeUFO(outPath) def runTests(): # unit tests import unittest import sys # unittest.main() will try parsing arguments, "-t" in this case sys.argv = sys.argv[:1] testsdir = os.path.join(os.path.dirname(__file__), os.path.pardir, "tests") if not os.path.exists(os.path.join(testsdir, "test_ufonormalizer.py")): print("tests not found; run this from the source directory") return 1 # make sure 'tests' folder is on PYTHONPATH so unittest can import sys.path.append(testsdir) testrun = unittest.main("test_ufonormalizer", exit=False, verbosity=2) # test file searching ufo_dir = os.path.join(testsdir, "data") paths = [] pattern = os.path.join(ufo_dir, "*.ufo") for inPath in glob.glob(pattern): if inPath.endswith("-n.ufo"): continue outPath = os.path.splitext(inPath)[0] + "-n.ufo" if os.path.exists(outPath): shutil.rmtree(outPath) paths.append((inPath, outPath)) if paths: # profile test import cProfile inPath, outPath = paths[0] shutil.copytree(inPath, outPath) cProfile.run("_runProfile('%s')" % outPath, sort="tottime") shutil.rmtree(outPath) # general test import time for inPath, outPath in paths: shutil.copytree(inPath, outPath) s = time.time() normalizeUFO(outPath) t = time.time() - s print(os.path.basename(inPath) + ":", t, "seconds") return not testrun.result.wasSuccessful() if __name__ == "__main__": main() ufoNormalizer-0.3.6/tests/000077500000000000000000000000001340575203400155215ustar00rootroot00000000000000ufoNormalizer-0.3.6/tests/__init__.py000066400000000000000000000000001340575203400176200ustar00rootroot00000000000000ufoNormalizer-0.3.6/tests/data/000077500000000000000000000000001340575203400164325ustar00rootroot00000000000000ufoNormalizer-0.3.6/tests/data/glif/000077500000000000000000000000001340575203400173535ustar00rootroot00000000000000ufoNormalizer-0.3.6/tests/data/glif/format1.glif000066400000000000000000000015371340575203400215750ustar00rootroot00000000000000 abc com.letterror.somestuff arbitrary custom data! ufoNormalizer-0.3.6/tests/data/glif/format2.glif000066400000000000000000000021331340575203400215670ustar00rootroot00000000000000 abc com.letterror.somestuff arbitrary custom data! public.markColor 1,0,0,0.5 arbitrary text about the glyph ufoNormalizer-0.3.6/tests/data/glif/formatNone.glif000066400000000000000000000000761340575203400223310ustar00rootroot00000000000000 ufoNormalizer-0.3.6/tests/test_ufonormalizer.py000066400000000000000000002472451340575203400220440ustar00rootroot00000000000000 # -*- coding: utf-8 -*- from __future__ import unicode_literals import os import sys import unittest import tempfile import shutil import datetime import plistlib import base64 from io import open from xml.etree import cElementTree as ET from ufonormalizer import ( normalizeGLIF, normalizeGlyphsDirectoryNames, normalizeGlyphNames, subpathJoin, subpathSplit, subpathExists, subpathReadFile, subpathReadPlist, subpathWriteFile, subpathWritePlist, subpathRenameFile, subpathRenameDirectory, subpathRenameDirectory, subpathRemoveFile, subpathGetModTime, subpathNeedsRefresh, modTimeLibKey, storeModTimes, readModTimes, UFONormalizerError, XMLWriter, tobytes, userNameToFileName, handleClash1, handleClash2, xmlEscapeText, xmlEscapeAttribute, xmlConvertValue, xmlConvertFloat, xmlConvertInt, _normalizeGlifAnchor, _normalizeGlifGuideline, _normalizeGlifLib, _normalizeGlifNote, _normalizeFontInfoGuidelines, _normalizeGlifUnicode, _normalizeGlifAdvance, _normalizeGlifImage, _normalizeDictGuideline, _normalizeLayerInfoColor, _normalizeGlifOutlineFormat1, _normalizeGlifContourFormat1, _normalizeGlifPointAttributesFormat1, _normalizeGlifComponentFormat1, _normalizeGlifComponentAttributesFormat1, _normalizeGlifOutlineFormat2, _normalizeGlifContourFormat2, _normalizeGlifPointAttributesFormat2, _normalizeGlifComponentAttributesFormat2, _normalizeGlifTransformation, _normalizeColorString, _convertPlistElementToObject, _normalizePlistFile, main, xmlDeclaration, plistDocType) from ufonormalizer import __version__ as ufonormalizerVersion # Python 3.4 deprecated readPlistFromBytes and writePlistToBytes # Python 2 has readPlistFromString and writePlistToString try: from plistlib import loads, dumps except ImportError: try: from plistlib import readPlistFromBytes as loads from plistlib import writePlistToBytes as dumps except ImportError: from plistlib import readPlistFromString as loads from plistlib import writePlistToString as dumps try: # python 2: a stream of *byte* strings from StringIO import StringIO except ImportError: # Python 3: a stream of *unicode* strings from io import StringIO try: from tempfile import TemporaryDirectory # Python 3 only except ImportError: # backport for Python 2.7 class TemporaryDirectory(object): """ Create and return a temporary directory. This has the same behavior as mkdtemp but can be used as a context manager. Adapted from tempfile.TemporaryDirectory (new in Python 3.2). """ def __init__(self, suffix="", prefix="tmp", dir=None): self._closed = False self.name = tempfile.mkdtemp(suffix, prefix, dir) def __enter__(self): return self.name def cleanup(self, _warn=False): if self.name and not self._closed: shutil.rmtree(self.name) self._closed = True def __exit__(self, exc, value, tb): self.cleanup() GLIFFORMAT1 = '''\ abc com.letterror.somestuff arbitrary custom data! ''' GLIFFORMAT2 = '''\ abc com.letterror.somestuff arbitrary custom data! public.markColor 1,0,0,0.5 arbitrary text about the glyph ''' INFOPLIST_GUIDELINES = """\ guidelines x1 y2 angle3 color1,0,0,.5 x4 y5 angle6 color0,1,0,.5 x7 y8 angle9 colorinvalid """ INFOPLIST_NO_GUIDELINES = '''\ guidelines ''' EMPTY_PLIST = "\n".join([xmlDeclaration, plistDocType, '']) METAINFO_PLIST = "\n".join([xmlDeclaration, plistDocType, """\ creator org.robofab.ufoLib formatVersion %d """]) class redirect_stderr(object): """ Context manager for temporarily redirecting stderr to another file. Adapted from CPython 3.5 'contextlib._RedirectStream' source: https://hg.python.org/cpython/file/3.5/Lib/contextlib.py#l162 """ def __init__(self, new_target): self._new_target = new_target # We use a list of old targets to make this CM re-entrant self._old_targets = [] def __enter__(self): self._old_targets.append(sys.stderr) sys.stderr = self._new_target return self._new_target def __exit__(self, exctype, excinst, exctb): sys.stderr = self._old_targets.pop() class UFONormalizerErrorTest(unittest.TestCase): def test_str(self): err = UFONormalizerError("Testing Error!") self.assertEqual(str(err), "Testing Error!") class UFONormalizerTest(unittest.TestCase): def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) # Python 3 renamed assertRaisesRegexp to assertRaisesRegex. if not hasattr(self, "assertRaisesRegex"): self.assertRaisesRegex = self.assertRaisesRegexp def _test_normalizeGlyphsDirectoryNames(self, oldLayers, expectedLayers): directory = tempfile.mkdtemp() for layerName, subDirectory in oldLayers: os.mkdir(os.path.join(directory, subDirectory)) self.assertEqual( sorted(os.listdir(directory)), sorted([oldDirectory for oldName, oldDirectory in oldLayers])) subpathWritePlist(oldLayers, directory, "layercontents.plist") newLayers = normalizeGlyphsDirectoryNames(directory) listing = os.listdir(directory) listing.remove("layercontents.plist") self.assertEqual( sorted(listing), sorted([newDirectory for newName, newDirectory in newLayers])) shutil.rmtree(directory) return newLayers == expectedLayers def _test_normalizeGlyphNames(self, oldGlyphMapping, expectedGlyphMapping): import tempfile directory = tempfile.mkdtemp() layerDirectory = "glyphs" fullLayerDirectory = subpathJoin(directory, layerDirectory) os.mkdir(fullLayerDirectory) for fileName in oldGlyphMapping.values(): subpathWriteFile("", directory, layerDirectory, fileName) self.assertEqual(sorted(os.listdir(fullLayerDirectory)), sorted(oldGlyphMapping.values())) subpathWritePlist(oldGlyphMapping, directory, layerDirectory, "contents.plist") newGlyphMapping = normalizeGlyphNames(directory, layerDirectory) listing = os.listdir(fullLayerDirectory) listing.remove("contents.plist") self.assertEqual(sorted(listing), sorted(newGlyphMapping.values())) self.assertEqual( subpathReadPlist(directory, layerDirectory, "contents.plist"), newGlyphMapping) shutil.rmtree(directory) return newGlyphMapping == expectedGlyphMapping def test_normalizeGlyphsDirectoryNames_non_standard(self): oldLayers = [ ("public.default", "glyphs"), ("Sketches", "glyphs.sketches"), ] expectedLayers = [ ("public.default", "glyphs"), ("Sketches", "glyphs.S_ketches"), ] self.assertTrue( self._test_normalizeGlyphsDirectoryNames( oldLayers, expectedLayers)) def test_normalizeGlyphsDirectoryNames_old_same_as_new(self): oldLayers = [ ("public.default", "glyphs"), ("one", "glyphs.two"), ("two", "glyphs.three") ] expectedLayers = [ ("public.default", "glyphs"), ("one", "glyphs.one"), ("two", "glyphs.two") ] self.assertTrue( self._test_normalizeGlyphsDirectoryNames( oldLayers, expectedLayers)) def test_normalizeLayerInfoPlist_color(self): obj = dict(color="1,0,0,.5") _normalizeLayerInfoColor(obj) self.assertEqual(obj, {'color': '1,0,0,0.5'}) obj = dict(color="invalid") _normalizeLayerInfoColor(obj) self.assertEqual(obj, {}) def test_normalizeGlyphNames_non_standard(self): oldNames = { "A": "a.glif", "B": "b.glif" } expectedNames = { "A": "A_.glif", "B": "B_.glif" } self.assertTrue( self._test_normalizeGlyphNames(oldNames, expectedNames)) def test_normalizeGlyphNames_old_same_as_new(self): oldNames = { "one": "two.glif", "two": "three.glif" } expectedNames = { "one": "one.glif", "two": "two.glif" } self.assertTrue( self._test_normalizeGlyphNames(oldNames, expectedNames)) def test_normalizeFontInfoPlist_guidelines(self): test = INFOPLIST_GUIDELINES expected = { "guidelines": [ dict(x=1, y=2, angle=3, color="1,0,0,0.5"), dict(x=4, y=5, angle=6, color="0,1,0,0.5"), dict(x=7, y=8, angle=9), ] } plist = loads(tobytes(test)) _normalizeFontInfoGuidelines(plist) self.assertEqual(plist, expected) def test_normalizeFontInfoPlist_no_guidelines(self): test = INFOPLIST_NO_GUIDELINES plist = loads(tobytes(test)) self.assertIsNone(_normalizeFontInfoGuidelines(plist)) def test_normalizeFontInfoPlist_guidelines_everything(self): guideline = dict(x=1, y=2, angle=3, name="test", color="1,0,0,.5", identifier="TEST") expected = dict(x=1, y=2, angle=3, name="test", color="1,0,0,0.5", identifier="TEST") result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_no_x(self): guideline = dict(y=2, name="test", color="1,0,0,.5", identifier="TEST") expected = dict(y=2, name="test", color="1,0,0,0.5", identifier="TEST") result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) guideline = dict(y=2, angle=3, name="test", color="1,0,0,.5", identifier="TEST") expected = None result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_invalid_x(self): guideline = dict(x="invalid", y=2, angle=3, name="test", color="1,0,0,.5", identifier="TEST") expected = None result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_no_y(self): guideline = dict(x=1, name="test", color="1,0,0,.5", identifier="TEST") expected = dict(x=1, name="test", color="1,0,0,0.5", identifier="TEST") result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) guideline = dict(x=1, angle=3, name="test", color="1,0,0,.5", identifier="TEST") expected = None result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_invalid_y(self): guideline = dict(x=1, y="invalid", angle=3, name="test", color="1,0,0,.5", identifier="TEST") expected = None result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_no_angle(self): guideline = dict(x=1, y=2, name="test", color="1,0,0,.5", identifier="TEST") expected = None result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_invalid_angle(self): guideline = dict(x=1, y=3, angle="invalid", name="test", color="1,0,0,.5", identifier="TEST") expected = None result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_no_name(self): guideline = dict(x=1, y=2, angle=3, color="1,0,0,.5", identifier="TEST") expected = dict(x=1, y=2, angle=3, color="1,0,0,0.5", identifier="TEST") result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_no_color(self): guideline = dict(x=1, y=2, angle=3, name="test", identifier="TEST") expected = dict(x=1, y=2, angle=3, name="test", identifier="TEST") result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_no_identifier(self): guideline = dict(x=1, y=2, angle=3, name="test", color="1,0,0,.5") expected = dict(x=1, y=2, angle=3, name="test", color="1,0,0,0.5") result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def test_normalizeFontInfoPlist_guidelines_zero_is_not_None(self): guideline = dict(x=0, y=0, angle=0) expected = dict(x=0, y=0, angle=0) result = _normalizeDictGuideline(guideline) self.assertEqual(result, expected) def _test_glifFormat(self): glifFormat = {} glifFormat[1] = GLIFFORMAT1.replace(" ", "\t") glifFormat[2] = GLIFFORMAT2.replace(" ", "\t") return glifFormat def test_normalizeGLIF_formats_1_and_2(self): self.maxDiff = None glifFormat = self._test_glifFormat() glifFolderPath = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'data', 'glif') for i in [1, 2]: glifFileName = 'format%s.glif' % i glifFilePath = os.path.join(glifFolderPath, glifFileName) normalizeGLIF(glifFolderPath, glifFileName) glifFile = open(glifFilePath, 'r') glifFileData = glifFile.read() glifFile.close() self.assertEqual(glifFileData, glifFormat[i]) def test_normalizeGLIF_no_formats(self): glifFileName = 'formatNone.glif' glifFolderPath = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'data', 'glif') with self.assertRaisesRegex( UFONormalizerError, r"Undefined GLIF format: .*formatNone.glif"): normalizeGLIF(glifFolderPath, glifFileName) def test_normalizeGLIF_unicode_without_hex(self): element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_unicode_with_hex(self): element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifUnicode(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_advance_undefined(self): element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_advance_defaults(self): element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_advance_width(self): element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_advance_height(self): element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_advance_invalid_values(self): element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring('') writer = XMLWriter(declaration=None) _normalizeGlifAdvance(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_image_everything(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifImage(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_image_empty(self): element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifImage(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_image_no_file_name(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifImage(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_image_no_transformation(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifImage(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_image_no_color(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifImage(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_anchor_everything(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_anchor_no_name(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_anchor_no_x(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_anchor_no_y(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_anchor_no_color(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_anchor_no_identifier(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifAnchor(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_guideline_everything(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifGuideline(element, writer) self.assertEqual( writer.getText(), '') def test_normalizeGLIF_guideline_invalid(self): element = ET.fromstring( "") writer = XMLWriter(declaration=None) _normalizeGlifGuideline(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_lib_defined(self): e = ''' foo bar abc def '''.strip() element = ET.fromstring(e) writer = XMLWriter(declaration=None) _normalizeGlifLib(element, writer) self.assertEqual( writer.getText(), '\n\t\n' '\t\tabc\n\t\t\n' '\t\tdef\n\t\t\n' '\t\tfoo\n\t\tbar\n' '\t\n') def test_normalizeGLIF_lib_undefined(self): element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifLib(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifLib(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_note_defined(self): element = ET.fromstring("Blah") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual(writer.getText(), "\n\tBlah\n") element = ET.fromstring(" Blah \t\n\t ") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual(writer.getText(), "\n\tBlah\n") element = ET.fromstring( tobytes("Don't forget to check the béziers!!", encoding="utf8")) writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tDon't forget to check the b\xe9ziers!!\n") element = ET.fromstring( tobytes("A quick brown fox jumps over the lazy dog.\n" "Příliš žluťoučký kůň úpěl ďábelské ódy.", encoding="utf-8")) writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tA quick brown fox jumps over the lazy dog.\n\t" "P\u0159\xedli\u0161 \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 " "\xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy.\n") element = ET.fromstring( " Line1 \t\n\n Line3\t ") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tLine1\n\t\n\t Line3\n") def test_normalizeGLIF_note_undefined(self): element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring(" ") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("\n\n") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual(writer.getText(), '') element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_outline_format1_empty(self): outline = "" element = ET.fromstring(outline) writer = XMLWriter(declaration=None) _normalizeGlifOutlineFormat1(element, writer) self.assertEqual(writer.getText(), '') outline = "\n" element = ET.fromstring(outline) writer = XMLWriter(declaration=None) _normalizeGlifOutlineFormat1(element, writer) self.assertEqual(writer.getText(), '') outline = "\n\t\n\t\n" element = ET.fromstring(outline) writer = XMLWriter(declaration=None) _normalizeGlifOutlineFormat1(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGLIF_outline_format1_element_order(self): outline = '''\ '''.strip().replace(" "*12, "") expected = '''\ '''.strip().replace(" "*12, "").replace(" ", "\t") element = ET.fromstring(outline) writer = XMLWriter(declaration=None) _normalizeGlifOutlineFormat1(element, writer) self.assertEqual(writer.getText(), expected) def test_normalizeGlif_contour_format1_empty(self): contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat1(element)) contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat1(element)) def test_normalizeGlif_contour_format1_point_without_attributes(self): contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat1(element)) def test_normalizeGlif_contour_format1_unkown_child_element(self): contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat1(element)) def test_normalizeGlif_contour_format1_unkown_point_type(self): contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat1(element)) def test_normalizeGlif_contour_format1_implied_anchor(self): contour = ''' ''' element = ET.fromstring(contour) self.assertEqual( sorted(_normalizeGlifContourFormat1(element).items()), [('name', 'anchor1'), ('type', 'anchor'), ('x', 0.0), ('y', 0.0)]) def test_normalizeGlif_contour_format1_implied_anchor_with_empty_name(self): contour = ''' ''' element = ET.fromstring(contour) self.assertEqual( sorted(_normalizeGlifContourFormat1(element).items()), [('name', ''), ('type', 'anchor'), ('x', 0.0), ('y', 0.0)]) def test_normalizeGlif_contour_format1_implied_anchor_without_name(self): contour = ''' ''' element = ET.fromstring(contour) self.assertEqual( sorted(_normalizeGlifContourFormat1(element).items()), [('type', 'anchor'), ('x', 0.0), ('y', 0.0)]) def test_normalizeGlif_contour_format1_normal(self): contour = ''' ''' element = ET.fromstring(contour) result = _normalizeGlifContourFormat1(element) result["type"] 'contour' self.assertEqual(len(result["points"]), 1) self.assertEqual( sorted(result["points"][0].items()), [('type', 'line'), ('x', 0.0), ('y', 0.0)]) contour = ''' ''' element = ET.fromstring(contour) result = _normalizeGlifContourFormat1(element) result["type"] 'contour' self.assertEqual(len(result["points"]), 2) self.assertEqual( sorted(result["points"][0].items()), [('type', 'move'), ('x', 0.0), ('y', 0.0)]) self.assertEqual( sorted(result["points"][1].items()), [('type', 'line'), ('x', 1.0), ('y', 1.0)]) def test_normalizeGlif_point_attributes_format1_everything(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('name', 'test'), ('smooth', 'yes'), ('type', 'line'), ('x', 1.0), ('y', 2.5)]) def test_normalizeGlif_point_attributes_format1_no_x(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), []) def test_normalizeGlif_point_attributes_format1_no_y(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), []) def test_normalizeGlif_point_attributes_format1_invalid_x(self): point = "" element = ET.fromstring(point) self.assertIsNone(_normalizeGlifPointAttributesFormat1(element)) def test_normalizeGlif_point_attributes_format1_invalid_y(self): point = "" element = ET.fromstring(point) self.assertIsNone(_normalizeGlifPointAttributesFormat1(element)) def test_normalizeGlif_point_attributes_format1_no_name(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('smooth', 'yes'), ('type', 'line'), ('x', 1.0), ('y', 2.5)]) def test_normalizeGlif_point_attributes_format1_empty_name(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('name', ''), ('smooth', 'yes'), ('type', 'line'), ('x', 1.0), ('y', 2.5)]) def test_normalizeGlif_point_attributes_format1_type_and_smooth(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('smooth', 'yes'), ('type', 'move'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'move'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'move'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('smooth', 'yes'), ('type', 'line'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'line'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'line'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('smooth', 'yes'), ('type', 'curve'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'curve'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'curve'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('smooth', 'yes'), ('type', 'qcurve'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'qcurve'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('type', 'qcurve'), ('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('x', 1.0), ('y', 2.5)]) point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), []) def test_normalizeGlif_point_attributes_format1_subelement(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat1(element).items()), [('x', 1.0), ('y', 2.5)]) def test_normalizeGlif_component_format1_everything(self): component = "" element = ET.fromstring(component) self.assertEqual( sorted(_normalizeGlifComponentFormat1(element).items()), [('base', 'test'), ('type', 'component'), ('xOffset', 5.0), ('xScale', 10.0), ('xyScale', 2.2), ('yOffset', 6.6), ('yScale', 4.4), ('yxScale', 3.0)]) def test_normalizeGlif_component_format1_no_base(self): component = "" element = ET.fromstring(component) _normalizeGlifComponentFormat1(element) def test_normalizeGlif_component_format1_subelement(self): component = "" element = ET.fromstring(component) self.assertEqual( sorted(_normalizeGlifComponentFormat1(element).items()), [('base', 'test'), ('type', 'component')]) def test_normalizeGlif_component_attributes_format1_everything(self): component = "" element = ET.fromstring(component) self.assertEqual( sorted(_normalizeGlifComponentAttributesFormat1(element).items()), [('base', 'test'), ('xOffset', 5.0), ('xScale', 10.0), ('xyScale', 2.2), ('yOffset', 6.6), ('yScale', 4.4), ('yxScale', 3.0)]) def test_normalizeGlif_component_attributes_format1_no_base(self): component = "" element = ET.fromstring(component) self.assertEqual( sorted(_normalizeGlifComponentAttributesFormat1(element).items()), []) def test_normalizeGlif_component_attributes_format1_no_transformation(self): component = "" element = ET.fromstring(component) self.assertEqual( sorted(_normalizeGlifComponentAttributesFormat1(element).items()), [('base', 'test')]) def test_normalizeGlif_component_attributes_format1_defaults(self): component = "" element = ET.fromstring(component) self.assertEqual( sorted(_normalizeGlifComponentAttributesFormat1(element).items()), [('base', 'test')]) def test_normalizeGlif_outline_format2_empty(self): outline = ''' ''' element = ET.fromstring(outline) writer = XMLWriter(declaration=None) _normalizeGlifOutlineFormat2(element, writer) self.assertEqual(writer.getText(), '') outline = ''' ''' element = ET.fromstring(outline) writer = XMLWriter(declaration=None) _normalizeGlifOutlineFormat2(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeGlif_outline_format2_element_order(self): outline = ''' '''.strip() expected = '\n'\ '\t\n'\ '\t\t\n'\ '\t\n'\ '\t\n'\ '\t\n'\ '\t\t\n'\ '\t\n'\ '\t\n'\ '' element = ET.fromstring(outline) writer = XMLWriter(declaration=None) _normalizeGlifOutlineFormat2(element, writer) self.assertEqual(writer.getText(), expected) def test_normalizeGlif_contour_format2_empty(self): contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat2(element)) def test_normalizeGlif_contour_format2_point_without_attributes(self): contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat2(element)) def test_normalizeGlif_contour_format2_unknown_child_element(self): contour = ''' ''' element = ET.fromstring(contour) self.assertIsNone(_normalizeGlifContourFormat2(element)) def test_normalizeGlif_contour_format2_normal(self): contour = ''' ''' element = ET.fromstring(contour) result = _normalizeGlifContourFormat2(element) self.assertEqual(result["type"], 'contour') self.assertEqual(result["identifier"], 'test') self.assertEqual(len(result["points"]), 1) self.assertEqual(sorted(result["points"][0].items()), [('type', 'line'), ('x', 0.0), ('y', 0.0)]) contour = ''' ''' element = ET.fromstring(contour) result = _normalizeGlifContourFormat2(element) self.assertEqual(result["type"], 'contour') self.assertEqual(result["identifier"], 'test') self.assertEqual(len(result["points"]), 2) self.assertEqual(sorted(result["points"][0].items()), [('type', 'move'), ('x', 0.0), ('y', 0.0)]) self.assertEqual(sorted(result["points"][1].items()), [('type', 'line'), ('x', 1.0), ('y', 1.0)]) def test_normalizeGlif_point_attributes_format2_everything(self): point = "" element = ET.fromstring(point) self.assertEqual( sorted(_normalizeGlifPointAttributesFormat2(element).items()), [('identifier', 'TEST'), ('name', 'test'), ('smooth', 'yes'), ('type', 'line'), ('x', 1.0), ('y', 2.5)]) def test_normalizeGlif_component_attributes_format2_everything(self): component = "" element = ET.fromstring(component) self.assertEqual( sorted(_normalizeGlifComponentAttributesFormat2(element).items()), [('base', 'test'), ('identifier', 'test'), ('xOffset', 5.0), ('xScale', 10.0), ('xyScale', 2.2), ('yOffset', 6.6), ('yScale', 4.4), ('yxScale', 3.0)]) def test_normalizeGlif_transformation_empty(self): element = ET.fromstring("") self.assertEqual(_normalizeGlifTransformation(element), {}) def test_normalizeGlif_transformation_default(self): element = ET.fromstring("") self.assertEqual(_normalizeGlifTransformation(element), {}) def test_normalizeGlif_transformation_non_default(self): element = ET.fromstring("") self.assertEqual( sorted(_normalizeGlifTransformation(element).items()), [('xOffset', 6.0), ('xScale', 2.0), ('xyScale', 3.0), ('yOffset', 7.0), ('yScale', 5.0), ('yxScale', 4.0)]) def test_normalizeGlif_transformation_invalid_value(self): element = ET.fromstring("") self.assertEqual(_normalizeGlifTransformation(element), {}) def test_normalizeGlif_transformation_unknown_attribute(self): element = ET.fromstring("") self.assertEqual(_normalizeGlifTransformation(element), {}) def test_normalize_color_string(self): _normalizeColorString("") _normalizeColorString("1,1,1") self.assertEqual(_normalizeColorString("1,1,1,1"), '1,1,1,1') self.assertEqual(_normalizeColorString(".1,.1,.1,.1"), '0.1,0.1,0.1,0.1') _normalizeColorString("1,1,1,a") _normalizeColorString("1,1,-1,1") _normalizeColorString("1,2,1,1") _normalizeColorString(",,,") def test_convert_plist_Element_to_object(self): element = ET.fromstring("") self.assertEqual(_convertPlistElementToObject(element), []) element = ET.fromstring("0.1") self.assertEqual(_convertPlistElementToObject(element), [0, 0.1]) element = ET.fromstring("") self.assertEqual(_convertPlistElementToObject(element), {}) element = ET.fromstring("foobar") self.assertEqual(_convertPlistElementToObject(element), {'foo': 'bar'}) element = ET.fromstring("A&BB&A") self.assertEqual(_convertPlistElementToObject(element), {'A&B': 'B&A'}) element = ET.fromstring("foo") self.assertEqual(_convertPlistElementToObject(element), 'foo') element = ET.fromstring("&") self.assertEqual(_convertPlistElementToObject(element), '&') element = ET.fromstring("2015-07-05T22:16:18Z") self.assertEqual(_convertPlistElementToObject(element), datetime.datetime(2015, 7, 5, 22, 16, 18)) element = ET.fromstring("") self.assertEqual(_convertPlistElementToObject(element), True) element = ET.fromstring("") self.assertEqual(_convertPlistElementToObject(element), False) element = ET.fromstring("1.1") self.assertEqual(_convertPlistElementToObject(element), 1.1) element = ET.fromstring("1") self.assertEqual(_convertPlistElementToObject(element), 1) element = ET.fromstring("YWJj") self.assertEqual(_convertPlistElementToObject(element), plistlib.Data(b'abc')) def test_main_verbose_or_quiet(self): stream = StringIO() with self.assertRaisesRegex(SystemExit, '2'): with redirect_stderr(stream): main(['-v', '-q', 'test.ufo']) self.assertTrue("options are mutually exclusive" in stream.getvalue()) def test_main_no_path(self): stream = StringIO() with self.assertRaisesRegex(SystemExit, '2'): with redirect_stderr(stream): main([]) self.assertTrue("No input path" in stream.getvalue()) def test_main_input_does_not_exist(self): stream = StringIO() with self.assertRaisesRegex(SystemExit, '2'): with redirect_stderr(stream): main(['foobarbazquz']) self.assertTrue("Input path does not exist" in stream.getvalue()) def test_main_input_not_ufo(self): # I use the path to the test module itself existing_not_ufo_file = os.path.realpath(__file__) stream = StringIO() with self.assertRaisesRegex(SystemExit, '2'): with redirect_stderr(stream): main([existing_not_ufo_file]) self.assertTrue("Input path is not a UFO" in stream.getvalue()) def test_main_invalid_float_precision(self): stream = StringIO() with TemporaryDirectory(suffix=".ufo") as tmp: with self.assertRaisesRegex(SystemExit, '2'): with redirect_stderr(stream): main(['--float-precision', '-10', tmp]) self.assertTrue("float precision must be >= 0" in stream.getvalue()) def test_main_no_metainfo_plist(self): with TemporaryDirectory(suffix=".ufo") as tmp: with self.assertRaisesRegex( UFONormalizerError, 'Required metainfo.plist file not in'): main([tmp]) def test_main_metainfo_unsupported_formatVersion(self): metainfo = METAINFO_PLIST % 1984 with TemporaryDirectory(suffix=".ufo") as tmp: with open(os.path.join(tmp, "metainfo.plist"), 'w') as f: f.write(metainfo) with self.assertRaisesRegex( UFONormalizerError, 'Unsupported UFO format'): main([tmp]) def test_main_metainfo_no_formatVersion(self): metainfo = EMPTY_PLIST with TemporaryDirectory(suffix=".ufo") as tmp: with open(os.path.join(tmp, "metainfo.plist"), 'w') as f: f.write(metainfo) with self.assertRaisesRegex( UFONormalizerError, 'Required formatVersion value not defined'): main([tmp]) def test_main_metainfo_invalid_formatVersion(self): metainfo = "\n".join([xmlDeclaration, plistDocType, """\ formatVersion foobar """]) with TemporaryDirectory(suffix=".ufo") as tmp: with open(os.path.join(tmp, "metainfo.plist"), 'w') as f: f.write(metainfo) with self.assertRaisesRegex( UFONormalizerError, 'Required formatVersion value not properly formatted'): main([tmp]) def test_main_outputPath_duplicateUFO(self): metainfo = METAINFO_PLIST % 3 with TemporaryDirectory(suffix=".ufo") as indir: with open(os.path.join(indir, "metainfo.plist"), 'w') as f: f.write(metainfo) # same as input path main(["-o", indir, indir]) # different but non existing path outdir = os.path.join(indir, "output.ufo") self.assertFalse(os.path.isdir(outdir)) main(["-o", outdir, indir]) self.assertTrue(os.path.exists(os.path.join(outdir, "metainfo.plist"))) # another existing dir with TemporaryDirectory(suffix=".ufo") as outdir: main(["-o", outdir, indir]) self.assertTrue(os.path.exists(os.path.join(outdir, "metainfo.plist"))) def test_main_float_precision_argument(self): metainfo = METAINFO_PLIST % 3 libdata = """ test_float 0.3333333333333334 """ with TemporaryDirectory(suffix=".ufo") as indir: outdir = os.path.join(indir, 'output.ufo') subpathWriteFile(metainfo, indir, "metainfo.plist") subpathWriteFile(libdata, indir, "lib.plist") # without --float-precision, it uses 10 decimal digits by default main(["-o", outdir, indir]) data = subpathReadPlist(outdir, "lib.plist") self.assertEqual(data["test_float"], 0.3333333333) main(["-o", outdir, "--float-precision=0", indir]) data = subpathReadPlist(outdir, "lib.plist") self.assertEqual(data["test_float"], 0) main(["-o", outdir, "--float-precision=6", indir]) data = subpathReadPlist(outdir, "lib.plist") self.assertEqual(data["test_float"], 0.333333) # -1 means no rounding, use repr() main(["-o", outdir, "--float-precision=-1", indir]) data = subpathReadPlist(outdir, "lib.plist") self.assertEqual(data["test_float"], 0.3333333333333334) def test_normalizeLibPlistWithBytesData(self): metainfo = METAINFO_PLIST % 3 libdata = """ org.robofab.fontlab.customdata gAJ9cQFVA2xpYnECY3BsaXN0bGliCl9JbnRlcm5hbERpY3QKcQMpgXEEVSdj b20uc2NocmlmdGdlc3RhbHR1bmcuR2x5cGhzLmxhc3RDaGFuZ2VxBVUTMjAx Ny8wOS8yNiAwOToxMzoyMXEGc31xB2JzLg== """ with TemporaryDirectory(suffix=".ufo") as indir: outdir = os.path.join(indir, 'output.ufo') subpathWriteFile(metainfo, indir, "metainfo.plist") subpathWriteFile(libdata, indir, "lib.plist") main(["-o", outdir, indir]) data = subpathReadPlist(outdir, "lib.plist") self.assertEqual( data['org.robofab.fontlab.customdata'], base64.b64decode("""\ gAJ9cQFVA2xpYnECY3BsaXN0bGliCl9JbnRlcm5hbERpY3QKcQMpgXEEVSdj b20uc2NocmlmdGdlc3RhbHR1bmcuR2x5cGhzLmxhc3RDaGFuZ2VxBVUTMjAx Ny8wOS8yNiAwOToxMzoyMXEGc31xB2JzLg== """)) class XMLWriterTest(unittest.TestCase): def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) # Python 3 renamed assertRaisesRegexp to assertRaisesRegex. if not hasattr(self, "assertRaisesRegex"): self.assertRaisesRegex = self.assertRaisesRegexp def test_propertyListObject_array(self): writer = XMLWriter(declaration=None) writer.propertyListObject([]) self.assertEqual(writer.getText(), '\n') writer = XMLWriter(declaration=None) writer.propertyListObject(["a"]) self.assertEqual(writer.getText(), '\n\ta\n') writer = XMLWriter(declaration=None) writer.propertyListObject([None]) self.assertEqual(writer.getText(), '\n') writer = XMLWriter(declaration=None) writer.propertyListObject([False]) self.assertEqual(writer.getText(), '\n\t\n') def test_propertyListObject_dict(self): writer = XMLWriter(declaration=None) writer.propertyListObject({}) self.assertEqual(writer.getText(), '\n') writer = XMLWriter(declaration=None) writer.propertyListObject({"a": "b"}) self.assertEqual( writer.getText(), '\n\ta\n\tb\n') writer = XMLWriter(declaration=None) writer.propertyListObject({"a&b": "b&a"}) self.assertEqual( writer.getText(), '\n\ta&b\n\tb&a\n') writer = XMLWriter(declaration=None) writer.propertyListObject({"a": 20.2}) self.assertEqual( writer.getText(), '\n\ta\n\t20.2\n') writer = XMLWriter(declaration=None) writer.propertyListObject({"a": 20.0}) self.assertEqual( writer.getText(), '\n\ta\n\t20\n') writer = XMLWriter(declaration=None) writer.propertyListObject({"": ""}) self.assertEqual( writer.getText(), '\n\t\n\t\n') writer = XMLWriter(declaration=None) writer.propertyListObject({None: ""}) self.assertEqual( writer.getText(), '\n\t\n\t\n') writer = XMLWriter(declaration=None) writer.propertyListObject({"": None}) self.assertEqual(writer.getText(), '\n\t\n') writer = XMLWriter(declaration=None) writer.propertyListObject({None: None}) self.assertEqual(writer.getText(), '\n\t\n') def test_propertyListObject_string(self): writer = XMLWriter(declaration=None) writer.propertyListObject("a") self.assertEqual(writer.getText(), 'a') writer = XMLWriter(declaration=None) writer.propertyListObject("&") self.assertEqual(writer.getText(), '&') writer = XMLWriter(declaration=None) writer.propertyListObject("1.000") self.assertEqual(writer.getText(), '1.000') writer = XMLWriter(declaration=None) writer.propertyListObject("") self.assertEqual(writer.getText(), '') def test_propertyListObject_boolean(self): writer = XMLWriter(declaration=None) writer.propertyListObject(True) self.assertEqual(writer.getText(), '') writer = XMLWriter(declaration=None) writer.propertyListObject(False) self.assertEqual(writer.getText(), '') def test_propertyListObject_float(self): writer = XMLWriter(declaration=None) writer.propertyListObject(1.1) self.assertEqual(writer.getText(), '1.1') writer = XMLWriter(declaration=None) writer.propertyListObject(-1.1) self.assertEqual(writer.getText(), '-1.1') def test_propertyListObject_integer(self): writer = XMLWriter(declaration=None) writer.propertyListObject(1.0) self.assertEqual(writer.getText(), '1') writer = XMLWriter(declaration=None) writer.propertyListObject(-1.0) self.assertEqual(writer.getText(), '-1') writer = XMLWriter(declaration=None) writer.propertyListObject(0.0) self.assertEqual(writer.getText(), '0') writer = XMLWriter(declaration=None) writer.propertyListObject(-0.0) self.assertEqual(writer.getText(), '0') writer = XMLWriter(declaration=None) writer.propertyListObject(1) self.assertEqual(writer.getText(), '1') writer = XMLWriter(declaration=None) writer.propertyListObject(-1) self.assertEqual(writer.getText(), '-1') writer = XMLWriter(declaration=None) writer.propertyListObject(+1) self.assertEqual(writer.getText(), '1') writer = XMLWriter(declaration=None) writer.propertyListObject(0) self.assertEqual(writer.getText(), '0') writer = XMLWriter(declaration=None) writer.propertyListObject(-0) self.assertEqual(writer.getText(), '0') writer = XMLWriter(declaration=None) writer.propertyListObject(2015-1-1) self.assertEqual(writer.getText(), '2013') def test_propertyListObject_date(self): writer = XMLWriter(declaration=None) date = datetime.datetime(2012, 9, 1) writer.propertyListObject(date) self.assertEqual(writer.getText(), '2012-09-01T00:00:00Z') writer = XMLWriter(declaration=None) date = datetime.datetime(2009, 11, 29, 16, 31, 53) writer.propertyListObject(date) self.assertEqual(writer.getText(), '2009-11-29T16:31:53Z') def test_propertyListObject_data(self): writer = XMLWriter(declaration=None) data = plistlib.Data(tobytes("abc")) writer.propertyListObject(data) self.assertEqual(writer.getText(), '\n\tYWJj\n') def test_propertyListObject_none(self): writer = XMLWriter(declaration=None) writer.propertyListObject(None) self.assertEqual(writer.getText(), '') def test_propertyListObject_unknown_data_type(self): writer = XMLWriter(declaration=None) with self.assertRaisesRegex( UFONormalizerError, r"Unknown data type in property list: <.* 'complex'>"): writer.propertyListObject(1.0j) def test_attributesToString(self): attrs = dict(a="blah", x=1, y=2.1) writer = XMLWriter(declaration=None) self.assertEqual( writer.attributesToString(attrs), 'x="1" y="2.1" a="blah"') def test_xmlEscapeText(self): self.assertEqual(xmlEscapeText("&"), "&") self.assertEqual(xmlEscapeText("<"), "<") self.assertEqual(xmlEscapeText(">"), ">") self.assertEqual(xmlEscapeText("a"), "a") self.assertEqual(xmlEscapeText("ä"), "ä") self.assertEqual(xmlEscapeText("ā"), "ā") self.assertEqual(xmlEscapeText("𐐀"), "𐐀") self.assertEqual(xmlEscapeText("©"), "©") self.assertEqual(xmlEscapeText("—"), "—") self.assertEqual(xmlEscapeText("1"), "1") self.assertEqual(xmlEscapeText("1.0"), "1.0") self.assertEqual(xmlEscapeText("'"), "'") self.assertEqual(xmlEscapeText("/"), "/") self.assertEqual(xmlEscapeText("\\"), "\\") self.assertEqual(xmlEscapeText("\r"), "\r") def test_xmlEscapeAttribute(self): self.assertEqual(xmlEscapeAttribute('"'), '"') self.assertEqual(xmlEscapeAttribute("'"), "'") self.assertEqual(xmlEscapeAttribute("abc"), 'abc') self.assertEqual(xmlEscapeAttribute("123"), '123') self.assertEqual(xmlEscapeAttribute("/"), '/') self.assertEqual(xmlEscapeAttribute("\\"), '\\') def test_xmlConvertValue(self): self.assertEqual(xmlConvertValue(0.0), '0') self.assertEqual(xmlConvertValue(-0.0), '0') self.assertEqual(xmlConvertValue(2.0), '2') self.assertEqual(xmlConvertValue(-2.0), '-2') self.assertEqual(xmlConvertValue(2.05), '2.05') self.assertEqual(xmlConvertValue(2), '2') self.assertEqual(xmlConvertValue(0.2), '0.2') self.assertEqual(xmlConvertValue("0.0"), '0.0') self.assertEqual(xmlConvertValue(1e-5), '0.00001') self.assertEqual(xmlConvertValue(1e-10), '0.0000000001') self.assertEqual(xmlConvertValue(1e-11), '0') self.assertEqual(xmlConvertValue(1e+5), '100000') self.assertEqual(xmlConvertValue(1e+10), '10000000000') def test_xmlConvertFloat(self): self.assertEqual(xmlConvertFloat(1.0), '1') self.assertEqual(xmlConvertFloat(1.01), '1.01') self.assertEqual(xmlConvertFloat(1.0000000001), '1.0000000001') self.assertEqual(xmlConvertFloat(1.00000000001), '1') self.assertEqual(xmlConvertFloat(1.00000000009), '1.0000000001') def test_xmlConvertFloat_no_rounding(self): import ufonormalizer oldFloatFormat = ufonormalizer.FLOAT_FORMAT ufonormalizer.FLOAT_FORMAT = None self.assertEqual(xmlConvertFloat(1.0000000000000002), '1.0000000000000002') self.assertEqual(xmlConvertFloat(1.00000000000000001), '1') self.assertEqual(xmlConvertFloat(1.00000000000000019), '1.0000000000000002') self.assertEqual(xmlConvertFloat(1e-16), '0.0000000000000001') self.assertEqual(xmlConvertFloat(1e-17), '0') ufonormalizer.FLOAT_FORMAT = oldFloatFormat def test_xmlConvertFloat_custom_precision(self): import ufonormalizer oldFloatFormat = ufonormalizer.FLOAT_FORMAT ufonormalizer.FLOAT_FORMAT = "%.3f" self.assertEqual(xmlConvertFloat(1.001), '1.001') self.assertEqual(xmlConvertFloat(1.0001), '1') self.assertEqual(xmlConvertFloat(1.0009), '1.001') ufonormalizer.FLOAT_FORMAT = "%.0f" self.assertEqual(xmlConvertFloat(1.001), '1') self.assertEqual(xmlConvertFloat(1.9), '2') self.assertEqual(xmlConvertFloat(10.0), '10') ufonormalizer.FLOAT_FORMAT = oldFloatFormat def test_xmlConvertInt(self): self.assertEqual(xmlConvertInt(1), '1') self.assertEqual(xmlConvertInt(-1), '-1') self.assertEqual(xmlConvertInt(- 1), '-1') self.assertEqual(xmlConvertInt(0), '0') self.assertEqual(xmlConvertInt(-0), '0') self.assertEqual(xmlConvertInt(0o01), '1') self.assertEqual(xmlConvertInt(- 0o01), '-1') self.assertEqual(xmlConvertInt(0o000001), '1') self.assertEqual(xmlConvertInt(0o0000000000000001), '1') self.assertEqual(xmlConvertInt(1000000000000001), '1000000000000001') self.assertEqual(xmlConvertInt(0o000001000001), '262145') self.assertEqual(xmlConvertInt(0o00000100000), '32768') self.assertEqual(xmlConvertInt(0o0000010), '8') self.assertEqual(xmlConvertInt(-0o0000010), '-8') self.assertEqual(xmlConvertInt(0o0000020), '16') self.assertEqual(xmlConvertInt(0o0000030), '24') self.assertEqual(xmlConvertInt(65536), '65536') class SubpathTest(unittest.TestCase): def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) self.filename = 'tmp' self.plistname = 'tmp.plist' def setUp(self): self.directory = tempfile.mkdtemp() self.filepath = os.path.join(self.directory, self.filename) self.plistpath = os.path.join(self.directory, self.plistname) def tearDown(self): shutil.rmtree(self.directory) def createTestFile(self, text, num=None): if num is None: with open(self.filepath, 'w', encoding='utf-8') as f: f.write(text) else: for i in range(num): filepath = self.filepath + str(i) with open(filepath, 'w', encoding='utf-8') as f: f.write(text) def test_subpathJoin(self): self.assertEqual(subpathJoin('a', 'b', 'c'), os.path.join('a', 'b', 'c')) self.assertEqual(subpathJoin('a', os.path.join('b', 'c')), os.path.join('a', 'b', 'c')) def test_subpathSplit(self): self.assertEqual(subpathSplit(os.path.join('a', 'b')), os.path.split(os.path.join('a', 'b'))) self.assertEqual(subpathSplit(os.path.join('a', 'b', 'c')), os.path.split(os.path.join('a', 'b', 'c'))) def test_subpathExists(self): self.createTestFile('') self.assertTrue(subpathExists(self.directory, self.filepath)) self.assertFalse(subpathExists(self.directory, 'nofile.txt')) def test_subpathReadFile(self): text = 'foo bar™⁜' self.createTestFile(text) self.assertEqual(text, subpathReadFile(self.directory, self.filename)) def test_subpathReadPlist(self): data = dict([('a', 'foo'), ('b', 'bar'), ('c', '™')]) with open(self.plistpath, 'wb') as f: f.write(dumps(data)) self.assertEqual(subpathReadPlist(self.directory, self.plistname), data) def test_subpathWriteFile(self): expected_text = 'foo bar™⁜' subpathWriteFile(expected_text, self.directory, self.filename) with open(self.filepath, 'r', encoding='utf-8') as f: text = f.read() self.assertEqual(text, expected_text) def test_subpathWriteFile_newline(self): mixed_eol_text = 'foo\r\nbar\nbaz\rquz' expected_text = 'foo\nbar\nbaz\nquz' subpathWriteFile(mixed_eol_text, self.directory, self.filename) with open(self.filepath, 'r', encoding='utf-8') as f: text = f.read() self.assertEqual(text, expected_text) def test_subpathWritePlist(self): expected_data = dict([('a', 'foo'), ('b', 'bar'), ('c', '™')]) subpathWritePlist(expected_data, self.directory, self.plistname) with open(self.plistpath, 'rb') as f: data = loads(f.read()) self.assertEqual(data, expected_data) def test_subpathRenameFile(self): self.createTestFile('') subpathRenameFile(self.directory, self.filename, self.filename + "_") self.assertTrue(os.path.exists(self.filepath + "_")) def test_subpathRenameDirectory(self): dirname = 'tmpdir' dirpath = os.path.join(self.directory, dirname) os.mkdir(dirpath) subpathRenameFile(self.directory, dirname, dirname + "_") self.assertTrue(os.path.exists(dirpath + "_")) def test_subpathRemoveFile(self): self.createTestFile('') subpathRemoveFile(self.directory, self.filename) self.assertFalse(os.path.exists(self.filepath)) def test__normalizePlistFile_remove_empty(self): emptyPlist = os.path.join(self.directory, "empty.plist") with open(emptyPlist, "w") as f: f.write(EMPTY_PLIST) # 'removeEmpty' keyword argument is True by default _normalizePlistFile({}, self.directory, "empty.plist") self.assertFalse(os.path.exists(emptyPlist)) def test__normalizePlistFile_keep_empty(self): emptyPlist = os.path.join(self.directory, "empty.plist") with open(emptyPlist, "w") as f: f.write(EMPTY_PLIST) _normalizePlistFile({}, self.directory, "empty.plist", removeEmpty=False) self.assertTrue(os.path.exists(emptyPlist)) def test_subpathGetModTime(self): self.createTestFile('') mtime = subpathGetModTime(self.directory, self.filename) self.assertEqual(os.path.getmtime(self.filepath), mtime) def test_subpathNeedsRefresh(self): import time self.createTestFile('') modTime = os.path.getmtime(self.filepath) modTimes = {} modTimes[self.filename] = float(modTime) self.assertFalse(subpathNeedsRefresh(modTimes, self.directory, self.filename)) time.sleep(1) # to get a different modtime with open(self.filepath, 'w', encoding='utf-8') as f: f.write('foo') self.assertTrue(subpathNeedsRefresh(modTimes, self.directory, self.filename)) def test_storeModTimes(self): num = 5 lib = {} modTimes = {} self.createTestFile('', num) filenames = [self.filename + str(i) for i in range(num)] for filename in filenames: filepath = os.path.join(self.directory, filename) modTime = os.path.getmtime(filepath) modTimes[filename] = float('%.1f' % (modTime)) lines = ['version: %s' % (ufonormalizerVersion)] lines += ['%.1f %s' % (modTimes[filename], filename) for filename in filenames] storeModTimes(lib, modTimes) self.assertEqual('\n'.join(lines), lib[modTimeLibKey]) def test_readModTimes(self): num = 5 lib = {} modTimes = {} lines = ['version: %s' % (ufonormalizerVersion)] filenames = [self.filename + str(i) for i in range(num)] modTime = float(os.path.getmtime(self.directory)) for i, filename in enumerate(filenames): modTimes[filename] = float('%.1f' % (modTime + i)) lines.append('%.1f %s' % (modTime + i, filename)) lib[modTimeLibKey] = '\n'.join(lines) self.assertEqual(readModTimes(lib), modTimes) class NameTranslationTest(unittest.TestCase): def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) # Python 3 renamed assertRegexpMatches() to assertRegex(). if not hasattr(self, "assertRegex"): self.assertRegex = self.assertRegexpMatches 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") self.assertEqual(userNameToFileName("a*"), "a_") self.assertEqual(userNameToFileName("a", ["a"]), "a000000000000001") self.assertEqual(userNameToFileName("Xy", ["x_y"]), "X_y000000000000001") 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_handleClash1_max_file_length(self): prefix = ("0" * 5) + "." suffix = "." + ("0" * 10) self.assertRegex( handleClash1(userName="ABCDEFGHIJKLMNOPQRSTUVWX_" * 10, prefix=prefix, suffix=suffix), r'00000.ABCDEFGHIJKLM.*NOPQRSTUVW000000000000001.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') if __name__ == "__main__": unittest.main()