Coverage for lib/lottie/objects/base.py: 78%

232 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-20 16:17 +0100

1import enum 

2import inspect 

3import importlib 

4from ..nvector import NVector 

5from ..utils.color import Color, ColorMode 

6 

7 

8class LottieBase: 

9 """! 

10 Base class for Lottie JSON objects bindings 

11 """ 

12 def to_dict(self): 

13 """! 

14 Serializes into a JSON object fit for the Lottie format 

15 """ 

16 raise NotImplementedError 

17 

18 @classmethod 

19 def load(cls, lottiedict): 

20 """! 

21 Loads from a JSON object 

22 @returns An instance of the class 

23 """ 

24 raise NotImplementedError 

25 

26 def clone(self): 

27 """! 

28 Returns a copy of the object 

29 """ 

30 raise NotImplementedError 

31 

32 

33class EnumMeta(enum.EnumMeta): 

34 """! 

35 Hack to counter-hack the hack in enum meta 

36 """ 

37 def __new__(cls, name, bases, classdict): 

38 classdict["__reduce_ex__"] = lambda *a, **kw: None # pragma: no cover 

39 return super().__new__(cls, name, bases, classdict) 

40 

41 

42class LottieEnum(LottieBase, enum.Enum, metaclass=EnumMeta): 

43 """! 

44 Base class for enum-like types in the Lottie JSON structure 

45 """ 

46 def to_dict(self): 

47 return self.value 

48 

49 @classmethod 

50 def load(cls, lottieint): 

51 return cls(lottieint) 

52 

53 def clone(self): 

54 return self 

55 

56 

57class PseudoList: 

58 """! 

59 List tag for some weird values in the Lottie JSON 

60 """ 

61 pass 

62 

63 

64class LottieValueConverter: 

65 """! 

66 Factory for property types that require special conversions 

67 """ 

68 def __init__(self, py, lottie, name=None): 

69 self.py = py 

70 self.lottie = lottie 

71 self.name = name or "%s but displayed as %s" % (self.py.__name__, self.lottie.__name__) 

72 

73 def py_to_lottie(self, val): 

74 return self.lottie(val) 

75 

76 def lottie_to_py(self, val): 

77 return self.py(val) 

78 

79 @property 

80 def __name__(self): 

81 return self.name 

82 

83 

84## For values in Lottie that are bools but ints in the JSON 

85PseudoBool = LottieValueConverter(bool, int, "0-1 int") 

86 

87 

88class LottieProp: 

89 """! 

90 Lottie <-> Python property mapper 

91 """ 

92 def __init__(self, name, lottie, type=float, list=False, cond=None): 

93 ## Name of the Python property 

94 self.name = name 

95 ## Name of the Lottie JSON property 

96 self.lottie = lottie 

97 ## Type of the property 

98 ## @see LottieValueConverter, PseudoBool 

99 self.type = type 

100 ## Whether the property is a list of self.type 

101 ## @see PseudoList 

102 self.list = list 

103 ## Condition on when the property is loaded from the Lottie JSON 

104 self.cond = cond 

105 

106 def get(self, obj): 

107 """! 

108 Returns the value of the property from a Python object 

109 """ 

110 return getattr(obj, self.name) 

111 

112 def set(self, obj, value): 

113 """! 

114 Sets the value of the property from a Python object 

115 """ 

116 if isinstance(getattr(obj.__class__, self.name, None), property): 

117 return 

118 

119 # Keep the default on missing value 

120 if hasattr(obj, self.name) and value is None: 

121 return 

122 

123 return setattr(obj, self.name, value) 

124 

125 def load_from_parent(self, lottiedict): 

126 """! 

127 Returns the value for this property from a JSON dict representing the parent object 

128 @returns The loaded value or @c None if the property is not in @p lottiedict 

129 """ 

130 if self.lottie in lottiedict: 

131 return self.load(lottiedict[self.lottie]) 

132 return None 

133 

134 def load_into(self, lottiedict, obj): 

135 """! 

136 Loads from a Lottie dict into an object 

137 """ 

138 if self.cond and not self.cond(lottiedict): 

139 return 

140 self.set(obj, self.load_from_parent(lottiedict)) 

141 

142 def load(self, lottieval): 

143 """! 

144 Loads the property from a JSON value 

145 @returns the Python equivalent of the JSON value 

146 """ 

147 if self.list is PseudoList and isinstance(lottieval, list): 

