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

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 

9 

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 

12 

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

17 

18SelectBoneMultiple = list[SelectBoneValue] 

19"""Type alias for values of a multiple SelectBone.""" 

20 

21 

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}.' 

25 

26 

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}.' 

30 

31 

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" 

38 

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

43 

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. 

61 

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 

77 

78 # handle list/tuple as dicts 

79 if isinstance(values, (list, tuple)): 

80 values = {value: value for value in values} 

81 

82 assert isinstance(values, (dict, OrderedDict)) or callable(values) 

83 self._values = values 

84 

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. 

89 

90 :param str item: The attribute name. 

91 :return: The value of the specified attribute. 

92 

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

101 

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} 

105 

106 assert isinstance(values, (dict, OrderedDict)) 

107 

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) 

111 

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 } 

121 

122 return values 

123 

124 return super().__getattribute__(item) 

125 

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 

132 

133 def singleValueSerialize(self, val, skel: 'SkeletonInstance', name: str, parentIndexed: bool): 

134 return self._atomic_dump(val) 

135 

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 

139 

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 ] 

148 

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 

153 

154 return key, None 

155 

156 return self.getEmptyValue(), [ 

157 ReadFromClientError( 

158 ReadFromClientErrorSeverity.Invalid, 

159 translate("core.bones.error.invalidselected", "Invalid value selected"), 

160 ) 

161 ] 

162 

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) 

167 

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 } 

174 

175 def _atomic_dump(self, value): 

176 if isinstance(self._values, enum.EnumMeta) and isinstance(value, self._values): 

177 return value.value 

178 

179 return value