| """ |
| 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 |