Spaces:
Runtime error
Runtime error
""" | |
colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such. | |
""" | |
import collections | |
import enum | |
from fontTools.ttLib.tables.otBase import ( | |
BaseTable, | |
FormatSwitchingBaseTable, | |
UInt8FormatSwitchingBaseTable, | |
) | |
from fontTools.ttLib.tables.otConverters import ( | |
ComputedInt, | |
SimpleValue, | |
Struct, | |
Short, | |
UInt8, | |
UShort, | |
IntValue, | |
FloatValue, | |
OptionalValue, | |
) | |
from fontTools.misc.roundTools import otRound | |
class BuildCallback(enum.Enum): | |
"""Keyed on (BEFORE_BUILD, class[, Format if available]). | |
Receives (dest, source). | |
Should return (dest, source), which can be new objects. | |
""" | |
BEFORE_BUILD = enum.auto() | |
"""Keyed on (AFTER_BUILD, class[, Format if available]). | |
Receives (dest). | |
Should return dest, which can be a new object. | |
""" | |
AFTER_BUILD = enum.auto() | |
"""Keyed on (CREATE_DEFAULT, class[, Format if available]). | |
Receives no arguments. | |
Should return a new instance of class. | |
""" | |
CREATE_DEFAULT = enum.auto() | |
def _assignable(convertersByName): | |
return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)} | |
def _isNonStrSequence(value): | |
return isinstance(value, collections.abc.Sequence) and not isinstance(value, str) | |
def _split_format(cls, source): | |
if _isNonStrSequence(source): | |
assert len(source) > 0, f"{cls} needs at least format from {source}" | |
fmt, remainder = source[0], source[1:] | |
elif isinstance(source, collections.abc.Mapping): | |
assert "Format" in source, f"{cls} needs at least Format from {source}" | |
remainder = source.copy() | |
fmt = remainder.pop("Format") | |
else: | |
raise ValueError(f"Not sure how to populate {cls} from {source}") | |
assert isinstance( | |
fmt, collections.abc.Hashable | |
), f"{cls} Format is not hashable: {fmt!r}" | |
assert fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}" | |
return fmt, remainder | |
class TableBuilder: | |
""" | |
Helps to populate things derived from BaseTable from maps, tuples, etc. | |
A table of lifecycle callbacks may be provided to add logic beyond what is possible | |
based on otData info for the target class. See BuildCallbacks. | |
""" | |
def __init__(self, callbackTable=None): | |
if callbackTable is None: | |
callbackTable = {} | |
self._callbackTable = callbackTable | |
def _convert(self, dest, field, converter, value): | |
enumClass = getattr(converter, "enumClass", None) | |
if enumClass: | |
if isinstance(value, enumClass): | |
pass | |
elif isinstance(value, str): | |
try: | |
value = getattr(enumClass, value.upper()) | |
except AttributeError: | |
raise ValueError(f"{value} is not a valid {enumClass}") | |
else: | |
value = enumClass(value) | |
elif isinstance(converter, IntValue): | |
value = otRound(value) | |
elif isinstance(converter, FloatValue): | |
value = float(value) | |
elif isinstance(converter, Struct): | |
if converter.repeat: | |
if _isNonStrSequence(value): | |
value = [self.build(converter.tableClass, v) for v in value] | |
else: | |
value = [self.build(converter.tableClass, value)] | |
setattr(dest, converter.repeat, len(value)) | |
else: | |
value = self.build(converter.tableClass, value) | |
elif callable(converter): | |
value = converter(value) | |
setattr(dest, field, value) | |
def build(self, cls, source): | |
assert issubclass(cls, BaseTable) | |
if isinstance(source, cls): | |
return source | |
callbackKey = (cls,) | |
fmt = None | |
if issubclass(cls, FormatSwitchingBaseTable): | |
fmt, source = _split_format(cls, source) | |
callbackKey = (cls, fmt) | |
dest = self._callbackTable.get( | |
(BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls() | |
)() | |
assert isinstance(dest, cls) | |
convByName = _assignable(cls.convertersByName) | |
skippedFields = set() | |
# For format switchers we need to resolve converters based on format | |
if issubclass(cls, FormatSwitchingBaseTable): | |
dest.Format = fmt | |
convByName = _assignable(convByName[dest.Format]) | |
skippedFields.add("Format") | |
# Convert sequence => mapping so before thunk only has to handle one format | |
if _isNonStrSequence(source): | |
# Sequence (typically list or tuple) assumed to match fields in declaration order | |
assert len(source) <= len( | |
convByName | |
), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values" | |
source = dict(zip(convByName.keys(), source)) | |
dest, source = self._callbackTable.get( | |
(BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s) | |
)(dest, source) | |
if isinstance(source, collections.abc.Mapping): | |
for field, value in source.items(): | |
if field in skippedFields: | |
continue | |
converter = convByName.get(field, None) | |
if not converter: | |
raise ValueError( | |
f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}" | |
) | |
self._convert(dest, field, converter, value) | |
else: | |
# let's try as a 1-tuple | |
dest = self.build(cls, (source,)) | |
for field, conv in convByName.items(): | |
if not hasattr(dest, field) and isinstance(conv, OptionalValue): | |
setattr(dest, field, conv.DEFAULT) | |
dest = self._callbackTable.get( | |
(BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d | |
)(dest) | |
return dest | |
class TableUnbuilder: | |
def __init__(self, callbackTable=None): | |
if callbackTable is None: | |
callbackTable = {} | |
self._callbackTable = callbackTable | |
def unbuild(self, table): | |
assert isinstance(table, BaseTable) | |
source = {} | |
callbackKey = (type(table),) | |
if isinstance(table, FormatSwitchingBaseTable): | |
source["Format"] = int(table.Format) | |
callbackKey += (table.Format,) | |
for converter in table.getConverters(): | |
if isinstance(converter, ComputedInt): | |
continue | |
value = getattr(table, converter.name) | |
enumClass = getattr(converter, "enumClass", None) | |
if enumClass: | |
source[converter.name] = value.name.lower() | |
elif isinstance(converter, Struct): | |
if converter.repeat: | |
source[converter.name] = [self.unbuild(v) for v in value] | |
else: | |
source[converter.name] = self.unbuild(value) | |
elif isinstance(converter, SimpleValue): | |
# "simple" values (e.g. int, float, str) need no further un-building | |
source[converter.name] = value | |
else: | |
raise NotImplementedError( | |
"Don't know how unbuild {value!r} with {converter!r}" | |
) | |
source = self._callbackTable.get(callbackKey, lambda s: s)(source) | |
return source | |