Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/select.py: 23%

67 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-03 12:27 +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 

8 

9if t.TYPE_CHECKING: 9 ↛ 10line 9 didn't jump to line 10 because the condition on line 9 was never true

10 from viur.core.skeleton import SkeletonInstance 

11 

12SelectBoneValue = t.Union[str, Number, enum.Enum] 

13""" 

14Type alias of possible values in a SelectBone. SelectBoneValue can be either a string (str) or a number (Number) 

15""" 

16 

17SelectBoneMultiple = list[SelectBoneValue] 

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

19 

20 

21def translation_key_prefix_skeleton_bonename(bones_instance: BaseBone) -> str: 

22 """Generate a translation key prefix based on the skeleton and bone name""" 

23 return f'skeleton.{bones_instance.skel_cls.__name__.lower().removesuffix("skel")}.{bones_instance.name}.' 

24 

25 

26def translation_key_prefix_bonename(bones_instance: BaseBone) -> str: 

27 """Generate a translation key prefix based on the bone name""" 

28 return f'bone.{bones_instance.name}.' 

29 

30 

31class SelectBone(BaseBone): 

32 """ 

33 A SelectBone is a bone which can take a value from a certain list of values. 

34 Inherits from the BaseBone class. The `type` attribute is set to "select". 

35 """ 

36 type = "select" 

37 

38 def __init__( 

39 self, 

40 *, 

41 defaultValue: t.Union[ 

42 SelectBoneValue, 

43 SelectBoneMultiple, 

44 t.Dict[str, t.Union[SelectBoneMultiple, SelectBoneValue]], 

45 t.Callable[["SkeletonInstance", t.Self], t.Any], 

46 ] = None, 

47 values: dict | list | tuple | t.Callable | enum.EnumMeta = (), 

48 translation_key_prefix: str | t.Callable[[t.Self], str] = "", 

49 add_missing_translations: bool = False, 

50 **kwargs 

51 ): 

52 """ 

53 Initializes a new SelectBone. 

54 

55 :param defaultValue: key(s) of the values which will be checked by default. 

56 :param values: dict of key->value pairs from which the user can choose from 

57 -- or a callable that returns a dict. 

58 :param translation_key_prefix: A prefix for the key of the translation object. 

59 It is empty by default, so that only the label (dict value) from the values is used. 

60 A static string or dynamic method can be used (like `translation_key_prefix_bonename`). 

61 :param kwargs: Additional keyword arguments that will be passed to the superclass' __init__ method. 

62 """ 

63 super().__init__(defaultValue=defaultValue, **kwargs) 

64 self.translation_key_prefix = translation_key_prefix 

65 self.add_missing_translations = add_missing_translations 

66 

67 # handle list/tuple as dicts 

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

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

70 

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

72 self._values = values 

73 

74 def __getattribute__(self, item): 

75 """ 

76 Overrides the default __getattribute__ method to handle the 'values' attribute dynamically. If the '_values' 

77 attribute is callable, it will be called and the result will be stored in the 'values' attribute. 

78 

79 :param str item: The attribute name. 

80 :return: The value of the specified attribute. 

81 

82 :raises AssertionError: If the resulting values are not of type dict or OrderedDict. 

83 """ 

84 if item == "values": 

85 values = self._values 

86 if isinstance(values, enum.EnumMeta): 

87 values = {value.value: value.name for value in values} 

88 elif callable(values): 

89 values = values() 

90 

91 # handle list/tuple as dicts 

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

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

94 

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

96 

97 prefix = self.translation_key_prefix 

98 if callable(prefix): 

99 prefix = prefix(self) 

100 

101 values = { 

102 key: label if isinstance(label, translate) else translate( 

103 f"{prefix}{key}", str(label), 

104 f"value {key} for {self.name}<{type(self).__name__}> " 

105 + f"in {self.skel_cls.__name__} in {self.skel_cls}", 

106 add_missing=self.add_missing_translations, 

107 ) 

108 for key, label in values.items() 

109 } 

110 

111 return values 

112 

113 return super().__getattribute__(item) 

114 

115 def singleValueUnserialize(self, val): 

116 if isinstance(self._values, enum.EnumMeta): 

117 for value in self._values: 

118 if value.value == val: 

119 return value 

120 return val 

121 

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

123 if isinstance(self._values, enum.EnumMeta) and isinstance(val, self._values): 

124 return val.value 

125 return val 

126 

127 def singleValueFromClient(self, value, skel, bone_name, client_data): 

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

129 return value, None 

130 

131 value = str(value) 

132 if not value: 

133 return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Empty, "No value selected")] 

134 

135 for key in self.values.keys(): 

136 if str(key) == value: 

137 if isinstance(self._values, enum.EnumMeta): 

138 return self._values(key), None 

139 

140 return key, None 

141 

142 return self.getEmptyValue(), [ 

143 ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Invalid value selected") 

144 ] 

145 

146 def structure(self) -> dict: 

147 return super().structure() | { 

148 "values": 

149 {k: str(v) for k, v in self.values.items()} # new-style dict 

150 if "bone.select.structure.values.keytuple" not in conf.compatibility 

151 else [(k, str(v)) for k, v in self.values.items()] # old-style key-tuple 

152 }