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
« 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
9from .adapter import ViurTagsSearchAdapter
10from .. import utils
11from ..bones.base import BaseBone, getSystemInitialized
12from ..config import conf
14_UNDEFINED_KINDNAME = object()
15ABSTRACT_SKEL_CLS_SUFFIX = "AbstractSkel"
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`.
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.
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)
38Skeleton_Cls = t.TypeVar("Skeleton_Cls", bound="BaseSkeleton")
39"""TypeVar for generic skeleton typing.
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.
45Example — typed helper function::
47 from viur.core.skeleton import Skeleton_Cls, SkeletonInstance
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
54Example — typed module override::
56 class ProductModule(List):
57 def editSkel(self) -> SkeletonInstance[ProductSkel]:
58 skel = super().editSkel()
59 skel.price.readOnly = True
60 return skel
62When calling a classmethod on a concrete Skeleton, use ``t.Self`` instead so the type checker
63automatically narrows to the calling class::
65 class BaseSkeleton:
66 @classmethod
67 def fromClient(cls, skel: SkeletonInstance[t.Self], data: dict) -> bool: ...
69 # Calling on a concrete class: type checker knows skel is SkeletonInstance[ProductSkel]
70 ProductSkel.fromClient(skel, request.POST)
71"""
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)
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 }
118 __allowed_chars = string.ascii_letters + string.digits + "_"
120 def __init__(cls, name, bases, dct, **kwargs):
121 cls.__boneMap__ = MetaBaseSkel.generate_bonemap(cls)
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)
126 super().__init__(name, bases, dct)
128 @staticmethod
129 def generate_bonemap(cls):
130 """
131 Recursively constructs a dict of bones from
132 """
133 map = {}
135 for base in cls.__bases__:
136 if "__viurBaseSkeletonMarker__" in dir(base):
137 map |= MetaBaseSkel.generate_bonemap(base)
139 for key in cls.__dict__:
140 prop = getattr(cls, key)
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")
148 map[key] = prop
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]
153 return map
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)
162class MetaSkel(MetaBaseSkel):
164 def __init__(cls, name, bases, dct, **kwargs):
165 super().__init__(name, bases, dct, **kwargs)
167 relNewFileName = relative_skeleton_path(inspect.getfile(cls))
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
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()
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}")
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}""")
216 if cls.kindName and cls.kindName is not _UNDEFINED_KINDNAME:
217 MetaBaseSkel._skelCache[cls.kindName] = cls
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()
223 # Always ensure that skel.database_adapters is an iterable
224 cls.database_adapters = utils.ensure_iterable(cls.database_adapters)