Coverage for /usr/lib/python3/dist-packages/fontTools/colorLib/table_builder.py: 18%

120 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

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 fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}" 

71 

72 return fmt, remainder 

73 

74 

75class TableBuilder: 

76 """ 

77 Helps to populate things derived from BaseTable from maps, tuples, etc. 

78 

79 A table of lifecycle callbacks may be provided to add logic beyond what is possible 

80 based on otData info for the target class. See BuildCallbacks. 

81 """ 

82 

83 def __init__(self, callbackTable=None): 

84 if callbackTable is None: 

85 callbackTable = {} 

86 self._callbackTable = callbackTable 

87 

88 def _convert(self, dest, field, converter, value): 

89 enumClass = getattr(converter, "enumClass", None) 

90 

91 if enumClass: 

92 if isinstance(value, enumClass): 

93 pass 

94 elif isinstance(value, str): 

95 try: 

96 value = getattr(enumClass, value.upper()) 

97 except AttributeError: 

98 raise ValueError(f"{value} is not a valid {enumClass}") 

99 else: 

100 value = enumClass(value) 

101 

102 elif isinstance(converter, IntValue): 

103 value = otRound(value) 

104 elif isinstance(converter, FloatValue): 

105 value = float(value) 

106 

107 elif isinstance(converter, Struct): 

108 if converter.repeat: 

109 if _isNonStrSequence(value): 

110 value = [self.build(converter.tableClass, v) for v in value] 

111 else: 

112 value = [self.build(converter.tableClass, value)] 

113 setattr(dest, converter.repeat, len(value)) 

114 else: 

115 value = self.build(converter.tableClass, value) 

116 elif callable(converter): 

117 value = converter(value) 

118 

119 setattr(dest, field, value) 

120 

121 def build(self, cls, source): 

122 assert issubclass(cls, BaseTable) 

123 

124 if isinstance(source, cls): 

125 return source 

126 

127 callbackKey = (cls,) 

128 fmt = None 

129 if issubclass(cls, FormatSwitchingBaseTable): 

130 fmt, source = _split_format(cls, source) 

131 callbackKey = (cls, fmt) 

132 

133 dest = self._callbackTable.get( 

134 (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls() 

135 )() 

136 assert isinstance(dest, cls) 

137 

138 convByName = _assignable(cls.convertersByName) 

139 skippedFields = set() 

140 

141 # For format switchers we need to resolve converters based on format 

142 if issubclass(cls, FormatSwitchingBaseTable): 

143 dest.Format = fmt 

144 convByName = _assignable(convByName[dest.Format]) 

145 skippedFields.add("Format") 

146 

147 # Convert sequence => mapping so before thunk only has to handle one format 

148 if _isNonStrSequence(source): 

149 # Sequence (typically list or tuple) assumed to match fields in declaration order 

150 assert len(source) <= len( 

151 convByName 

152 ), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values" 

153 source = dict(zip(convByName.keys(), source)) 

154 

155 dest, source = self._callbackTable.get( 

156 (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s) 

157 )(dest, source) 

158 

159 if isinstance(source, collections.abc.Mapping): 

160 for field, value in source.items(): 

161 if field in skippedFields: 

162 continue 

163 converter = convByName.get(field, None) 

164 if not converter: 

165 raise ValueError( 

166 f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}" 

167 ) 

168 self._convert(dest, field, converter, value) 

169 else: 

170 # let's try as a 1-tuple 

171 dest = self.build(cls, (source,)) 

172 

173 for field, conv in convByName.items(): 

174 if not hasattr(dest, field) and isinstance(conv, OptionalValue): 

175 setattr(dest, field, conv.DEFAULT) 

176 

177 dest = self._callbackTable.get( 

178 (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d 

179 )(dest) 

180 

181 return dest 

182 

183 

184class TableUnbuilder: 

185 def __init__(self, callbackTable=None): 

186 if callbackTable is None: 

187 callbackTable = {} 

188 self._callbackTable = callbackTable 

189 

190 def unbuild(self, table): 

191 assert isinstance(table, BaseTable) 

192 

193 source = {} 

194 

195 callbackKey = (type(table),) 

196 if isinstance(table, FormatSwitchingBaseTable): 

197 source["Format"] = int(table.Format) 

198 callbackKey += (table.Format,) 

199 

200 for converter in table.getConverters(): 

201 if isinstance(converter, ComputedInt): 

202 continue 

203 value = getattr(table, converter.name) 

204 

205 enumClass = getattr(converter, "enumClass", None) 

206 if enumClass: 

207 source[converter.name] = value.name.lower() 

208 elif isinstance(converter, Struct): 

209 if converter.repeat: 

210 source[converter.name] = [self.unbuild(v) for v in value] 

211 else: 

212 source[converter.name] = self.unbuild(value) 

213 elif isinstance(converter, SimpleValue): 

214 # "simple" values (e.g. int, float, str) need no further un-building 

215 source[converter.name] = value 

216 else: 

217 raise NotImplementedError( 

218 "Don't know how unbuild {value!r} with {converter!r}" 

219 ) 

220 

221 source = self._callbackTable.get(callbackKey, lambda s: s)(source) 

222 

223 return source