1""" 2colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such. 3 4""" 5 6import collections 7import enum 8from fontTools.ttLib.tables.otBase import ( 9 BaseTable, 10 FormatSwitchingBaseTable, 11 UInt8FormatSwitchingBaseTable, 12) 13from fontTools.ttLib.tables.otConverters import ( 14 ComputedInt, 15 SimpleValue, 16 Struct, 17 Short, 18 UInt8, 19 UShort, 20 IntValue, 21 FloatValue, 22 OptionalValue, 23) 24from fontTools.misc.roundTools import otRound 25 26 27class BuildCallback(enum.Enum): 28 """Keyed on (BEFORE_BUILD, class[, Format if available]). 29 Receives (dest, source). 30 Should return (dest, source), which can be new objects. 31 """ 32 33 BEFORE_BUILD = enum.auto() 34 35 """Keyed on (AFTER_BUILD, class[, Format if available]). 36 Receives (dest). 37 Should return dest, which can be a new object. 38 """ 39 AFTER_BUILD = enum.auto() 40 41 """Keyed on (CREATE_DEFAULT, class[, Format if available]). 42 Receives no arguments. 43 Should return a new instance of class. 44 """ 45 CREATE_DEFAULT = enum.auto() 46 47 48def _assignable(convertersByName): 49 return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)} 50 51 52def _isNonStrSequence(value): 53 return isinstance(value, collections.abc.Sequence) and not isinstance(value, str) 54 55 56def _split_format(cls, source): 57 if _isNonStrSequence(source): 58 assert len(source) > 0, f"{cls} needs at least format from {source}" 59 fmt, remainder = source[0], source[1:] 60 elif isinstance(source, collections.abc.Mapping): 61 assert "Format" in source, f"{cls} needs at least Format from {source}" 62 remainder = source.copy() 63 fmt = remainder.pop("Format") 64 else: 65 raise ValueError(f"Not sure how to populate {cls} from {source}") 66 67 assert isinstance( 68 fmt, collections.abc.Hashable 69 ), f"{cls} Format is not hashable: {fmt!r}" 70 assert ( 71 fmt in cls.convertersByName 72 ), f"{cls} invalid Format: {fmt!r}" 73 74 return fmt, remainder 75 76 77class TableBuilder: 78 """ 79 Helps to populate things derived from BaseTable from maps, tuples, etc. 80 81 A table of lifecycle callbacks may be provided to add logic beyond what is possible 82 based on otData info for the target class. See BuildCallbacks. 83 """ 84 85 def __init__(self, callbackTable=None): 86 if callbackTable is None: 87 callbackTable = {} 88 self._callbackTable = callbackTable 89 90 def _convert(self, dest, field, converter, value): 91 enumClass = getattr(converter, "enumClass", None) 92 93 if enumClass: 94 if isinstance(value, enumClass): 95 pass 96 elif isinstance(value, str): 97 try: 98 value = getattr(enumClass, value.upper()) 99 except AttributeError: 100 raise ValueError(f"{value} is not a valid {enumClass}") 101 else: 102 value = enumClass(value) 103 104 elif isinstance(converter, IntValue): 105 value = otRound(value) 106 elif isinstance(converter, FloatValue): 107 value = float(value) 108 109 elif isinstance(converter, Struct): 110 if converter.repeat: 111 if _isNonStrSequence(value): 112 value = [self.build(converter.tableClass, v) for v in value] 113 else: 114 value = [self.build(converter.tableClass, value)] 115 setattr(dest, converter.repeat, len(value)) 116 else: 117 value = self.build(converter.tableClass, value) 118 elif callable(converter): 119 value = converter(value) 120 121 setattr(dest, field, value) 122 123 def build(self, cls, source): 124 assert issubclass(cls, BaseTable) 125 126 if isinstance(source, cls): 127 return source 128 129 callbackKey = (cls,) 130 fmt = None 131 if issubclass(cls, FormatSwitchingBaseTable): 132 fmt, source = _split_format(cls, source) 133 callbackKey = (cls, fmt) 134 135 dest = self._callbackTable.get( 136 (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls() 137 )() 138 assert isinstance(dest, cls) 139 140 convByName = _assignable(cls.convertersByName) 141 skippedFields = set() 142 143 # For format switchers we need to resolve converters based on format 144 if issubclass(cls, FormatSwitchingBaseTable): 145 dest.Format = fmt 146 convByName = _assignable(convByName[dest.Format]) 147 skippedFields.add("Format") 148 149 # Convert sequence => mapping so before thunk only has to handle one format 150 if _isNonStrSequence(source): 151 # Sequence (typically list or tuple) assumed to match fields in declaration order 152 assert len(source) <= len( 153 convByName 154 ), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values" 155 source = dict(zip(convByName.keys(), source)) 156 157 dest, source = self._callbackTable.get( 158 (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s) 159 )(dest, source) 160 161 if isinstance(source, collections.abc.Mapping): 162 for field, value in source.items(): 163 if field in skippedFields: 164 continue 165 converter = convByName.get(field, None) 166 if not converter: 167 raise ValueError( 168 f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}" 169 ) 170 self._convert(dest, field, converter, value) 171 else: 172 # let's try as a 1-tuple 173 dest = self.build(cls, (source,)) 174 175 for field, conv in convByName.items(): 176 if not hasattr(dest, field) and isinstance(conv, OptionalValue): 177 setattr(dest, field, conv.DEFAULT) 178 179 dest = self._callbackTable.get( 180 (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d 181 )(dest) 182 183 return dest 184 185 186class TableUnbuilder: 187 def __init__(self, callbackTable=None): 188 if callbackTable is None: 189 callbackTable = {} 190 self._callbackTable = callbackTable 191 192 def unbuild(self, table): 193 assert isinstance(table, BaseTable) 194 195 source = {} 196 197 callbackKey = (type(table),) 198 if isinstance(table, FormatSwitchingBaseTable): 199 source["Format"] = int(table.Format) 200 callbackKey += (table.Format,) 201 202 for converter in table.getConverters(): 203 if isinstance(converter, ComputedInt): 204 continue 205 value = getattr(table, converter.name) 206 207 enumClass = getattr(converter, "enumClass", None) 208 if enumClass: 209 source[converter.name] = value.name.lower() 210 elif isinstance(converter, Struct): 211 if converter.repeat: 212 source[converter.name] = [self.unbuild(v) for v in value] 213 else: 214 source[converter.name] = self.unbuild(value) 215 elif isinstance(converter, SimpleValue): 216 # "simple" values (e.g. int, float, str) need no further un-building 217 source[converter.name] = value 218 else: 219 raise NotImplementedError( 220 "Don't know how unbuild {value!r} with {converter!r}" 221 ) 222 223 source = self._callbackTable.get(callbackKey, lambda s: s)(source) 224 225 return source 226