Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/color.py: 96%

33 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-25 15:02 +0000

1import string 

2import typing as t 

3from viur.core import i18n 

4from .base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity 

5 

6 

7class ColorBone(BaseBone): 

8 r""" 

9 ColorBone is a custom bone class for storing color values in the ViUR framework. 

10 It inherits from the BaseBone class in the viur.core.bones.base module. 

11 

12 :param type: A string representing the bone type, set to "color". 

13 :param mode: A string specifying the color mode, either "rgb" or "rgba". Default is "rgb". 

14 :param \**kwargs: Additional keyword arguments passed to the BaseBone constructor. 

15 """ 

16 type = "color" 

17 

18 # Accepted number of hex digits (without the leading "#") per mode 

19 VALID_LENGTHS: t.Final[dict[str, tuple[int, ...]]] = { 

20 "rgb": (3, 6), 

21 "rgba": (8,), 

22 } 

23 

24 def __init__(self, *, mode="rgb", **kwargs): # mode rgb/rgba 

25 super().__init__(**kwargs) 

26 if mode not in self.VALID_LENGTHS: 

27 raise ValueError(f"{mode=} is not in {self.VALID_LENGTHS!r}") 

28 self.mode = mode 

29 

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

31 """ 

32 Normalize a hex color to a lower-case value with exactly one leading "#". 

33 

34 A leading "#" is optional in the input, but it's the only position a "#" may 

35 appear in. The remaining characters must be hex digits in a length valid for 

36 the bone's mode; the 3-digit shorthand is expanded. Everything else is 

37 rejected as invalid. 

38 """ 

39 def invalid(): 

40 return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid)] 

41 

42 if not isinstance(value, str): 

43 return invalid() 

44 

45 value = value.lower() 

46 

47 if value.startswith("#"): # strip the optional leading "#" 

48 value = value[1:] 

49 

50 if any(char not in string.hexdigits for char in value): 

51 return invalid() 

52 

53 if len(value) not in self.VALID_LENGTHS[self.mode]: 

54 return invalid() 

55 

56 if len(value) == 3: # expand the shorthand: "abc" --> "aabbcc" 

57 value = "".join(char * 2 for char in value) 

58 

59 value = f"#{value}" 

60 

61 err = self.isInvalid(value) 

62 if not err: 62 ↛ 65line 62 didn't jump to line 65 because the condition on line 62 was always true

63 return value, None 

64 

65 return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err)] 

66 

67 def structure(self) -> dict: 

68 return super().structure() | { 

69 "mode": self.mode, 

70 }