""".. The Color API"""from__future__importannotations__all__=("Color",)importrefromtyping_extensionsimportNamedTuple,Selffrom.utilsimportarg_value_error_range# Unfortunately, `re` doesn't support repeated capturing groups; only the last match# is captured. So, we'd have to repeat manually.# Allows mixture of letter case to simplify parsing.XX="[0-9a-f]{2}"_RGBA_HEX_RE=re.compile(rf"#?({XX})({XX})({XX})({XX})?",re.A|re.I)delXX# To bypass `NamedTuple`'s `__new__()` override limitationclass_DummyColor(NamedTuple):r:intg:intb:inta:int=255
[docs]classColor(_DummyColor):"""A color. Args: r: The red channel. g: The green channel. b: The blue channel. a: The alpha channel (opacity). Raises: ValueError: The value of a channel is not within the valid range. NOTE: The valid value range for all channels is 0 to 255, both inclusive. TIP: This class is a :py:class:`~typing.NamedTuple` of four fields. WARNING: In the case of multiple inheritance i.e if subclassing this class along with other classes, this class should appear last (i.e to the far right) in the base class list. """__slots__=()# Overrides these descriptors in order to speed up attribute resolution and to# simplify auto documentation.r:int=_DummyColor.rr.__doc__="""The red channel"""g:int=_DummyColor.gg.__doc__="""The green channel"""b:int=_DummyColor.bb.__doc__="""The blue channel"""a:int=_DummyColor.aa.__doc__="""The alpha channel (opacity)"""def__new__(cls,r:int,g:int,b:int,a:int=255)->Self:# Only the 8 LSb may be set for any value within the range [0, 255].# `x & ~255` unsets the 8 LSb. Hence, if the result is non-zero (i.e any# of the bits above the lowest 8 is set), it implies `x` is out of range.## Actually benchmarked *this* against the "simpler" logically-negated chained# comparison (i.e `0 <= x <= 255`) and *this* was significantly faster for all# cases i.e within, on the boundaries, and outside (on both sides).if(r|g|b|a)&~255:# First test to see if *any* is out of rangeifr&~255:raisearg_value_error_range("r",r)ifg&~255:raisearg_value_error_range("g",g)ifb&~255:raisearg_value_error_range("b",b)# There's no point checking since at least one is out of rangeraisearg_value_error_range("a",a)# Using `tuple` directly instead of `super()` for performancereturntuple.__new__(cls,(r,g,b,a))@propertydefhex(self)->str:"""Converts the color to its RGBA hexadecimal representation. Returns: The RGBA hex color string, starting with the ``#`` character i.e ``#rrggbbaa``. Each channel is represented by two **lowercase** hex digits ranging from ``00`` to ``ff``. """return"#%02x%02x%02x%02x"%self@propertydefrgb(self)->tuple[int,int,int]:"""Extracts the R, G and B channels of the color. Returns: A 3-tuple containing the red, green and blue channel values. """returnself[:3]@propertydefrgb_hex(self)->str:"""Converts the color to its RGB hexadecimal representation. Returns: The RGB hex color string, starting with the ``#`` character i.e ``#rrggbb``. Each channel is represented by two **lowercase** hex digits ranging from ``00`` to ``ff``. """return"#%02x%02x%02x"%self[:3]
[docs]@classmethoddeffrom_hex(cls,color:str)->Self:"""Creates a new instance from a hexadecimal color string. Args: color: A **case-insensitive** RGB or RGBA hex color string, **optionally** starting with the ``#`` (pound) character i.e ``[#]rrggbb[aa]``. Returns: A new instance representing the given hex color string. Raises: ValueError: Invalid hex color string. NOTE: For an RGB hex color string, the value of A (the alpha channel) is taken to be 255. """ifnot(match:=_RGBA_HEX_RE.fullmatch(color)):raiseValueError(f"Invalid hex color string (got: {color!r})")returntuple.__new__(cls,[int(x,16)forxinmatch.groups("ff")])
@classmethoddef_new(cls,r:int,g:int,b:int,a:int=255)->Self:"""Alternate constructor for internal use only."""returntuple.__new__(cls,(r,g,b,a))