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

1import inspect 

2import logging # noqa 

3import os 

4import string 

5import sys 

6import typing as t 

7 

8from .adapter import ViurTagsSearchAdapter 

9from .. import utils 

10from ..bones.base import BaseBone, getSystemInitialized 

11from ..config import conf 

12 

13_UNDEFINED_KINDNAME = object() 

14ABSTRACT_SKEL_CLS_SUFFIX = "AbstractSkel" 

15 

16Skeleton_Cls = t.TypeVar("Skeleton_Cls", bound="BaseSkeleton") 

17"""TypeVar for generic skeleton typing. 

18 

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. 

22 

23Example — typed helper function:: 

24 

25 from viur.core.skeleton import Skeleton_Cls, SkeletonInstance 

26 

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 

31 

32Example — typed module override:: 

33 

34 class ProductModule(List): 

35 def editSkel(self) -> SkeletonInstance[ProductSkel]: 

36 skel = super().editSkel() 

37 skel.price.readOnly = True 

38 return skel 

39 

40When calling a classmethod on a concrete Skeleton, use ``t.Self`` instead so the type checker 

41automatically narrows to the calling class:: 

42 

43 class BaseSkeleton: 

44 @classmethod 

45 def fromClient(cls, skel: SkeletonInstance[t.Self], data: dict) -> bool: ... 

46 

47 # Calling on a concrete class: type checker knows skel is SkeletonInstance[ProductSkel] 

48 ProductSkel.fromClient(skel, request.POST) 

49""" 

50 

51 

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) 

59 

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 } 

95 

96 __allowed_chars = string.ascii_letters + string.digits + "_" 

97 

98 def __init__(cls, name, bases, dct, **kwargs): 

99 cls.__boneMap__ = MetaBaseSkel.generate_bonemap(cls) 

100 

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) 

103 

104 super().__init__(name, bases, dct) 

105 

106 @staticmethod 

107 def generate_bonemap(cls): 

108 """ 

109 Recursively constructs a dict of bones from 

110 """ 

111 map = {} 

112 

113 for base in cls.__bases__: 

114 if "__viurBaseSkeletonMarker__" in dir(base): 

115 map |= MetaBaseSkel.generate_bonemap(base) 

116 

117 for key in cls.__dict__: 

118 prop = getattr(cls, key) 

119 

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") 

125 

126 map[key] = prop 

127 

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] 

130 

131 return map 

132 

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) 

138 

139 

140class MetaSkel(MetaBaseSkel): 

141 

142 def __init__(cls, name, bases, dct, **kwargs): 

143 super().__init__(name, bases, dct, **kwargs) 

144 

145 relNewFileName = inspect.getfile(cls) \ 

146 .replace(str(conf.instance.project_base_path), "") \ 

147 .replace(str(conf.instance.core_base_path), "") 

148 

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 

156 

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() 

167 

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}") 

189 

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}""") 

197 

198 if cls.kindName and cls.kindName is not _UNDEFINED_KINDNAME: 

199 MetaBaseSkel._skelCache[cls.kindName] = cls 

200 

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() 

204 

205 # Always ensure that skel.database_adapters is an iterable 

206 cls.database_adapters = utils.ensure_iterable(cls.database_adapters)