148 return self._load_scalar(lottieval[0]) 

149 elif self.list is True: 

150 return list(filter(lambda x: x is not None, ( 

151 self._load_scalar(it) 

152 for it in lottieval 

153 ))) 

154 return self._load_scalar(lottieval) 

155 

156 def _load_scalar(self, lottieval): 

157 if lottieval is None: 157 ↛ 158line 157 didn't jump to line 158, because the condition on line 157 was never true

158 return None 

159 if inspect.isclass(self.type) and issubclass(self.type, LottieBase): 

160 return self.type.load(lottieval) 

161 elif isinstance(self.type, type) and isinstance(lottieval, self.type): 

162 return lottieval 

163 elif isinstance(self.type, LottieValueConverter): 

164 return self.type.lottie_to_py(lottieval) 

165 elif self.type is NVector: 

166 return NVector(*lottieval) 

167 elif self.type is Color: 167 ↛ 168line 167 didn't jump to line 168, because the condition on line 167 was never true

168 return Color(*lottieval) 

169 if isinstance(lottieval, list) and lottieval: 169 ↛ 170line 169 didn't jump to line 170, because the condition on line 169 was never true

170 lottieval = lottieval[0] 

171 return self.type(lottieval) 

172 

173 def to_dict(self, obj): 

174 """! 

175 Converts the value of the property as from @p obj into a JSON value 

176 @param obj LottieObject with this property 

177 """ 

178 val = self.get(obj) 

179 if isinstance(self.type, LottieValueConverter): 

180 val = self.type.py_to_lottie(val) 

181 

182 val = self._basic_to_dict(val) 

183 

184 if self.list is PseudoList: 

185 if not isinstance(obj, list): 185 ↛ 187line 185 didn't jump to line 187, because the condition on line 185 was never false

186 return [val] 

187 return val 

188 

189 def _basic_to_dict(self, v): 

190 if isinstance(v, LottieBase): 

191 return v.to_dict() 

192 elif isinstance(v, Color): 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true

193 return list(map(self._basic_to_dict, v.to_rgb().components)) 

194 elif isinstance(v, NVector): 

195 return list(map(self._basic_to_dict, v.components)) 

196 elif isinstance(v, (list, tuple)): 

197 return list(map(self._basic_to_dict, v)) 

198 elif isinstance(v, (int, str, bool)): 

199 return v 

200 elif isinstance(v, float): 

201 if v % 1 == 0: 201 ↛ 203line 201 didn't jump to line 203, because the condition on line 201 was never false

202 return int(v) 

203 return v # round(v, 3) 

204 else: 

205 raise Exception("Unknown value {!r}".format(v)) 

206 

207 def __repr__(self): 

208 return "<LottieProp %s:%s>" % (self.name, self.lottie) 

209 

210 def clone_value(self, value): 

211 if isinstance(value, (list, tuple)): 

212 return [self.clone_value(v) for v in value] 

213 if isinstance(value, (LottieBase, NVector)): 

214 return value.clone() 

215 if isinstance(value, (int, float, bool, str)) or value is None: 215 ↛ 217line 215 didn't jump to line 217, because the condition on line 215 was never false

216 return value 

217 raise Exception("Could not convert {!r}".format(value)) 

218 

219 

220class LottieObjectMeta(type): 

221 def __new__(cls, name, bases, attr): 

222 props = [] 

223 for base in bases: 

224 if type(base) == cls: 

225 props += base._props 

226 attr["_props"] = props + attr.get("_props", []) 

227 return super().__new__(cls, name, bases, attr) 

228 

229 

230class LottieObject(LottieBase, metaclass=LottieObjectMeta): 

231 """! 

232 @brief Base class for mapping Python classes into Lottie JSON objects 

233 """ 

234 def __init__(self): 

235 pass 

236 

237 def to_dict(self): 

238 return { 

239 prop.lottie: prop.to_dict(self) 

240 for prop in self._props 

241 if prop.get(self) is not None 

242 } 

243 

244 @classmethod 

245 def load(cls, lottiedict): 

246 if "__pyclass" in lottiedict: 

247 return CustomObject.load(lottiedict) 

248 if not lottiedict: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 return None 

250 cls = cls._load_get_class(lottiedict) 

251 obj = cls() 

252 for prop in cls._props: 

253 prop.load_into(lottiedict, obj) 

254 return obj 

