Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/skeleton/meta.py: 68%
73 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
1import inspect
2import logging # noqa
3import os
4import string
5import sys
6import typing as t
8from .adapter import ViurTagsSearchAdapter
9from .. import utils
10from ..bones.base import BaseBone, getSystemInitialized
11from ..config import conf
13_UNDEFINED_KINDNAME = object()
14ABSTRACT_SKEL_CLS_SUFFIX = "AbstractSkel"
16Skeleton_Cls = t.TypeVar("Skeleton_Cls", bound="BaseSkeleton")
17"""TypeVar for generic skeleton typing.
19Use this to annotate functions and classes that work with a specific, but not yet known,
20Skeleton subclass. The type checker then knows which concrete Skeleton is in use and can
21validate bone access.
23Example — typed helper function::
25 from viur.core.skeleton import Skeleton_Cls, SkeletonInstance
27 def clone_and_set_owner(skel: SkeletonInstance[Skeleton_Cls], owner: str) -> SkeletonInstance[Skeleton_Cls]:
28 cloned = skel.clone()
29 cloned["owner"] = owner
30 return cloned
32Example — typed module override::
34 class ProductModule(List):
35 def editSkel(self) -> SkeletonInstance[ProductSkel]:
36 skel = super().editSkel()
37 skel.price.readOnly = True
38 return skel
40When calling a classmethod on a concrete Skeleton, use ``t.Self`` instead so the type checker
41automatically narrows to the calling class::
43 class BaseSkeleton:
44 @classmethod
45 def fromClient(cls, skel: SkeletonInstance[t.Self], data: dict) -> bool: ...
47 # Calling on a concrete class: type checker knows skel is SkeletonInstance[ProductSkel]
48 ProductSkel.fromClient(skel, request.POST)
49"""
52class MetaBaseSkel(type):
53 """
54 This is the metaclass for Skeletons.
55 It is used to enforce several restrictions on bone names, etc.
56 """
57 _skelCache = {} # Mapping kindName -> SkelCls
58 _allSkelClasses = set() # list of all known skeleton classes (including Ref and Mail-Skels)
60 # List of reserved keywords and function names
61 __reserved_keywords = {
62 "all",
63 "bounce",
64 "clone",
65 "cursor",
66 "delete",
67 "errors",
68 "fromClient",
69 "fromDB",
70 "get",
71 "getCurrentSEOKeys",
72 "items",
73 "keys",
74 "limit",
75 "orderby",
76 "orderdir",
77 "patch",
78 "postDeletedHandler",
79 "postSavedHandler",
80 "preProcessBlobLocks",
81 "preProcessSerializedData",
82 "read",
83 "readonly",
84 "refresh",
85 "self",
86 "serialize",
87 "setBoneValue",
88 "structure",
89 "style",
90 "toDB",
91 "unserialize",
92 "values",
93 "write",
94 }
96 __allowed_chars = string.ascii_letters + string.digits + "_"
98 def __init__(cls, name, bases, dct, **kwargs):
99 cls.__boneMap__ = MetaBaseSkel.generate_bonemap(cls)
101 if not getSystemInitialized() and not cls.__name__.endswith(ABSTRACT_SKEL_CLS_SUFFIX): 101 ↛ 104line 101 didn't jump to line 104 because the condition on line 101 was always true
102 MetaBaseSkel._allSkelClasses.add(cls)
104 super().__init__(name, bases, dct)
106 @staticmethod
107 def generate_bonemap(cls):
108 """
109 Recursively constructs a dict of bones from
110 """
111 map = {}
113 for base in cls.__bases__:
114 if "__viurBaseSkeletonMarker__" in dir(base):
115 map |= MetaBaseSkel.generate_bonemap(base)
117 for key in cls.__dict__:
118 prop = getattr(cls, key)
120 if isinstance(prop, BaseBone):
121 if not all([c in MetaBaseSkel.__allowed_chars for c in key]): 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 raise AttributeError(f"Invalid bone name: {key!r} contains invalid characters")
123 elif key in MetaBaseSkel.__reserved_keywords: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 raise AttributeError(f"Invalid bone name: {key!r} is reserved and cannot be used")
126 map[key] = prop
128 elif prop is None and key in map: # Allow removing a bone in a subclass by setting it to None
129 del map[key]
131 return map
133 def __setattr__(self, key, value):
134 super().__setattr__(key, value)
135 if isinstance(value, BaseBone): 135 ↛ 137line 135 didn't jump to line 137 because the condition on line 135 was never true
136 # Call BaseBone.__set_name__ manually for bones that are assigned at runtime
137 value.__set_name__(self, key)
140class MetaSkel(MetaBaseSkel):
142 def __init__(cls, name, bases, dct, **kwargs):
143 super().__init__(name, bases, dct, **kwargs)
145 relNewFileName = inspect.getfile(cls) \
146 .replace(str(conf.instance.project_base_path), "") \
147 .replace(str(conf.instance.core_base_path), "")
149 # Check if we have an abstract skeleton
150 if cls.__name__.endswith(ABSTRACT_SKEL_CLS_SUFFIX): 150 ↛ 152line 150 didn't jump to line 152 because the condition on line 150 was never true
151 # Ensure that it doesn't have a kindName
152 assert cls.kindName is _UNDEFINED_KINDNAME or cls.kindName is None, \
153 "Abstract Skeletons can't have a kindName"
154 # Prevent any further processing by this class; it has to be sub-classed before it can be used
155 return
157 # Automatic determination of the kindName, if the class is not part of viur.core.
158 if ( 158 ↛ 163line 158 didn't jump to line 163 because the condition on line 158 was never true
159 cls.kindName is _UNDEFINED_KINDNAME
160 and not relNewFileName.strip(os.path.sep).startswith("viur")
161 and "viur_doc_build" not in dir(sys) # do not check during documentation build
162 ):
163 if cls.__name__.endswith("Skel"):
164 cls.kindName = cls.__name__.lower()[:-4]
165 else:
166 cls.kindName = cls.__name__.lower()
168 # Try to determine which skeleton definition takes precedence
169 if cls.kindName and cls.kindName is not _UNDEFINED_KINDNAME and cls.kindName in MetaBaseSkel._skelCache: 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true
170 relOldFileName = inspect.getfile(MetaBaseSkel._skelCache[cls.kindName]) \
171 .replace(str(conf.instance.project_base_path), "") \
172 .replace(str(conf.instance.core_base_path), "")
173 idxOld = min(
174 [x for (x, y) in enumerate(conf.skeleton_search_path) if relOldFileName.startswith(y)] + [999])
175 idxNew = min(
176 [x for (x, y) in enumerate(conf.skeleton_search_path) if relNewFileName.startswith(y)] + [999])
177 if idxNew == 999:
178 # We could not determine a priority for this class as its from a path not listed in the config
179 raise NotImplementedError(
180 "Skeletons must be defined in a folder listed in conf.skeleton_search_path")
181 elif idxOld < idxNew: # Lower index takes precedence
182 # The currently processed skeleton has a lower priority than the one we already saw - just ignore it
183 return
184 elif idxOld > idxNew:
185 # The currently processed skeleton has a higher priority, use that from now
186 MetaBaseSkel._skelCache[cls.kindName] = cls
187 else: # They seem to be from the same Package - raise as something is messed up
188 raise ValueError(f"Duplicate definition for {cls.kindName} in {relNewFileName} and {relOldFileName}")
190 # Ensure that all skeletons are defined in folders listed in conf.skeleton_search_path
191 if ( 191 ↛ 195line 191 didn't jump to line 195 because the condition on line 191 was never true
192 not any([relNewFileName.startswith(path) for path in conf.skeleton_search_path])
193 and "viur_doc_build" not in dir(sys) # do not check during documentation build
194 ):
195 raise NotImplementedError(
196 f"""{relNewFileName} must be defined in a folder listed in {conf.skeleton_search_path}""")
198 if cls.kindName and cls.kindName is not _UNDEFINED_KINDNAME:
199 MetaBaseSkel._skelCache[cls.kindName] = cls
201 # Auto-Add ViUR Search Tags Adapter if the skeleton has no adapter attached
202 if cls.database_adapters is _UNDEFINED_KINDNAME:
203 cls.database_adapters = ViurTagsSearchAdapter()
205 # Always ensure that skel.database_adapters is an iterable
206 cls.database_adapters = utils.ensure_iterable(cls.database_adapters)