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
« 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
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.
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"
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 }
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
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 "#".
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)]
42 if not isinstance(value, str):
43 return invalid()
45 value = value.lower()
47 if value.startswith("#"): # strip the optional leading "#"
48 value = value[1:]
50 if any(char not in string.hexdigits for char in value):
51 return invalid()
53 if len(value) not in self.VALID_LENGTHS[self.mode]:
54 return invalid()
56 if len(value) == 3: # expand the shorthand: "abc" --> "aabbcc"
57 value = "".join(char * 2 for char in value)
59 value = f"#{value}"
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
65 return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err)]
67 def structure(self) -> dict:
68 return super().structure() | {
69 "mode": self.mode,
70 }