255 

256 @classmethod 

257 def _load_get_class(cls, lottiedict): 

258 return cls 

259 

260 def find(self, search, propname="name"): 

261 """! 

262 @param search The value of the property to search 

263 @param propname The name of the property used to search 

264 @brief Recursively searches for child objects with a matching property 

265 """ 

266 if getattr(self, propname, None) == search: 

267 return self 

268 for prop in self._props: 

269 v = prop.get(self) 

270 if isinstance(v, LottieObject): 

271 found = v.find(search, propname) 

272 if found: 

273 return found 

274 elif isinstance(v, list) and v and isinstance(v[0], LottieObject): 

275 for obj in v: 

276 found = obj.find(search, propname) 

277 if found: 

278 return found 

279 return None 

280 

281 def find_all(self, type, predicate=None, include_self=True): 

282 """! 

283 Find all child objects that match a predicate 

284 @param type Type (or tuple of types) of the objects to match 

285 @param predicate Function that returns true on the objects to find 

286 @param include_self Whether should counsider `self` for a potential match 

287 """ 

288 

289 if isinstance(self, type) and include_self: 

290 if not predicate or predicate(self): 

291 yield self 

292 

293 for prop in self._props: 

294 v = prop.get(self) 

295 

296 if isinstance(v, LottieObject): 

297 for found in v.find_all(type, predicate, True): 

298 yield found 

299 elif isinstance(v, list) and v and isinstance(v[0], LottieObject): 

300 for child in v: 

301 for found in child.find_all(type, predicate, True): 

302 yield found 

303 

304 def clone(self): 

305 obj = self.__class__() 

306 for prop in self._props: 

307 v = prop.get(self) 

308 prop.set(obj, prop.clone_value(v)) 

309 return obj 

310 

311 def clone_into(self, other): 

312 for prop in self._props: 

313 v = prop.get(self) 

314 prop.set(other, prop.clone_value(v)) 

315 

316 def __str__(self): 

317 return type(self).__name__ 

318 

319 

320class Index: 

321 """! 

322 @brief Simple iterator to generate increasing integers 

323 """ 

324 def __init__(self): 

325 self._i = -1 

326 

327 def __next__(self): 

328 self._i += 1 

329 return self._i 

330 

331 

332class CustomObject(LottieObject): 

333 """! 

334 Allows extending the Lottie shapes with custom Python classes 

335 """ 

336 wrapped_lottie = LottieObject 

337 

338 def __init__(self): 

339 self.wrapped = self.wrapped_lottie() 

340 

341 @classmethod 

342 def load(cls, lottiedict): 

343 ld = lottiedict.copy() 

344 classname = ld.pop("__pyclass") 

345 modn, clsn = classname.rsplit(".", 1) 

346 subcls = getattr(importlib.import_module(modn), clsn) 

347 obj = subcls() 

348 for prop in subcls._props: 

349 prop.load_into(lottiedict, obj) 

350 obj.wrapped = subcls.wrapped_lottie.load(ld) 

351 return obj 

352 

353 def clone(self): 

354 obj = self.__class__(**self.to_pyctor()) 

355 obj.wrapped = self.wrapped.clone() 

356 return obj 

357 

358 def to_dict(self): 

359 dict = self.wrapped.to_dict() 

360 dict["__pyclass"] = "{0.__module__}.{0.__name__}".format(self.__class__) 

361 dict.update(LottieObject.to_dict(self)) 

362 return dict 

363 

364 def _build_wrapped(self): 

365 return self.wrapped_lottie() 

366 

367 def refresh(self): 

368 self.wrapped = self._build_wrapped() 

369 

370 

371class ObjectVisitor: 

372 DONT_RECURSE = object() 

373 

374 def __call__(self, lottie_object): 

375 self._process(lottie_object) 

376 

377 def _process(self, lottie_object): 

378 self.visit(lottie_object) 

379 for p in lottie_object._props: 

380 pval = p.get(lottie_object) 

381 if self.visit_property(lottie_object, p, pval) is not self.DONT_RECURSE: 

382 if isinstance(pval, LottieObject): 

383 self._process(pval) 

384 elif isinstance(pval, list) and pval and isinstance(pval[0], LottieObject): 

385 for c in pval: 

386 self._process(c) 

387 

388 def visit(self, object): 

389 pass 

390 

391 def visit_property(self, object, property, value): 

392 pass