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
« 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.
4"""
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
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 """
33 BEFORE_BUILD = enum.auto()
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()
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()
48def _assignable(convertersByName):
49 return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)}
52def _isNonStrSequence(value):
53 return isinstance(value, collections.abc.Sequence) and not isinstance(value, str)
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}")
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}"
72 return fmt, remainder
75class TableBuilder:
76 """
77 Helps to populate things derived from BaseTable from maps, tuples, etc.
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 """
83 def __init__(self, callbackTable=None):
84 if callbackTable is None:
85 callbackTable = {}
86 self._callbackTable = callbackTable
88 def _convert(self, dest, field, converter, value):
89 enumClass = getattr(converter, "enumClass", None)
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)
102 elif isinstance(converter, IntValue):
103 value = otRound(value)
104 elif isinstance(converter, FloatValue):
105 value = float(value)
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)
119 setattr(dest, field, value)
121 def build(self, cls, source):
122 assert issubclass(cls, BaseTable)
124 if isinstance(source, cls):
125 return source
127 callbackKey = (cls,)
128 fmt = None
129 if issubclass(cls, FormatSwitchingBaseTable):
130 fmt, source = _split_format(cls, source)
131 callbackKey = (cls, fmt)
133 dest = self._callbackTable.get(
134 (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls()
135 )()
136 assert isinstance(dest, cls)
138 convByName = _assignable(cls.convertersByName)
139 skippedFields = set()
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")
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))
155 dest, source = self._callbackTable.get(
156 (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s)
157 )(dest, source)
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,))
173 for field, conv in convByName.items():
174 if not hasattr(dest, field) and isinstance(conv, OptionalValue):
175 setattr(dest, field, conv.DEFAULT)
177 dest = self._callbackTable.get(
178 (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d
179 )(dest)
181 return dest
184class TableUnbuilder:
185 def __init__(self, callbackTable=None):
186 if callbackTable is None:
187 callbackTable = {}
188 self._callbackTable = callbackTable
190 def unbuild(self, table):
191 assert isinstance(table, BaseTable)
193 source = {}
195 callbackKey = (type(table),)
196 if isinstance(table, FormatSwitchingBaseTable):
197 source["Format"] = int(table.Format)
198 callbackKey += (table.Format,)
200 for converter in table.getConverters():
201 if isinstance(converter, ComputedInt):
202 continue
203 value = getattr(table, converter.name)
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 )
221 source = self._callbackTable.get(callbackKey, lambda s: s)(source)
223 return source