Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/skeleton/meta.py: 71%

80 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-27 10:50 +0000

1import inspect 

2import logging # noqa 

3import os 

4import string 

5import sys 

6import typing as t 

7from pathlib import Path 

8 

9from .adapter import ViurTagsSearchAdapter 

10from .. import utils 

11from ..bones.base import BaseBone, getSystemInitialized 

12from ..config import conf 

13 

14_UNDEFINED_KINDNAME = object() 

15ABSTRACT_SKEL_CLS_SUFFIX = "AbstractSkel" 

16 

17 

18def relative_skeleton_path(filename: str | os.PathLike) -> str: 

19 """ 

20 Return the path of a skeleton's file as it is matched against :attr:`conf.skeleton_search_path`. 

21 

22 The path is made relative to ``conf.instance.core_base_path`` first, and only then to 

23 ``conf.instance.project_base_path``. The core may be located below the working directory 

24 (an editable installation, a submodule, a checkout running its test suite), and its 

25 skeletons must resolve to ``/viur/core/...`` regardless of where the process was started. 

26 

27 :param filename: Absolute path of the file defining the skeleton. 

28 :return: The path relative to the core or the project with a leading ``/``, 

29 or the unchanged path if it is located in neither of them. 

30 """ 

31 path = Path(filename) 

32 for base_path in (conf.instance.core_base_path, conf.instance.project_base_path): 

33 if path.is_relative_to(base_path): 

34 return f"/{path.relative_to(base_path).as_posix()}" 

35 return str(path) 

36 

37 

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

39"""TypeVar for generic skeleton typing. 

40 

41Use this to annotate functions and classes that work with a specific, but not yet known, 

42Skeleton subclass. The type checker then knows which concrete Skeleton is in use and can 

43validate bone access. 

44 

45Example — typed helper function:: 

46 

47 from viur.core.skeleton import Skeleton_Cls, SkeletonInstance 

48 

49 def clone_and_set_owner(skel: SkeletonInstance[Skeleton_Cls], owner: str) -> SkeletonInstance[Skeleton_Cls]: 

50 cloned = skel.clone() 

51 cloned["owner"] = owner 

52 return cloned 

53 

54Example — typed module override:: 

55 

56 class ProductModule(List): 

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

58 skel = super().editSkel() 

59 skel.price.readOnly = True 

60 return skel 

61 

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

63automatically narrows to the calling class:: 

64 

65 class BaseSkeleton: 

66 @classmethod 

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

68 

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

70 ProductSkel.fromClient(skel, request.POST) 

71""" 

72 

73 

74class MetaBaseSkel(type): 

75 """ 

76 This is the metaclass for Skeletons. 

77 It is used to enforce several restrictions on bone names, etc. 

78 """ 

79 _skelCache = {} # Mapping kindName -> SkelCls 

80 _allSkelClasses = set() # list of all known skeleton classes (including Ref and Mail-Skels) 

81 

82 # List of reserved keywords and function names 

83 __reserved_keywords = { 

84 "all", 

85 "bounce", 

86 "clone", 

87 "cursor", 

88 "delete", 

89 "errors", 

90 "fromClient", 

91 "fromDB", 

92 "get", 

93 "getCurrentSEOKeys", 

94 "items", 

95 "keys", 

96 "limit", 

97 "orderby", 

98 "orderdir", 

99 "patch", 

100 "postDeletedHandler", 

101 "postSavedHandler", 

102 "preProcessBlobLocks", 

103 "preProcessSerializedData", 

104 "read", 

105 "readonly", 

106 "refresh", 

107 "self", 

108 "serialize", 

109 "setBoneValue", 

110 "structure", 

111 "style", 

112 "toDB", 

113 "unserialize", 

114 "values", 

115 "write", 

116 } 

117 

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

119 

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

121 cls.__boneMap__ = MetaBaseSkel.generate_bonemap(cls) 

122 

123 if not getSystemInitialized() and not cls.__name__.endswith(ABSTRACT_SKEL_CLS_SUFFIX): 123 ↛ 126line 123 didn't jump to line 126 because the condition on line 123 was always true

124 MetaBaseSkel._allSkelClasses.add(cls) 

125 

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

127 

128 @staticmethod 

129 def generate_bonemap(cls): 

130 """ 

131 Recursively constructs a dict of bones from 

132 """ 

133 map = {} 

134 

135 for base in cls.__bases__: 

136 if "__viurBaseSkeletonMarker__" in dir(base): 

137 map |= MetaBaseSkel.generate_bonemap(base) 

138 

139 for key in cls.__dict__: 

