Use individual exception classes instead of enum

This commit is contained in:
Simon Cozens 2021-03-18 15:49:49 +00:00
parent 02ebcf8077
commit 6c547864b6
2 changed files with 144 additions and 105 deletions

View File

@ -1,22 +1,6 @@
from enum import Enum, auto
import textwrap
class VarLibMergeFailure(Enum):
ShouldBeConstant = "some values were different, but should have been the same"
MismatchedTypes = "data had inconsistent types"
LengthsDiffer = "a list of objects had inconsistent lengths"
KeysDiffer = "a list of objects had different keys"
InconsistentGlyphOrder = "the glyph order was inconsistent between masters"
FoundANone = "one of the values in a list was empty when it shouldn't have been"
NotANone = "one of the values in a list was not empty when it should have been"
UnsupportedFormat = "an OpenType subtable (%s) had a format I didn't expect"
InconsistentFormat = (
"an OpenType subtable (%s) had inconsistent formats between masters"
)
InconsistentExtensions = "the masters use extension lookups in inconsistent ways"
class VarLibError(Exception):
"""Base exception for the varLib module."""
@ -32,6 +16,18 @@ class VarLibMergeError(VarLibError):
self.merger = merger
self.args = args
@property
def cause(self):
return self.args[0]
@property
def stack(self):
return self.args[1:]
@property
def reason(self):
return self.__doc__
def _master_name(self, ix):
ttf = self.merger.ttfs[ix]
if (
@ -45,26 +41,55 @@ class VarLibMergeError(VarLibError):
else:
return "master number %i" % ix
def _offender(self, cause):
reason = cause["reason"].value
if "expected" in cause and "got" in cause:
index = [x == cause["expected"] for x in cause["got"]].index(False)
return index, self._master_name(index)
if reason is VarLibMergeFailure.FoundANone:
index = [x is None for x in cause["got"]].index(True)
@property
def offender(self):
if "expected" in self.cause and "got" in self.cause:
index = [x == self.cause["expected"] for x in self.cause["got"]].index(
False
)
return index, self._master_name(index)
return None, None
def _incompatible_features(self, offender_index):
cause, stack = self.args[0], self.args[1:]
@property
def details(self):
if "expected" in self.cause and "got" in self.cause:
offender_index, offender = self.offender
got = self.cause["got"][offender_index]
return f"Expected to see {self.stack[0]}=={self.cause['expected']}, instead saw {got}\n"
return ""
def __str__(self):
offender_index, offender = self.offender
location = ""
if offender:
location = f"\n\nThe problem is likely to be in {offender}:\n"
context = "".join(reversed(self.stack))
basic = textwrap.fill(
f"Couldn't merge the fonts, because {self.reason}. "
f"This happened while performing the following operation: {context}",
width=78,
)
return "\n\n" + basic + location + self.details
class VarLibMergeFailureShouldBeConstant(VarLibMergeError):
"""some values were different, but should have been the same"""
@property
def details(self):
if self.stack[0] != ".FeatureCount":
return super()
offender_index, offender = self.offender
bad_ttf = self.merger.ttfs[offender_index]
good_ttf = self.merger.ttfs[offender_index - 1]
good_features = [
x.FeatureTag for x in good_ttf[stack[-1]].table.FeatureList.FeatureRecord
x.FeatureTag
for x in good_ttf[self.stack[-1]].table.FeatureList.FeatureRecord
]
bad_features = [
x.FeatureTag for x in bad_ttf[stack[-1]].table.FeatureList.FeatureRecord
x.FeatureTag
for x in bad_ttf[self.stack[-1]].table.FeatureList.FeatureRecord
]
return (
"\nIncompatible features between masters.\n"
@ -72,39 +97,65 @@ class VarLibMergeError(VarLibError):
f"Got: {', '.join(bad_features)}.\n"
)
def __str__(self):
cause, stack = self.args[0], self.args[1:]
context = "".join(reversed(stack))
details = ""
reason = cause["reason"].value
offender_index, offender = self._offender(cause)
if offender:
details = f"\n\nThe problem is likely to be in {offender}:\n"
if cause["reason"] is VarLibMergeFailure.FoundANone:
details = details + f"{stack[0]}=={cause['got']}\n"
elif (
cause["reason"] is VarLibMergeFailure.ShouldBeConstant
and stack[0] == ".FeatureCount"
):
# Common case
details = details + self._incompatible_features(offender_index)
elif "expected" in cause and "got" in cause:
got = cause["got"][offender_index]
details = details + (
f"Expected to see {stack[0]}=={cause['expected']}, instead saw {got}\n"
)
if (
cause["reason"] is VarLibMergeFailure.UnsupportedFormat
or cause["reason"] is VarLibMergeFailure.InconsistentFormat
):
reason = reason % cause["subtable"]
basic = textwrap.fill(
f"Couldn't merge the fonts, because {reason}. "
f"This happened while performing the following operation: {context}",
width=78,
)
return "\n\n" + basic + details
class VarLibMergeFailureFoundANone(VarLibMergeError):
"""one of the values in a list was empty when it shouldn't have been"""
@property
def offender(self):
cause = self.argv[0]
index = [x is None for x in cause["got"]].index(True)
return index, self._master_name(index)
@property
def details(self):
cause, stack = self.args[0], self.args[1:]
return f"{stack[0]}=={cause['got']}\n"
class VarLibMergeFailureMismatchedTypes(VarLibMergeError):
"""data had inconsistent types"""
pass
class VarLibMergeFailureLengthsDiffer(VarLibMergeError):
"""a list of objects had inconsistent lengths"""
pass
class VarLibMergeFailureKeysDiffer(VarLibMergeError):
"""a list of objects had different keys"""
pass
class VarLibMergeFailureInconsistentGlyphOrder(VarLibMergeError):
"""the glyph order was inconsistent between masters"""
pass
class VarLibMergeFailureInconsistentExtensions(VarLibMergeError):
"""the masters use extension lookups in inconsistent ways"""
pass
class VarLibMergeFailureUnsupportedFormat(VarLibMergeError):
"""an OpenType subtable (%s) had a format I didn't expect"""
@property
def reason(self):
cause, stack = self.args[0], self.args[1:]
return self.__doc__ % cause["subtable"]
class VarLibMergeFailureUnsupportedFormat(VarLibMergeFailureUnsupportedFormat):
"""an OpenType subtable (%s) had inconsistent formats between masters"""
pass
class VarLibCFFDictMergeError(VarLibMergeError):

View File

@ -14,8 +14,18 @@ from fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce
from fontTools.otlLib.builder import buildSinglePos
from .errors import VarLibMergeError, VarLibMergeFailure
from .errors import (
VarLibMergeFailureShouldBeConstant,
VarLibMergeFailureFoundANone,
VarLibMergeFailureMismatchedTypes,
VarLibMergeFailureLengthsDiffer,
VarLibMergeFailureKeysDiffer,
VarLibMergeFailureInconsistentGlyphOrder,
VarLibMergeFailureInconsistentExtensions,
VarLibMergeFailureUnsupportedFormat,
VarLibMergeFailureUnsupportedFormat,
VarLibMergeError,
)
class Merger(object):
@ -69,8 +79,7 @@ class Merger(object):
item.ensureDecompiled()
keys = sorted(vars(out).keys())
if not all(keys == sorted(vars(v).keys()) for v in lst):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.KeysDiffer,
raise VarLibMergeFailureKeysDiffer(self, ({
"expected": keys,
"got": [sorted(vars(v).keys()) for v in lst]},))
mergers = self.mergersFor(out)
@ -88,8 +97,7 @@ class Merger(object):
def mergeLists(self, out, lst):
if not allEqualTo(out, lst, len):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.LengthsDiffer,
raise VarLibMergeFailureLengthsDiffer(self, ({
"expected": len(out),
"got": [len(x) for x in lst]},))
for i,(value,values) in enumerate(zip(out, zip(*lst))):
@ -101,8 +109,7 @@ class Merger(object):
def mergeThings(self, out, lst):
if not allEqualTo(out, lst, type):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.MismatchedTypes,
raise VarLibMergeFailureMismatchedTypes(self, ({
"expected": type(out),
"got": [type(x) for x in lst]},))
mergerFunc = self.mergersFor(out).get(None, None)
@ -114,8 +121,7 @@ class Merger(object):
self.mergeLists(out, lst)
else:
if not allEqualTo(out, lst):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.ShouldBeConstant,
raise VarLibMergeFailureShouldBeConstant(self, ({
"expected": out,
"got": lst},))
@ -140,8 +146,7 @@ class AligningMerger(Merger):
def merge(merger, self, lst):
if self is None:
if not allNone(lst):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.NotANone,
raise VarLibMergeFailureNotANone(self, ({
"expected": None,
"got": lst
},))
@ -157,8 +162,7 @@ def merge(merger, self, lst):
for k in allKeys:
allValues = nonNone(l.get(k) for l in lst)
if not allEqual(allValues):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.ShouldBeConstant,
raise VarLibMergeFailureShouldBeConstant(self, ({
"expected": allValues[0],
"got": lst,
}, "."+k))
@ -198,8 +202,7 @@ def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
order = sorted(combined, key=sortKey)
# Make sure all input glyphsets were in proper order
if not all(sorted(vs, key=sortKey) == vs for vs in lst):
raise VarLibMergeError(self, ({
"reason" : VarLibMergeFailure.InconsistentGlyphOrder
raise VarLibMergeFailureInconsistentGlyphOrder(self, ({
},))
del combined
@ -227,8 +230,7 @@ def _Lookup_SinglePos_get_effective_value(subtables, glyph):
elif self.Format == 2:
return self.Value[self.Coverage.glyphs.index(glyph)]
else:
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "single positioning lookup"
},))
return None
@ -252,8 +254,7 @@ def _Lookup_PairPos_get_effective_value_pair(subtables, firstGlyph, secondGlyph)
klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
return self.Class1Record[klass1].Class2Record[klass2]
else:
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "pair positioning lookup"
},))
return None
@ -262,8 +263,7 @@ def _Lookup_PairPos_get_effective_value_pair(subtables, firstGlyph, secondGlyph)
def merge(merger, self, lst):
self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "single positioning lookup"
},))
@ -552,8 +552,7 @@ def merge(merger, self, lst):
elif self.Format == 2:
_PairPosFormat2_merge(self, lst, merger)
else:
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "pair positioning lookup"
},))
@ -621,8 +620,7 @@ def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
# input masters.
if not allEqual(allClasses):
raise VarLibMergeError(self, allClasses)
if not allClasses:
raise allClasses(self, allClasses)
rec = None
else:
rec = ot.MarkRecord()
@ -671,32 +669,28 @@ def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
@AligningMerger.merger(ot.MarkBasePos)
def merge(merger, self, lst):
if not allEqualTo(self.Format, (l.Format for l in lst)):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.InconsistentFormats,
raise VarLibMergeFailureInconsistentFormats(self, ({
"subtable": "mark-to-base positioning lookup",
"expected": self.Format,
"got": [l.Format for l in lst]},))
if self.Format == 1:
_MarkBasePosFormat1_merge(self, lst, merger)
else:
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "mark-to-base positioning lookup",
}))
@AligningMerger.merger(ot.MarkMarkPos)
def merge(merger, self, lst):
if not allEqualTo(self.Format, (l.Format for l in lst)):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.InconsistentFormats,
raise VarLibMergeFailureInconsistentFormats(self, ({
"subtable": "mark-to-mark positioning lookup",
"expected": self.Format,
"got": [l.Format for l in lst]},))
if self.Format == 1:
_MarkBasePosFormat1_merge(self, lst, merger, 'Mark1', 'Mark2')
else:
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "mark-to-mark positioning lookup",
}))
@ -828,14 +822,12 @@ def merge(merger, self, lst):
continue
if sts[0].__class__.__name__.startswith('Extension'):
if not allEqual([st.__class__ for st in sts]):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.InconsistentExtensions,
raise VarLibMergeFailureInconsistentExtensions(self, ({
"expected": "Extension",
"got": [st.__class__.__name__ for st in sts]
},))
if not allEqual([st.ExtensionLookupType for st in sts]):
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.InconsistentExtensions,
raise VarLibMergeFailureInconsistentExtensions(self, ({
},))
l.LookupType = sts[0].ExtensionLookupType
new_sts = [st.ExtSubTable for st in sts]
@ -1065,8 +1057,7 @@ class VariationMerger(AligningMerger):
if None in lst:
if allNone(lst):
if out is not None:
raise VarLibMergeError(self, ({
"reason": VarLibMergeFailure.FoundANone,
raise VarLibMergeFailureFoundANone(self, ({
"got": lst
},))
return
@ -1089,8 +1080,7 @@ def buildVarDevTable(store_builder, master_values):
@VariationMerger.merger(ot.BaseCoord)
def merge(merger, self, lst):
if self.Format != 1:
raise VarLibMergeError(self, ({
"cause": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "a baseline coordinate"
},))
self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst])
@ -1101,8 +1091,7 @@ def merge(merger, self, lst):
@VariationMerger.merger(ot.CaretValue)
def merge(merger, self, lst):
if self.Format != 1:
raise VarLibMergeError(self, ({
"cause": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "a caret"
},))
self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst])
@ -1113,8 +1102,7 @@ def merge(merger, self, lst):
@VariationMerger.merger(ot.Anchor)
def merge(merger, self, lst):
if self.Format != 1:
raise VarLibMergeError(self, ({
"cause": VarLibMergeFailure.UnsupportedFormat,
raise VarLibMergeFailureUnsupportedFormat(self, ({
"subtable": "an anchor"
},))
self.XCoordinate, XDeviceTable = buildVarDevTable(merger.store_builder, [a.XCoordinate for a in lst])