From 34a8f35003015780a45c5675f399ef465dca501e Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 8 Apr 2016 15:46:31 +0100 Subject: [PATCH 1/3] add --float-precision command line argument Default is 10 decimal places. Value of -1 means no rounding: i.e. use built-in repr() Also, backport tempfile.TemporaryDirectory to python 2.7 (can be useful for tests) --- normalization/test_ufonormalizer.py | 38 +++++++++++++++++++++++++++++ normalization/ufonormalizer.py | 25 +++++++++++++++---- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/normalization/test_ufonormalizer.py b/normalization/test_ufonormalizer.py index 0f37818..32223ec 100644 --- a/normalization/test_ufonormalizer.py +++ b/normalization/test_ufonormalizer.py @@ -51,6 +51,32 @@ # Python 3: a stream of *unicode* strings from io import StringIO +try: + from tempfile import TemporaryDirectory # Python 3 only +except ImportError: + # backport for Python 2.7 + class TemporaryDirectory(object): + """ Create and return a temporary directory. This has the same + behavior as mkdtemp but can be used as a context manager. + + Adapted from tempfile.TemporaryDirectory (new in Python 3.2). + """ + def __init__(self, suffix="", prefix="tmp", dir=None): + self._closed = False + self.name = tempfile.mkdtemp(suffix, prefix, dir) + + def __enter__(self): + return self.name + + def cleanup(self, _warn=False): + if self.name and not self._closed: + shutil.rmtree(self.name) + self._closed = True + + def __exit__(self, exc, value, tb): + self.cleanup() + + GLIFFORMAT1 = '''\ @@ -1386,6 +1412,14 @@ def test_main_input_not_ufo(self): 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()) + class XMLWriterTest(unittest.TestCase): def __init__(self, methodName): @@ -1638,6 +1672,10 @@ def test_xmlConvertFloat_custom_precision(self): 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): diff --git a/normalization/ufonormalizer.py b/normalization/ufonormalizer.py index dbcc3a5..d7544ea 100644 --- a/normalization/ufonormalizer.py +++ b/normalization/ufonormalizer.py @@ -37,6 +37,7 @@ 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") @@ -44,13 +45,17 @@ def main(args=None): parser.add_argument("-a", "--all", help="Normalize all files in the UFO. By default, only files modified since the previous normalization will be processed.", action="store_true") parser.add_argument("-v", "--verbose", help="Print more info to console.", action="store_true") parser.add_argument("-q", "--quiet", help="Suppress all non-error messages.", action="store_true") + parser.add_argument("--float-precision", type=int, default=DEFAULT_FLOAT_PRECISION, help="Round floats to the specified number of decimal places (default is %d). The value -1 means no rounding (i.e. use built-in repr()." % DEFAULT_FLOAT_PRECISION) 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) @@ -60,6 +65,14 @@ def main(args=None): parser.error('Input path does not exist: "%s".' % inputPath) if os.path.splitext(inputPath)[-1].lower() != ".ufo": parser.error('Input path is not a UFO: "%s".' % inputPath) + + if args.float_precision >= 0: + floatPrecision = args.float_precision + elif args.float_precision == -1: + floatPrecision = None + else: + parser.error("float precision must be >= 0 or -1 (no round).") + message = 'Normalizing "%s".' if not onlyModified: message += " Processing all files." @@ -142,10 +155,11 @@ def tounicode(s, encoding='ascii', errors='strict'): class UFONormalizerError(Exception): pass -FLOAT_FORMAT = "%.10f" +DEFAULT_FLOAT_PRECISION = 10 +FLOAT_FORMAT = "%%.%df" % DEFAULT_FLOAT_PRECISION -def normalizeUFO(ufoPath, outputPath=None, onlyModified=True, floatPrecision=10): +def normalizeUFO(ufoPath, outputPath=None, onlyModified=True, floatPrecision=DEFAULT_FLOAT_PRECISION): global FLOAT_FORMAT if floatPrecision is None: # use repr() and don't round floats @@ -1303,9 +1317,10 @@ def xmlConvertFloat(value): string = "%.16f" % value else: string = FLOAT_FORMAT % value - string = string.rstrip("0") - if string[-1] == ".": - return xmlConvertInt(int(value)) + if "." in string: + string = string.rstrip("0") + if string[-1] == ".": + return xmlConvertInt(int(value)) return string def xmlConvertInt(value): From 0cae6c776dffd9fa72ac46ec062862dc8665466e Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 8 Apr 2016 16:05:32 +0100 Subject: [PATCH 2/3] skip duplicateUFO if the outputPath == ufoPath otherwise, copytree raises FileNotFoundError --- normalization/ufonormalizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/normalization/ufonormalizer.py b/normalization/ufonormalizer.py index d7544ea..f4114a9 100644 --- a/normalization/ufonormalizer.py +++ b/normalization/ufonormalizer.py @@ -171,7 +171,7 @@ def normalizeUFO(ufoPath, outputPath=None, onlyModified=True, floatPrecision=DEF # 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: + if outputPath is not None and outputPath != ufoPath: duplicateUFO(ufoPath, outputPath) ufoPath = outputPath # get the UFO format version From 1d6eeee6480cdc60de2b2ad4665960cb22b24177 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 8 Apr 2016 16:07:02 +0100 Subject: [PATCH 3/3] add tests for duplicateUFO and for invalid metainfo.plist --- normalization/test_ufonormalizer.py | 80 +++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/normalization/test_ufonormalizer.py b/normalization/test_ufonormalizer.py index 32223ec..0b03650 100644 --- a/normalization/test_ufonormalizer.py +++ b/normalization/test_ufonormalizer.py @@ -29,7 +29,7 @@ _normalizeGlifPointAttributesFormat2, _normalizeGlifComponentAttributesFormat2, _normalizeGlifTransformation, _normalizeColorString, _convertPlistElementToObject, _normalizePlistFile, - main) + main, xmlDeclaration, plistDocType) from ufonormalizer import __version__ as ufonormalizerVersion # Python 3.4 deprecated readPlistFromBytes and writePlistToBytes @@ -190,14 +190,18 @@ def __exit__(self, exc, value, tb): ''' -EMPTY_PLIST = '''\ - - +EMPTY_PLIST = "\n".join([xmlDeclaration, plistDocType, '']) + +METAINFO_PLIST = "\n".join([xmlDeclaration, plistDocType, """\ - - + + creator + org.robofab.ufoLib + formatVersion + %d + -''' +"""]) class redirect_stderr(object): @@ -1420,6 +1424,65 @@ def test_main_invalid_float_precision(self): 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"))) + class XMLWriterTest(unittest.TestCase): def __init__(self, methodName): @@ -1697,9 +1760,6 @@ def test_xmlConvertInt(self): self.assertEqual(xmlConvertInt(0o0000030), '24') self.assertEqual(xmlConvertInt(65536), '65536') - def test_duplicateUFO(self): - pass - class SubpathTest(unittest.TestCase): def __init__(self, methodName):