140 prop = getattr(cls, key) 

141 

142 if isinstance(prop, BaseBone): 

143 if not all([c in MetaBaseSkel.__allowed_chars for c in key]): 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true

144 raise AttributeError(f"Invalid bone name: {key!r} contains invalid characters") 

145 elif key in MetaBaseSkel.__reserved_keywords: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true

146 raise AttributeError(f"Invalid bone name: {key!r} is reserved and cannot be used") 

147 

148 map[key] = prop 

149 

150 elif prop is None and key in map: # Allow removing a bone in a subclass by setting it to None 

151 del map[key] 

152 

153 return map 

154 

155 def __setattr__(self, key, value): 

156 super().__setattr__(key, value) 

157 if isinstance(value, BaseBone): 157 ↛ 159line 157 didn't jump to line 159 because the condition on line 157 was never true

158 # Call BaseBone.__set_name__ manually for bones that are assigned at runtime 

159 value.__set_name__(self, key) 

160 

161 

162class MetaSkel(MetaBaseSkel): 

163 

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

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

166 

167 relNewFileName = relative_skeleton_path(inspect.getfile(cls)) 

168 

169 # Check if we have an abstract skeleton 

170 if cls.__name__.endswith(ABSTRACT_SKEL_CLS_SUFFIX): 170 ↛ 172line 170 didn't jump to line 172 because the condition on line 170 was never true

171 # Ensure that it doesn't have a kindName 

172 assert cls.kindName is _UNDEFINED_KINDNAME or cls.kindName is None, \ 

173 "Abstract Skeletons can't have a kindName" 

174 # Prevent any further processing by this class; it has to be sub-classed before it can be used 

175 return 

176 

177 # Automatic determination of the kindName, if the class is not part of viur.core. 

178 if ( 178 ↛ 183line 178 didn't jump to line 183 because the condition on line 178 was never true

179 cls.kindName is _UNDEFINED_KINDNAME 

180 and not relNewFileName.strip(os.path.sep).startswith("viur") 

181 and "viur_doc_build" not in dir(sys) # do not check during documentation build 

182 ): 

183 if cls.__name__.endswith("Skel"): 

184 cls.kindName = cls.__name__.lower()[:-4] 

185 else: 

186 cls.kindName = cls.__name__.lower() 

187 

188 # Try to determine which skeleton definition takes precedence 

189 if cls.kindName and cls.kindName is not _UNDEFINED_KINDNAME and cls.kindName in MetaBaseSkel._skelCache: 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true

190 relOldFileName = relative_skeleton_path(inspect.getfile(MetaBaseSkel._skelCache[cls.kindName])) 

191 idxOld = min( 

192 [x for (x, y) in enumerate(conf.skeleton_search_path) if relOldFileName.startswith(y)] + [999]) 

193 idxNew = min( 

194 [x for (x, y) in enumerate(conf.skeleton_search_path) if relNewFileName.startswith(y)] + [999]) 

195 if idxNew == 999: 

196 # We could not determine a priority for this class as its from a path not listed in the config 

197 raise NotImplementedError( 

198 "Skeletons must be defined in a folder listed in conf.skeleton_search_path") 

199 elif idxOld < idxNew: # Lower index takes precedence 

200 # The currently processed skeleton has a lower priority than the one we already saw - just ignore it 

201 return 

202 elif idxOld > idxNew: 

203 # The currently processed skeleton has a higher priority, use that from now 

204 MetaBaseSkel._skelCache[cls.kindName] = cls 

205 else: # They seem to be from the same Package - raise as something is messed up 

206 raise ValueError(f"Duplicate definition for {cls.kindName} in {relNewFileName} and {relOldFileName}") 

207 

208 # Ensure that all skeletons are defined in folders listed in conf.skeleton_search_path 

209 if ( 209 ↛ 213line 209 didn't jump to line 213 because the condition on line 209 was never true

210 not any([relNewFileName.startswith(path) for path in conf.skeleton_search_path]) 

211 and "viur_doc_build" not in dir(sys) # do not check during documentation build 

212 ): 

213 raise NotImplementedError( 

214 f"""{relNewFileName} must be defined in a folder listed in {conf.skeleton_search_path}""") 

215 

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

217 MetaBaseSkel._skelCache[cls.kindName] = cls 

218 

219 # Auto-Add ViUR Search Tags Adapter if the skeleton has no adapter attached 

220 if cls.database_adapters is _UNDEFINED_KINDNAME: 

221 cls.database_adapters = ViurTagsSearchAdapter() 

222 

223 # Always ensure that skel.database_adapters is an iterable 

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