pax_global_header00006660000000000000000000000064137724023570014524gustar00rootroot0000000000000052 comment=783ec1479ed35f26a9992cb18927f1902207b0af ufoNormalizer-0.5.3/000077500000000000000000000000001377240235700143655ustar00rootroot00000000000000ufoNormalizer-0.5.3/.coveragerc000066400000000000000000000014521377240235700165100ustar00rootroot00000000000000[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.5.3/.flake8000066400000000000000000000000351377240235700155360ustar00rootroot00000000000000[flake8] max-line-length=110 ufoNormalizer-0.5.3/.github/000077500000000000000000000000001377240235700157255ustar00rootroot00000000000000ufoNormalizer-0.5.3/.github/workflows/000077500000000000000000000000001377240235700177625ustar00rootroot00000000000000ufoNormalizer-0.5.3/.github/workflows/publish-package.yml000066400000000000000000000017301377240235700235450ustar00rootroot00000000000000name: Build and Publish Python Package on: push: tags: - '[0-9].*' jobs: create_release: name: Create GitHub Release runs-on: ubuntu-latest steps: - name: Create release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: false prerelease: true deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* ufoNormalizer-0.5.3/.github/workflows/run-tests.yml000066400000000000000000000022611377240235700224520ustar00rootroot00000000000000name: Run Tests on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: inputs: reason: description: 'Reason for running workflow' required: true jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.6', '3.7', '3.8', '3.9'] steps: - name: Log reason (manual run only) if: github.event_name == 'workflow_dispatch' run: | echo "Reason for triggering: ${{ github.event.inputs.reason }}" - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 coveralls if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | flake8 --count --show-source --statistics - name: Run test suite & upload coverage env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | coverage run setup.py test coveralls ufoNormalizer-0.5.3/.gitignore000066400000000000000000000014121377240235700163530ustar00rootroot00000000000000# 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/ # scm version src/ufonormalizer/_version.py ufoNormalizer-0.5.3/LICENSE.txt000066400000000000000000000032021377240235700162050ustar00rootroot00000000000000ufoNormalizer 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.5.3/README.md000066400000000000000000000066031377240235700156510ustar00rootroot00000000000000[![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-3.6%2C%203.7%2C%203.8%2C%203.9-blue.svg) [![PyPI Version](https://img.shields.io/pypi/v/ufonormalizer.svg)](https://pypi.python.org/pypi/ufonormalizer) # ufoNormalizer Provides a standard formatting so that there are meaningful diffs in version control rather than formatting noise. Examples of formatting applied by ufoNormalizer include: - Changing floating-point numbers to integers where it doesn't alter the value (e.g. `x="95.0"` becomes `x="95"` ) - Rounding floating-point numbers to 10 digits - Formatting XML with tabs rather than spaces ## Usage in RoboFont RoboFont comes with ufoNormalizer pre-installed, and you can set a preference to normalize UFOs on save. Simply open the Scripting Window and run the following code: ``` from mojo.UI import setDefault, getDefault setDefault("shouldNormalizeOnSave", True) print("shouldNormalizeOnSave is set to " + str(getDefault("shouldNormalizeOnSave"))) ``` ## Advanced usage ### Installation Install these tools using the [pip](https://pip.pypa.io/en/stable/installing/) package [hosted on PyPI](https://pypi.org/project/ufonormalizer/): ``` pip install --upgrade ufonormalizer ``` ### Command line Use on the command line: ``` ufonormalizer /font.ufo ``` To view all arguments, run: ``` ufonormalizer --help ``` Note: if you are working on a UFO within RoboFont and run ufoNormalizer on that UFO, RoboFont will notify you that the UFO has been updated externally. Simply accept this by selecting "Update." ### Automating via Git hooks Beyond basic command-line usage, ufoNormalizer can be used in an automated manner. Of course, you can automate it to run from a shell script or a Python script. One useful possibility is using it within a Git hook. > Git hooks are scripts that Git executes before or after events such as: commit, push, and receive. Git hooks are a built-in feature - no need to download anything. Git hooks are run locally. – from [Git Hooks](https://githooks.com/) It's easy to set up a git hook that will normalize ufos in a project immediately before each commit, ensuring that you only ever commit clean UFO data. In a Git project, navigate to `/.git/hooks` and replace `pre-commit.sample` with the following code, then remove the file extension: ```bash #!/bin/sh # A hook script to verify what is about to be committed. # Called by "git commit" with no arguments. # # Uses bash syntax to call arguments as needed. # # To enable this hook, save this to /.git/hooks/pre-commit (with no file extension) set -e for ufo in ./*.ufo; do ufonormalizer "$ufo" done ``` Now, each time you commit, all `.ufo`s in your Git project will be normalized before being recorded by Git. Because this hook is setup within the immediate project, this configuration will only apply to the immediate project. You will need to update each project to use this Git hook if you wish to normalize UFOs elsewhere. If you want this hook to be added to all future git projects, you can [configure a global git template](https://coderwall.com/p/jp7d5q/create-a-global-git-commit-hook). However, this approach probably doesn't make sense if you also work on projects that don't involve UFO files. ufoNormalizer-0.5.3/pyproject.toml000066400000000000000000000003051377240235700172770ustar00rootroot00000000000000[build-system] requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] [tool.setuptools_scm] write_to = 'src/ufonormalizer/_version.py' write_to_template = '__version__ = "{version}"' ufoNormalizer-0.5.3/setup.cfg000066400000000000000000000001501377240235700162020ustar00rootroot00000000000000[sdist] formats = zip [metadata] license_file = LICENSE.txt [options] setup_requires = setuptools_scm ufoNormalizer-0.5.3/setup.py000066400000000000000000000026331377240235700161030ustar00rootroot00000000000000from setuptools import setup, find_packages setup( name="ufonormalizer", 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"}, packages=find_packages(where="src"), entry_points={ 'console_scripts': [ "ufonormalizer = ufonormalizer:main", ] }, use_scm_version={ "write_to": 'src/ufonormalizer/_version.py', "write_to_template": '__version__ = "{version}"', }, setup_requires=['setuptools_scm'], 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 :: 3", "Topic :: Text Processing :: Fonts", "Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics :: Graphics Conversion", ], python_requires='>=3.6', zip_safe=True, ) ufoNormalizer-0.5.3/src/000077500000000000000000000000001377240235700151545ustar00rootroot00000000000000ufoNormalizer-0.5.3/src/ufonormalizer/000077500000000000000000000000001377240235700200505ustar00rootroot00000000000000ufoNormalizer-0.5.3/src/ufonormalizer/__init__.py000066400000000000000000001651071377240235700221730ustar00rootroot00000000000000#! /usr/bin/env python3 # -*- coding: utf-8 -*- import binascii import time import os import re 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 try: from ._version import __version__ except ImportError: try: from setuptools_scm import get_version __version__ = get_version() except ImportError: __version__ = 'unknown' """ - filter out unknown attributes and subelements - add doctests for the image purging - things that need to be improved are marked with "# TO DO" """ description = f""" UFO Normalizer (version {__version__}): This tool processes the contents of a UFO and normalizes all possible files to a standard XML formatting, data structure and file naming scheme. """ 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 " f"places (default is {DEFAULT_FLOAT_PRECISION}). " "The value -1 means no " "rounding (i.e. use built-in " "repr().") 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(f'Input path does not exist: "{ inputPath }".') if os.path.splitext(inputPath)[-1].lower() != ".ufo": parser.error(f'Input path is not a UFO: "{ 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" def _loads(data): return plistlib.loads(data) def _dumps(plist): return plistlib.dumps(plist) # Python 3.9 deprecated plistlib.Data. The following _*code_base64 functions # preserve some behavior related to that API. def _decode_base64(s): if isinstance(s, str): return binascii.a2b_base64(s.encode("utf-8")) else: return binascii.a2b_base64(s) def _encode_base64(s, maxlinelength=76): # copied from base64.encodebytes(), with added maxlinelength argument maxbinsize = (maxlinelength//4)*3 pieces = [] for i in range(0, len(s), maxbinsize): chunk = s[i: i + maxbinsize] pieces.append(binascii.b2a_base64(chunk)) return b''.join(pieces) # 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, str): 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(f"Required metainfo.plist file not in " f"{ufoPath}") metaInfo = subpathReadPlist(ufoPath, "metainfo.plist") formatVersion = metaInfo.get("formatVersion") if formatVersion is None: raise UFONormalizerError(f"Required formatVersion value not defined " f"in metainfo.plist in {ufoPath}") try: fV = int(formatVersion) formatVersion = fV except ValueError: raise UFONormalizerError(f"Required formatVersion value not properly " f"formatted in metainfo.plist in {ufoPath}") if formatVersion > 3: raise UFONormalizerError(f"Unsupported UFO format " f"({formatVersion}) in {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(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 = f"org.unifiedfontobject.normalizer.{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(str(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 = f"org.unifiedfontobject.normalizer.{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. However or are allowed, and the 0 becomes None. """ x = guideline.get("x") y = guideline.get("y") angle = guideline.get("angle") name = guideline.get("name") color = guideline.get("color") identifier = guideline.get("identifier") # 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 # The spec was ambiguous about y=0 or x=0, so don't raise an error here, # instead, or are allowed, and the 0 becomes None. if angle is None: if x == 0: x = None if y == 0: y = None # 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 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=None): 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 if imageFileRef is None: imageFileRef = [] 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 = f"{d:04X}" 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)" r"(?:-(?P\d\d)(?:T(?P\d\d)" r"(?::(?P\d\d)" r"(?::(?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 (f'{data.year:04d}-{data.month:02d}-' f'{data.day:02d}T{data.hour:02d}:' f'{data.minute:02d}:{data.second:02d}Z') 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 b'' return binascii.a2b_base64(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("\n") text = dedent_tabs(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=None, 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=None): 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, str): self._plistString(data) elif isinstance(data, bool): self._plistBoolean(data) elif isinstance(data, int): 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, bytes): self._plistData(data) elif isinstance(data, datetime.datetime): self._plistDate(data) else: raise UFONormalizerError(f"Unknown data type in property list: " f"{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 = _encode_base64(data, 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) # --------------- # Text Operations # --------------- WHITESPACE_ONLY_RE = re.compile(r'^[\s\t]+$', re.MULTILINE) LEADING_WHITESPACE_RE = re.compile(r'(^(?:\s{4}|\t)*)(?:[^\t\n])', re.MULTILINE) def dedent_tabs(text): """ Based on `textwrap.dedent`, but modified to only work on tabs and 4-space indents Remove any common leading tabs from every line in `text`. This can be used to make triple-quoted strings line up with the left edge of the display, while still presenting them in the source code in indented form. Entirely blank lines are normalized to a newline character. """ # Look for the longest leading string of spaces and tabs common to # all lines. margin = None text = WHITESPACE_ONLY_RE.sub('', text) indents = LEADING_WHITESPACE_RE.findall(text) for indent in indents: if margin is None: margin = indent # Current line more deeply indented than previous winner: # no change (previous winner is still on top). elif indent.startswith(margin): pass # Current line consistent with and no deeper than previous winner: # it's the new winner. elif margin.startswith(indent): margin = indent # Find the largest common whitespace between current line and previous # winner. else: for i, (x, y) in enumerate(zip(margin, indent)): if x != y: margin = margin[:i] break # sanity check (testing/debugging only) if 0 and margin: for line in text.split("\n"): assert not line or line.startswith(margin), \ "line = %r, margin = %r" % (line, margin) if margin: text = re.sub(r'(?m)^' + margin, '', text) return text # --------------- # 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, str): 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, str): fromSubpath = [fromSubpath] if isinstance(toSubpath, str): 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, str): fromSubpath = [fromSubpath] if isinstance(toSubpath, str): 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=None, prefix="", suffix=""): """ existing should be a case-insensitive list of all existing file names. """ if existing is None: existing = [] # the incoming name must be a string assert isinstance(userName, str), "The value for userName must be a 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=None, prefix="", suffix=""): """ existing must be a case-insensitive list of all existing file names. """ if existing is None: existing = [] # 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=None, prefix="", suffix=""): """ existing must be a case-insensitive list of all existing file names. """ if existing is None: existing = [] # 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.5.3/tests/000077500000000000000000000000001377240235700155275ustar00rootroot00000000000000ufoNormalizer-0.5.3/tests/__init__.py000066400000000000000000000000001377240235700176260ustar00rootroot00000000000000ufoNormalizer-0.5.3/tests/data/000077500000000000000000000000001377240235700164405ustar00rootroot00000000000000ufoNormalizer-0.5.3/tests/data/glif/000077500000000000000000000000001377240235700173615ustar00rootroot00000000000000ufoNormalizer-0.5.3/tests/data/glif/format1.glif000066400000000000000000000015371377240235700216030ustar00rootroot00000000000000 abc com.letterror.somestuff arbitrary custom data! ufoNormalizer-0.5.3/tests/data/glif/format2.glif000066400000000000000000000021331377240235700215750ustar00rootroot00000000000000 abc com.letterror.somestuff arbitrary custom data! public.markColor 1,0,0,0.5 arbitrary text about the glyph ufoNormalizer-0.5.3/tests/data/glif/formatNone.glif000066400000000000000000000000761377240235700223370ustar00rootroot00000000000000 ufoNormalizer-0.5.3/tests/test_ufonormalizer.py000066400000000000000000002530121377240235700220370ustar00rootroot00000000000000 # -*- coding: utf-8 -*- import os import sys import unittest import tempfile import shutil import datetime 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, 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, _decode_base64) from ufonormalizer import __version__ as ufonormalizerVersion from plistlib import loads, dumps from io import StringIO from tempfile import TemporaryDirectory 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_normalizeFontInfoPlist_guidelines_vertical_y_is_zero(self): # Actually a vertical guide element = ET.fromstring("") writer = XMLWriter(declaration=None) _normalizeGlifGuideline(element, writer) self.assertEqual(writer.getText(), '') def test_normalizeFontInfoPlist_guidelines_horizontal_x_is_zero(self): # Actually an horizontal guide 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") # Normalizer should not indent Line2 and Line3 more than already indented element = ET.fromstring( "\n\tLine1\n\tLine2\n\tLine3\n") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tLine1\n\tLine2\n\tLine3\n") # Normalizer should keep the extra tab in line 2 element = ET.fromstring( "\n\tLine1\n\t\tLine2\n\tLine3\n") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tLine1\n\t\tLine2\n\tLine3\n") # Normalizer should keep the extra spaces on line 2 element = ET.fromstring( "\n\tLine1\n\t Line2\n\tLine3\n") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tLine1\n\t Line2\n\tLine3\n") # Normalizer should remove the extra tab all lines have in common, # but leave the additional tab on line 2 element = ET.fromstring( "\n\t\tLine1\n\t\t\tLine2\n\t\tLine3\n") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tLine1\n\t\tLine2\n\tLine3\n") # Normalizer should remove the extra 4-space all lines have in common, # but leave the additional 4-space on line 2 element = ET.fromstring( "\n Line1\n Line2\n Line3\n") writer = XMLWriter(declaration=None) _normalizeGlifNote(element, writer) self.assertEqual( writer.getText(), "\n\tLine1\n\t Line2\n\tLine3\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), 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'], _decode_base64("""\ gAJ9cQFVA2xpYnECY3BsaXN0bGliCl9JbnRlcm5hbERpY3QKcQMpgXEEVSdj b20uc2NocmlmdGdlc3RhbHR1bmcuR2x5cGhzLmxhc3RDaGFuZ2VxBVUTMjAx Ny8wOS8yNiAwOToxMzoyMXEGc31xB2JzLg== """)) def test_version_not_unknown(self): """Test that package version is not 'unknown', which should only happen in cases where the package has not been installed and is outside of source control. """ self.assertNotEqual(ufonormalizerVersion, 'unknown') 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 = tobytes("abc") writer.propertyListObject(data) self.assertEqual(writer.getText(), '\n\tYWJj\n') def test_propertyListObject_data_wrap(self): writer = XMLWriter(declaration=None) data = tobytes("XYZ" * 30) writer.propertyListObject(data) expected = "\n".join([ "", "\tWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFla", "\tWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFlaWFla", ""]) self.assertEqual(writer.getText(), expected) 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()