Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/skeleton/instance.py: 13%
179 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 11:04 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 11:04 +0000
1from __future__ import annotations # noqa: required for pre-defined annotations
3import copy
4import fnmatch
5import typing as t
6import warnings
8from functools import partial
9from ..bones.base import BaseBone
10from .skeleton import Skeleton
11from viur.core import db
14class SkeletonInstance:
15 """
16 The actual wrapper around a Skeleton-Class. An object of this class is what's actually returned when you
17 call a Skeleton-Class. With ViUR3, you don't get an instance of a Skeleton-Class any more - it's always this
18 class. This is much faster as this is a small class.
19 """
20 __slots__ = {
21 "_cascade_deletion",
22 "accessedValues",
23 "boneMap",
24 "dbEntity",
25 "errors",
26 "is_cloned",
27 "renderAccessedValues",
28 "renderPreparation",
29 "skeletonCls",
30 }
32 def __init__(
33 self,
34 skel_cls: t.Type[Skeleton],
35 entity: t.Optional[db.Entity | dict] = None,
36 *,
37 bones: t.Iterable[str] = (),
38 bone_map: t.Optional[t.Dict[str, BaseBone]] = None,
39 clone: bool = False,
40 # FIXME: BELOW IS DEPRECATED!
41 clonedBoneMap: t.Optional[t.Dict[str, BaseBone]] = None,
42 ):
43 """
44 Creates a new SkeletonInstance based on `skel_cls`.
46 :param skel_cls: Is the base skeleton class to inherit from and reference to.
47 :param bones: If given, defines an iterable of bones that are take into the SkeletonInstance.
48 The order of the bones defines the order in the SkeletonInstance.
49 :param bone_map: A pre-defined bone map to use, or extend.
50 :param clone: If set True, performs a cloning of the used bone map, to be entirely stand-alone.
51 """
53 # TODO: Remove with ViUR-core 3.8; required by viur-datastore :'-(
54 if clonedBoneMap:
55 msg = "'clonedBoneMap' was renamed into 'bone_map'"
56 warnings.warn(msg, DeprecationWarning, stacklevel=2)
57 # logging.warning(msg, stacklevel=2)
59 if bone_map:
60 raise ValueError("Can't provide both 'bone_map' and 'clonedBoneMap'")
62 bone_map = clonedBoneMap
64 bone_map = bone_map or {}
66 if bones:
67 names = ("key",) + tuple(bones)
69 # generate full keys sequence based on definition; keeps order of patterns!
70 keys = []
71 for name in names:
72 if name in skel_cls.__boneMap__:
73 keys.append(name)
74 else:
75 keys.extend(fnmatch.filter(skel_cls.__boneMap__.keys(), name))
77 if clone:
78 bone_map |= {k: copy.deepcopy(skel_cls.__boneMap__[k]) for k in keys if skel_cls.__boneMap__[k]}
79 else:
80 bone_map |= {k: skel_cls.__boneMap__[k] for k in keys if skel_cls.__boneMap__[k]}
82 elif clone:
83 if bone_map:
84 bone_map = copy.deepcopy(bone_map)
85 else:
86 bone_map = copy.deepcopy(skel_cls.__boneMap__)
88 # generated or use provided bone_map
89 if bone_map:
90 self.boneMap = bone_map
92 else: # No Subskel, no Clone
93 self.boneMap = skel_cls.__boneMap__.copy()
95 if clone:
96 for v in self.boneMap.values():
97 v.isClonedInstance = True
99 self._cascade_deletion = False
100 self.accessedValues = {}
101 self.dbEntity = entity
102 self.errors = []
103 self.is_cloned = clone
104 self.renderAccessedValues = {}
105 self.renderPreparation = None
106 self.skeletonCls = skel_cls
108 def items(self, yieldBoneValues: bool = False) -> t.Iterable[tuple[str, BaseBone]]:
109 if yieldBoneValues:
110 for key in self.boneMap.keys():
111 yield key, self[key]
112 else:
113 yield from self.boneMap.items()
115 def keys(self) -> t.Iterable[str]:
116 yield from self.boneMap.keys()
118 def values(self) -> t.Iterable[t.Any]:
119 yield from self.boneMap.values()
121 def __iter__(self) -> t.Iterable[str]:
122 yield from self.keys()
124 def __contains__(self, item):
125 return item in self.boneMap
127 def __bool__(self):
128 return bool(self.accessedValues or self.dbEntity)
130 def get(self, item, default=None):
131 if item not in self:
132 return default
134 return self[item]
136 def update(self, *args, **kwargs) -> None:
137 self.__ior__(dict(*args, **kwargs))
139 def __setitem__(self, key, value):
140 assert self.renderPreparation is None, "Cannot modify values while rendering"
141 if isinstance(value, BaseBone):
142 raise AttributeError(f"Don't assign this bone object as skel[\"{key}\"] = ... anymore to the skeleton. "
143 f"Use skel.{key} = ... for bone to skeleton assignment!")
144 self.accessedValues[key] = value
146 def __getitem__(self, key):
147 if self.renderPreparation:
148 if key in self.renderAccessedValues:
149 return self.renderAccessedValues[key]
150 if key not in self.accessedValues:
151 boneInstance = self.boneMap.get(key, None)
152 if boneInstance:
153 if self.dbEntity is not None:
154 boneInstance.unserialize(self, key)
155 else:
156 self.accessedValues[key] = boneInstance.getDefaultValue(self)
157 if not self.renderPreparation:
158 return self.accessedValues.get(key)
159 value = self.renderPreparation(getattr(self, key), self, key, self.accessedValues.get(key))
160 self.renderAccessedValues[key] = value
161 return value
163 def __getattr__(self, item: str):
164 """
165 Get a special attribute from the SkeletonInstance
167 __getattr__ is called when an attribute access fails with an
168 AttributeError. So we know that this is not a real attribute of
169 the SkeletonInstance. But there are still a few special cases in which
170 attributes are loaded from the skeleton class.
171 """
172 if item == "boneMap":
173 return {} # There are __setAttr__ calls before __init__ has run
175 # Load attribute value from the Skeleton class
176 elif item in {
177 "database_adapters",
178 "interBoneValidations",
179 "kindName",
180 }:
181 return getattr(self.skeletonCls, item)
183 # FIXME: viur-datastore backward compatiblity REMOVE WITH VIUR4
184 elif item == "customDatabaseAdapter":
185 if prop := getattr(self.skeletonCls, "database_adapters"):
186 return prop[0] # viur-datastore assumes there is only ONE!
188 return None
190 # Load a @classmethod from the Skeleton class and bound this SkeletonInstance
191 elif item in {
192 "all",
193 "delete",
194 "patch",
195 "fromClient",
196 "fromDB",
197 "getCurrentSEOKeys",
198 "postDeletedHandler",
199 "postSavedHandler",
200 "preProcessBlobLocks",
201 "preProcessSerializedData",
202 "read",
203 "readonly",
204 "refresh",
205 "serialize",
206 "setBoneValue",
207 "toDB",
208 "unserialize",
209 "write",
210 }:
211 return partial(getattr(self.skeletonCls, item), self)
213 # Load a @property from the Skeleton class
214 try:
215 # Use try/except to save an if check
216 class_value = getattr(self.skeletonCls, item)
218 except AttributeError:
219 # Not inside the Skeleton class, okay at this point.
220 pass
222 else:
223 if isinstance(class_value, property):
224 # The attribute is a @property and can be called
225 # Note: `self` is this SkeletonInstance, not the Skeleton class.
226 # Therefore, you can access values inside the property method
227 # with item-access like `self["key"]`.
228 try:
229 return class_value.fget(self)
230 except AttributeError as exc:
231 # The AttributeError cannot be re-raised any further at this point.
232 # Since this would then be evaluated as an access error
233 # to the property attribute.
234 # Otherwise, it would be lost that it is an incorrect attribute access
235 # within this property (during the method call).
236 msg, *args = exc.args
237 msg = f"AttributeError: {msg}"
238 raise ValueError(msg, *args) from exc
239 # Load the bone instance from the bone map of this SkeletonInstance
240 try:
241 return self.boneMap[item]
242 except KeyError as exc:
243 raise AttributeError(f"{self.__class__.__name__!r} object has no attribute '{item}'") from exc
245 def __delattr__(self, item):
246 del self.boneMap[item]
247 if item in self.accessedValues:
248 del self.accessedValues[item]
249 if item in self.renderAccessedValues:
250 del self.renderAccessedValues[item]
252 def __setattr__(self, key, value):
253 if key in self.boneMap or isinstance(value, BaseBone):
254 if value is None:
255 del self.boneMap[key]
256 else:
257 value.__set_name__(self.skeletonCls, key)
258 self.boneMap[key] = value
259 elif key == "renderPreparation":
260 super().__setattr__(key, value)
261 self.renderAccessedValues.clear()
262 else:
263 super().__setattr__(key, value)
265 def __repr__(self) -> str:
266 return f"<SkeletonInstance of {self.skeletonCls.__name__} with {dict(self)}>"
268 def __str__(self) -> str:
269 return str(dict(self))
271 def __len__(self) -> int:
272 return len(self.boneMap)
274 def __ior__(self, other: dict | SkeletonInstance | db.Entity) -> SkeletonInstance:
275 if isinstance(other, dict):
276 for key, value in other.items():
277 self.setBoneValue(key, value)
278 elif isinstance(other, db.Entity):
279 new_entity = self.dbEntity or db.Entity()
280 # We're not overriding the key
281 for key, value in other.items():
282 new_entity[key] = value
283 self.setEntity(new_entity)
284 elif isinstance(other, SkeletonInstance):
285 for key, value in other.accessedValues.items():
286 self.accessedValues[key] = value
287 for key, value in other.dbEntity.items():
288 self.dbEntity[key] = value
289 else:
290 raise ValueError("Unsupported Type")
291 return self
293 def clone(self, *, apply_clone_strategy: bool = False) -> t.Self:
294 """
295 Clones a SkeletonInstance into a modificable, stand-alone instance.
296 This will also allow to modify the underlying data model.
297 """
298 res = SkeletonInstance(self.skeletonCls, bone_map=self.boneMap, clone=True)
299 if apply_clone_strategy:
300 for bone_name, bone_instance in self.items():
301 bone_instance.clone_value(res, self, bone_name)
302 else:
303 res.accessedValues = copy.deepcopy(self.accessedValues)
304 res.dbEntity = copy.deepcopy(self.dbEntity)
305 res.is_cloned = True
306 if not apply_clone_strategy:
307 res.renderAccessedValues = copy.deepcopy(self.renderAccessedValues)
308 # else: Depending on the strategy the values are cloned in bone_instance.clone_value too
310 return res
312 def ensure_is_cloned(self):
313 """
314 Ensured this SkeletonInstance is a stand-alone clone, which can be modified.
315 Does nothing in case it was already cloned before.
316 """
317 if not self.is_cloned:
318 return self.clone()
320 return self
322 def setEntity(self, entity: db.Entity):
323 self.dbEntity = entity
324 self.accessedValues = {}
325 self.renderAccessedValues = {}
327 def structure(self) -> dict:
328 return {
329 key: bone.structure() | {"sortindex": i}
330 for i, (key, bone) in enumerate(self.items())
331 }
333 def dump(self):
334 """
335 Return a simplified version of the bone values in this skeleton.
336 This can be used for example in the JSON renderer.
337 """
339 return {
340 bone_name: bone.dump(self, bone_name) for bone_name, bone in self.items()
341 }
343 def __deepcopy__(self, memodict):
344 res = self.clone()
345 memodict[id(self)] = res
346 return res