Added HEX↔RGBA color utilities and tests (Hacktoberfest 2025) (#1988)

* Added color utility functions: hex_to_rgba, rgba_to_hex, is_valid_hex with tests
* Parametrize hex and RGBA utility tests in `test_utils.py` for improved clarity and coverage
* Enhance hex and RGBA utilities: improve test coverage, validations, and docstrings; refactor shared logic.
* Refactor color input handling with `_normalize_color_input` utility; add hex string support across annotators and enhance test coverage

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
This commit is contained in:
Soham Walam 2026-02-20 14:28:15 +05:30 committed by GitHub
parent 162a46cae1
commit f8f62891de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 267 additions and 51 deletions

View File

@ -31,7 +31,12 @@ from supervision.annotators.core import (
TraceAnnotator,
TriangleAnnotator,
)
from supervision.annotators.utils import ColorLookup
from supervision.annotators.utils import (
ColorLookup,
hex_to_rgba,
is_valid_hex,
rgba_to_hex,
)
from supervision.classification.core import Classifications
from supervision.dataset.core import (
BaseDataset,
@ -228,6 +233,8 @@ __all__ = [
"get_polygon_center",
"get_video_frames_generator",
"grayscale_image",
"hex_to_rgba",
"is_valid_hex",
"letterbox_image",
"list_files_with_extensions",
"mask_iou_batch",
@ -248,6 +255,7 @@ __all__ = [
"polygon_to_xyxy",
"process_video",
"resize_image",
"rgba_to_hex",
"rle_to_mask",
"scale_boxes",
"scale_image",

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from functools import lru_cache
from math import sqrt
from typing import Any
from typing import Any, overload
import cv2
import numpy as np
@ -16,6 +16,7 @@ from supervision.annotators.utils import (
ColorLookup,
Trace,
get_labels_text,
hex_to_rgba,
resolve_color,
resolve_text_background_xyxy,
snap_boxes,
@ -45,6 +46,30 @@ from supervision.utils.image import (
scale_image,
)
@overload
def _normalize_color_input(color: Color | str) -> Color: ...
@overload
def _normalize_color_input(
color: Color | ColorPalette | str,
) -> Color | ColorPalette: ...
def _normalize_color_input(color: Color | ColorPalette | str) -> Color | ColorPalette:
"""Normalize accepted color inputs to internal color objects.
Accepts `Color`, `ColorPalette`, or hex string input. Hex strings are parsed via
`hex_to_rgba` and converted to `Color` (alpha channel is ignored because annotator
drawing uses RGB/BGR colors).
"""
if isinstance(color, str):
r, g, b, _ = hex_to_rgba(color)
return Color.from_rgb_tuple((r, g, b))
return color
CV2_FONT = cv2.FONT_HERSHEY_SIMPLEX
@ -70,9 +95,9 @@ class _BaseLabelAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_color: Color | ColorPalette = Color.WHITE,
text_color: Color | ColorPalette | str = Color.WHITE,
text_padding: int = 10,
text_position: Position = Position.TOP_LEFT,
text_offset: tuple[int, int] = (0, 0),
@ -102,9 +127,9 @@ class _BaseLabelAnnotator(BaseAnnotator):
max_line_length: Maximum number of characters per
line before wrapping the text. None means no wrapping.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.color_lookup: ColorLookup = color_lookup
self.text_color: Color | ColorPalette = text_color
self.text_color: Color | ColorPalette = _normalize_color_input(text_color)
self.text_padding: int = text_padding
self.text_anchor: Position = text_position
self.text_offset: tuple[int, int] = text_offset
@ -162,7 +187,7 @@ class BoxAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
@ -174,7 +199,7 @@ class BoxAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ -249,7 +274,7 @@ class OrientedBoxAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
@ -261,7 +286,7 @@ class OrientedBoxAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ -340,7 +365,7 @@ class MaskAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
opacity: float = 0.5,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
@ -352,7 +377,7 @@ class MaskAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.opacity = opacity
self.color_lookup: ColorLookup = color_lookup
@ -435,7 +460,7 @@ class PolygonAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
@ -447,7 +472,7 @@ class PolygonAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ -526,7 +551,7 @@ class ColorAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
opacity: float = 0.5,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
@ -538,7 +563,7 @@ class ColorAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.color_lookup: ColorLookup = color_lookup
self.opacity = opacity
@ -622,7 +647,7 @@ class HaloAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
opacity: float = 0.8,
kernel_size: int = 40,
color_lookup: ColorLookup = ColorLookup.CLASS,
@ -637,7 +662,7 @@ class HaloAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.opacity = opacity
self.color_lookup: ColorLookup = color_lookup
self.kernel_size: int = kernel_size
@ -724,7 +749,7 @@ class EllipseAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
thickness: int = 2,
start_angle: int = -45,
end_angle: int = 235,
@ -740,7 +765,7 @@ class EllipseAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.thickness: int = thickness
self.start_angle: int = start_angle
self.end_angle: int = end_angle
@ -823,7 +848,7 @@ class BoxCornerAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
thickness: int = 4,
corner_length: int = 15,
color_lookup: ColorLookup = ColorLookup.CLASS,
@ -837,7 +862,7 @@ class BoxCornerAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.thickness: int = thickness
self.corner_length: int = corner_length
self.color_lookup: ColorLookup = color_lookup
@ -918,7 +943,7 @@ class CircleAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
):
@ -931,7 +956,7 @@ class CircleAnnotator(BaseAnnotator):
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
@ -1011,12 +1036,12 @@ class DotAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
radius: int = 4,
position: Position = Position.CENTER,
color_lookup: ColorLookup = ColorLookup.CLASS,
outline_thickness: int = 0,
outline_color: Color | ColorPalette = Color.BLACK,
outline_color: Color | ColorPalette | str = Color.BLACK,
):
"""
Args:
@ -1031,12 +1056,12 @@ class DotAnnotator(BaseAnnotator):
use for outline. It is activated by setting outline_thickness to a value
greater than 0.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.radius: int = radius
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
self.outline_thickness = outline_thickness
self.outline_color: Color | ColorPalette = outline_color
self.outline_color: Color | ColorPalette = _normalize_color_input(outline_color)
@ensure_cv2_image_for_class_method
def annotate(
@ -1121,9 +1146,9 @@ class LabelAnnotator(_BaseLabelAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_color: Color | ColorPalette = Color.WHITE,
text_color: Color | ColorPalette | str = Color.WHITE,
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
@ -1437,9 +1462,9 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
color_lookup: ColorLookup = ColorLookup.CLASS,
text_color: Color | ColorPalette = Color.WHITE,
text_color: Color | ColorPalette | str = Color.WHITE,
font_path: str | None = None,
font_size: int = 10,
text_padding: int = 10,
@ -1885,7 +1910,7 @@ class TraceAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
position: Position = Position.CENTER,
trace_length: int = 30,
thickness: int = 2,
@ -1905,7 +1930,7 @@ class TraceAnnotator(BaseAnnotator):
color_lookup: Strategy for mapping colors to annotations.
Options are `INDEX`, `CLASS`, `TRACK`.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.trace = Trace(max_size=trace_length, anchor=position)
self.thickness = thickness
self.smooth = smooth
@ -2190,13 +2215,13 @@ class TriangleAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
base: int = 10,
height: int = 10,
position: Position = Position.TOP_CENTER,
color_lookup: ColorLookup = ColorLookup.CLASS,
outline_thickness: int = 0,
outline_color: Color | ColorPalette = Color.BLACK,
outline_color: Color | ColorPalette | str = Color.BLACK,
):
"""
Args:
@ -2212,13 +2237,13 @@ class TriangleAnnotator(BaseAnnotator):
use for outline. It is activated by setting outline_thickness to a value
greater than 0.
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.base: int = base
self.height: int = height
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
self.outline_thickness: int = outline_thickness
self.outline_color: Color | ColorPalette = outline_color
self.outline_color: Color | ColorPalette = _normalize_color_input(outline_color)
@ensure_cv2_image_for_class_method
def annotate(
@ -2312,7 +2337,7 @@ class RoundBoxAnnotator(BaseAnnotator):
def __init__(
self,
color: Color | ColorPalette = ColorPalette.DEFAULT,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
thickness: int = 2,
color_lookup: ColorLookup = ColorLookup.CLASS,
roundness: float = 0.6,
@ -2329,7 +2354,7 @@ class RoundBoxAnnotator(BaseAnnotator):
By default roundness percent is calculated based on smaller side
length (width or height).
"""
self.color: Color | ColorPalette = color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.thickness: int = thickness
self.color_lookup: ColorLookup = color_lookup
if not 0 < roundness <= 1.0:
@ -2449,8 +2474,8 @@ class PercentageBarAnnotator(BaseAnnotator):
self,
height: int = 16,
width: int = 80,
color: Color | ColorPalette = ColorPalette.DEFAULT,
border_color: Color = Color.BLACK,
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
border_color: Color | str = Color.BLACK,
position: Position = Position.TOP_CENTER,
color_lookup: ColorLookup = ColorLookup.CLASS,
border_thickness: int | None = None,
@ -2469,8 +2494,8 @@ class PercentageBarAnnotator(BaseAnnotator):
"""
self.height: int = height
self.width: int = width
self.color: Color | ColorPalette = color
self.border_color: Color = border_color
self.color: Color | ColorPalette = _normalize_color_input(color)
self.border_color: Color = _normalize_color_input(border_color)
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
@ -2644,7 +2669,7 @@ class CropAnnotator(BaseAnnotator):
self,
position: Position = Position.TOP_CENTER,
scale_factor: float = 2.0,
border_color: Color | ColorPalette = ColorPalette.DEFAULT,
border_color: Color | ColorPalette | str = ColorPalette.DEFAULT,
border_thickness: int = 2,
border_color_lookup: ColorLookup = ColorLookup.CLASS,
):
@ -2663,7 +2688,7 @@ class CropAnnotator(BaseAnnotator):
"""
self.position: Position = position
self.scale_factor: float = scale_factor
self.border_color: Color | ColorPalette = border_color
self.border_color: Color | ColorPalette = _normalize_color_input(border_color)
self.border_thickness: int = border_thickness
self.border_color_lookup: ColorLookup = border_color_lookup

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import re
import textwrap
from enum import Enum
from typing import Any
@ -364,7 +365,67 @@ class Trace:
self.current_frame_id += 1
def get(self, tracker_id: int) -> np.ndarray[Any, np.dtype[np.float32]]:
result: np.ndarray[Any, np.dtype[np.float32]] = self.xy[
self.tracker_id == tracker_id
].copy()
return result
filtered: np.ndarray[Any, np.dtype[np.float32]] = (
self.xy[self.tracker_id == tracker_id].copy().astype(np.float32, copy=False)
)
return filtered
def hex_to_rgba(hex_color: str) -> tuple[int, int, int, int]:
"""
Converts a hex color string (e.g. "#FF00FF" or "#FF00FF80") to an RGBA tuple.
Args:
hex_color (str): A hex color string.
Returns:
tuple[int, int, int, int]: RGBA values in range 0-255.
Raises:
ValueError: If the format is invalid.
"""
hex_color = hex_color.strip().lstrip("#")
if len(hex_color) == 6:
hex_color += "FF" # default full opacity
if len(hex_color) != 8:
raise ValueError(f"Invalid hex color format: {hex_color}")
try:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
a = int(hex_color[6:8], 16)
except ValueError as exc:
raise ValueError(f"Invalid hex digits in {hex_color}") from exc
return (r, g, b, a)
def rgba_to_hex(rgba: tuple[int, int, int, int]) -> str:
"""
Converts an RGBA tuple (0-255 each) to a hex color string.
Args:
rgba: RGBA values in range 0-255.
Returns:
Hex color string in the format "#RRGGBBAA".
Raises:
ValueError: If `rgba` is not a 4-tuple or contains values outside 0-255.
"""
if len(rgba) != 4 or not all(0 <= c <= 255 for c in rgba):
raise ValueError("RGBA must be a 4-tuple with values between 0-255.")
return "#{:02X}{:02X}{:02X}{:02X}".format(*rgba)
def is_valid_hex(hex_color: str) -> bool:
"""
Checks if a given string is a valid hex color.
Args:
hex_color: A hex color string with an optional leading "#". Supports
6-digit (RGB) or 8-digit (RGBA) formats.
Returns:
True if the string is a valid 6- or 8-digit hex color, otherwise False.
"""
return bool(re.fullmatch(r"#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?", hex_color.strip()))

View File

@ -25,6 +25,7 @@ from supervision.annotators.core import (
PolygonAnnotator,
RichLabelAnnotator,
RoundBoxAnnotator,
TraceAnnotator,
TriangleAnnotator,
)
from supervision.annotators.utils import ColorLookup
@ -58,6 +59,53 @@ def gradient_image() -> np.ndarray:
return image
@pytest.mark.parametrize(
("factory", "expected_colors"),
[
(lambda: BoxAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: OrientedBoxAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: MaskAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: PolygonAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: ColorAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: HaloAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: EllipseAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: BoxCornerAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(lambda: CircleAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(
lambda: DotAnnotator(color="#010203", outline_color="#040506"),
{"color": (1, 2, 3), "outline_color": (4, 5, 6)},
),
(
lambda: LabelAnnotator(color="#010203", text_color="#040506"),
{"color": (1, 2, 3), "text_color": (4, 5, 6)},
),
(
lambda: RichLabelAnnotator(color="#010203", text_color="#040506"),
{"color": (1, 2, 3), "text_color": (4, 5, 6)},
),
(lambda: TraceAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(
lambda: TriangleAnnotator(color="#010203", outline_color="#040506"),
{"color": (1, 2, 3), "outline_color": (4, 5, 6)},
),
(lambda: RoundBoxAnnotator(color="#010203"), {"color": (1, 2, 3)}),
(
lambda: PercentageBarAnnotator(color="#010203", border_color="#040506"),
{"color": (1, 2, 3), "border_color": (4, 5, 6)},
),
(lambda: CropAnnotator(border_color="#010203"), {"border_color": (1, 2, 3)}),
],
)
def test_hex_color_support_across_annotators(
factory, expected_colors: dict[str, tuple[int, int, int]]
) -> None:
annotator = factory()
for attribute_name, expected_rgb in expected_colors.items():
color = getattr(annotator, attribute_name)
assert isinstance(color, Color)
assert color.as_rgb() == expected_rgb
class TestBoxAnnotator:
"""
Verify that BoxAnnotator correctly draws bounding boxes on an image.

View File

@ -5,7 +5,14 @@ from contextlib import ExitStack as DoesNotRaise
import numpy as np
import pytest
from supervision.annotators.utils import ColorLookup, resolve_color_idx, wrap_text
from supervision.annotators.utils import (
ColorLookup,
hex_to_rgba,
is_valid_hex,
resolve_color_idx,
rgba_to_hex,
wrap_text,
)
from supervision.detection.core import Detections
from tests.helpers import _create_detections
@ -172,3 +179,70 @@ def test_wrap_text(
with exception:
result = wrap_text(text=text, max_line_length=max_line_length)
assert result == expected_result
@pytest.mark.parametrize(
("hex_color", "expected_rgba"),
[
("#FF00FF", (255, 0, 255, 255)),
("FF00FF", (255, 0, 255, 255)),
("#FF00FF80", (255, 0, 255, 128)),
("00FF0080", (0, 255, 0, 128)),
(" #ff00ff80 ", (255, 0, 255, 128)),
("abcdef", (171, 205, 239, 255)),
],
)
def test_hex_to_rgba_valid(
hex_color: str, expected_rgba: tuple[int, int, int, int]
) -> None:
assert hex_to_rgba(hex_color) == expected_rgba
@pytest.mark.parametrize("hex_color", ["#FF00F", "#GGHHII", "#FFF", "1234567"])
def test_hex_to_rgba_invalid(hex_color: str) -> None:
with pytest.raises(ValueError, match="Invalid hex"):
hex_to_rgba(hex_color)
@pytest.mark.parametrize(
("rgba", "expected_hex"),
[
((0, 0, 0, 0), "#00000000"),
((255, 0, 255, 255), "#FF00FFFF"),
((0, 255, 0, 128), "#00FF0080"),
((255, 255, 255, 255), "#FFFFFFFF"),
],
)
def test_rgba_to_hex(rgba: tuple[int, int, int, int], expected_hex: str) -> None:
assert rgba_to_hex(rgba) == expected_hex
@pytest.mark.parametrize(
"rgba",
[
(255, 0, 0),
(256, 0, 0, 255),
(-1, 0, 0, 255),
(255, 0, 0, -1),
],
)
def test_rgba_to_hex_invalid(rgba: tuple[int, ...]) -> None:
with pytest.raises(ValueError, match="RGBA must be a 4-tuple"):
rgba_to_hex(rgba) # type: ignore[arg-type]
@pytest.mark.parametrize(
("hex_color", "expected_result"),
[
("#FF00FF", True),
("ff00ff", True),
("00FF0080", True),
(" 00ff0080 ", True),
("#XYZ123", False),
("FF00F", False),
("#FFF", False),
("#1234567", False),
],
)
def test_is_valid_hex(hex_color: str, expected_result: bool) -> None:
assert is_valid_hex(hex_color) is expected_result