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
« 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
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
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
26 def clone(self):
27 """!
28 Returns a copy of the object
29 """
30 raise NotImplementedError
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)
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
49 @classmethod
50 def load(cls, lottieint):
51 return cls(lottieint)
53 def clone(self):
54 return self
57class PseudoList:
58 """!
59 List tag for some weird values in the Lottie JSON
60 """
61 pass
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__)
73 def py_to_lottie(self, val):
74 return self.lottie(val)
76 def lottie_to_py(self, val):
77 return self.py(val)
79 @property
80 def __name__(self):
81 return self.name
84## For values in Lottie that are bools but ints in the JSON
85PseudoBool = LottieValueConverter(bool, int, "0-1 int")
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
106 def get(self, obj):
107 """!
108 Returns the value of the property from a Python object
109 """
110 return getattr(obj, self.name)
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
119 # Keep the default on missing value
120 if hasattr(obj, self.name) and value is None:
121 return
123 return setattr(obj, self.name, value)
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
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))
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)
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)
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)
182 val = self._basic_to_dict(val)
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
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))
207 def __repr__(self):
208 return "<LottieProp %s:%s>" % (self.name, self.lottie)
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))
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)
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
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 }
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
256 @classmethod
257 def _load_get_class(cls, lottiedict):
258 return cls
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
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 """
289 if isinstance(self, type) and include_self:
290 if not predicate or predicate(self):
291 yield self
293 for prop in self._props:
294 v = prop.get(self)
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
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
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))
316 def __str__(self):
317 return type(self).__name__
320class Index:
321 """!
322 @brief Simple iterator to generate increasing integers
323 """
324 def __init__(self):
325 self._i = -1
327 def __next__(self):
328 self._i += 1
329 return self._i
332class CustomObject(LottieObject):
333 """!
334 Allows extending the Lottie shapes with custom Python classes
335 """
336 wrapped_lottie = LottieObject
338 def __init__(self):
339 self.wrapped = self.wrapped_lottie()
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
353 def clone(self):
354 obj = self.__class__(**self.to_pyctor())
355 obj.wrapped = self.wrapped.clone()
356 return obj
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
364 def _build_wrapped(self):
365 return self.wrapped_lottie()
367 def refresh(self):
368 self.wrapped = self._build_wrapped()
371class ObjectVisitor:
372 DONT_RECURSE = object()
374 def __call__(self, lottie_object):
375 self._process(lottie_object)
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)
388 def visit(self, object):
389 pass
391 def visit_property(self, object, property, value):
392 pass