Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/select.py: 86%
77 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 enum
2import typing as t
3from collections import OrderedDict
4from numbers import Number
5from viur.core.config import conf
6from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity
7from viur.core.i18n import translate
8from viur.core.utils.string import normalize_ascii
10if t.TYPE_CHECKING: 10 ↛ 11line 10 didn't jump to line 11 because the condition on line 10 was never true
11 from viur.core.skeleton import SkeletonInstance
13SelectBoneValue = t.Union[str, Number, enum.Enum]
14"""
15Type alias of possible values in a SelectBone. SelectBoneValue can be either a string (str) or a number (Number)
16"""
18SelectBoneMultiple = list[SelectBoneValue]
19"""Type alias for values of a multiple SelectBone."""
22def translation_key_prefix_skeleton_bonename(bones_instance: BaseBone) -> str:
23 """Generate a translation key prefix based on the skeleton and bone name"""
24 return f'skeleton.{bones_instance.skel_cls.__name__.lower().removesuffix("skel")}.{bones_instance.name}.'
27def translation_key_prefix_bonename(bones_instance: BaseBone) -> str:
28 """Generate a translation key prefix based on the bone name"""
29 return f'bone.{bones_instance.name}.'
32class SelectBone(BaseBone):
33 """
34 A SelectBone is a bone which can take a value from a certain list of values.
35 Inherits from the BaseBone class. The `type` attribute is set to "select".
36 """
37 type = "select"
39 @staticmethod
40 def sort_by_label(item: tuple[SelectBoneValue, str]) -> str:
41 """Sort key for `SelectBone(sort=...)`: by the label, accented letters like their base letter"""
42 return normalize_ascii(item[1]).casefold()
44 def __init__(
45 self,
46 *,
47 defaultValue: t.Union[
48 SelectBoneValue,
49 SelectBoneMultiple,
50 t.Dict[str, t.Union[SelectBoneMultiple, SelectBoneValue]],
51 t.Callable[["SkeletonInstance", t.Self], t.Any],
52 ] = None,
53 values: dict | list | tuple | t.Callable | enum.EnumMeta = (),
54 translation_key_prefix: str | t.Callable[[t.Self], str] = "",
55 add_missing_translations: bool = False,
56 sort: t.Callable[[tuple[SelectBoneValue, str]], t.Any] | None = None,
57 **kwargs
58 ):
59 """
60 Initializes a new SelectBone.
62 :param defaultValue: key(s) of the values which will be checked by default.
63 :param values: dict of key->value pairs from which the user can choose from
64 -- or a callable that returns a dict.
65 :param translation_key_prefix: A prefix for the key of the translation object.
66 It is empty by default, so that only the label (dict value) from the values is used.
67 A static string or dynamic method can be used (like `translation_key_prefix_bonename`).
68 :param sort: Sort key function for the values in the structure, called with a (key, label) tuple
69 after the labels are translated into the request language (like `SelectBone.sort_by_label`).
70 By default, the values keep their given order.
71 :param kwargs: Additional keyword arguments that will be passed to the superclass' __init__ method.
72 """
73 super().__init__(defaultValue=defaultValue, **kwargs)
74 self.translation_key_prefix = translation_key_prefix
75 self.add_missing_translations = add_missing_translations
76 self.sort = sort
78 # handle list/tuple as dicts
79 if isinstance(values, (list, tuple)):
80 values = {value: value for value in values}
82 assert isinstance(values, (dict, OrderedDict)) or callable(values)
83 self._values = values
85 def __getattribute__(self, item):
86 """
87 Overrides the default __getattribute__ method to handle the 'values' attribute dynamically. If the '_values'
88 attribute is callable, it will be called and the result will be stored in the 'values' attribute.
90 :param str item: The attribute name.
91 :return: The value of the specified attribute.
93 :raises AssertionError: If the resulting values are not of type dict or OrderedDict.
94 """
95 if item == "values":
96 values = self._values
97 if isinstance(values, enum.EnumMeta):
98 values = {value.value: value.name for value in values}
99 elif callable(values):
100 values = values()
102 # handle list/tuple as dicts
103 if isinstance(values, (list, tuple)): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 values = {value: value for value in values}
106 assert isinstance(values, (dict, OrderedDict))
108 prefix = self.translation_key_prefix
109 if callable(prefix): 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 prefix = prefix(self)
112 values = {
113 key: label if isinstance(label, translate) else translate(
114 f"{prefix}{key}", str(label),
115 f"value {key} for {self.name}<{type(self).__name__}> "
116 + f"in {self.skel_cls.__name__} in {self.skel_cls}",
117 add_missing=self.add_missing_translations,
118 )
119 for key, label in values.items()
120 }
122 return values
124 return super().__getattribute__(item)
126 def singleValueUnserialize(self, val):
127 if isinstance(self._values, enum.EnumMeta): 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 for value in self._values:
129 if value.value == val:
130 return value
131 return val
133 def singleValueSerialize(self, val, skel: 'SkeletonInstance', name: str, parentIndexed: bool):
134 return self._atomic_dump(val)
136 def singleValueFromClient(self, value, skel, bone_name, client_data):
137 if isinstance(self._values, enum.EnumMeta) and isinstance(value, self._values):
138 return value, None
140 value = str(value)
141 if not value:
142 return self.getEmptyValue(), [
143 ReadFromClientError(
144 ReadFromClientErrorSeverity.Empty,
145 translate("core.bones.error.nothingselected", "No value selected"),
146 )
147 ]
149 for key in self.values.keys():
150 if str(key) == value:
151 if isinstance(self._values, enum.EnumMeta):
152 return self._values(key), None
154 return key, None
156 return self.getEmptyValue(), [
157 ReadFromClientError(
158 ReadFromClientErrorSeverity.Invalid,
159 translate("core.bones.error.invalidselected", "Invalid value selected"),
160 )
161 ]
163 def structure(self) -> dict:
164 values = [(k, str(v)) for k, v in self.values.items()]
165 if self.sort:
166 values.sort(key=self.sort)
168 return super().structure() | {
169 "values":
170 dict(values) # new-style dict
171 if "bone.select.structure.values.keytuple" not in conf.compatibility
172 else values # old-style key-tuple
173 }
175 def _atomic_dump(self, value):
176 if isinstance(self._values, enum.EnumMeta) and isinstance(value, self._values):
177 return value.value
179 return value