Files
wehub-resource-sync 9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:06:10 +08:00

372 lines
13 KiB
Python

import importlib
import sys
from contextlib import ExitStack as DoesNotRaise
import pytest
from supervision.draw.color import Color, ColorPalette, unify_to_bgr
@pytest.mark.parametrize(
("color_hex", "expected_result", "exception"),
[
("fff", Color.WHITE, DoesNotRaise()),
("#fff", Color.WHITE, DoesNotRaise()),
("ffffff", Color.WHITE, DoesNotRaise()),
("#ffffff", Color.WHITE, DoesNotRaise()),
("f00", Color.RED, DoesNotRaise()),
("0f0", Color.GREEN, DoesNotRaise()),
("00f", Color.BLUE, DoesNotRaise()),
("#808000", Color(r=128, g=128, b=0), DoesNotRaise()),
# RGBA hex codes (4 digits)
("f0f8", Color(r=255, g=0, b=255, a=136), DoesNotRaise()),
("#f0f8", Color(r=255, g=0, b=255, a=136), DoesNotRaise()),
("ffff", Color(r=255, g=255, b=255, a=255), DoesNotRaise()),
("f008", Color(r=255, g=0, b=0, a=136), DoesNotRaise()),
# RGBA hex codes (8 digits)
("ff00ff80", Color(r=255, g=0, b=255, a=128), DoesNotRaise()),
("#ff00ff80", Color(r=255, g=0, b=255, a=128), DoesNotRaise()),
("ffffff00", Color(r=255, g=255, b=255, a=0), DoesNotRaise()),
("00ff00ff", Color(r=0, g=255, b=0, a=255), DoesNotRaise()),
# Invalid hex codes
("", None, pytest.raises(ValueError, match="Invalid length of color hash")),
("00", None, pytest.raises(ValueError, match="Invalid length of color hash")),
(
"00000",
None,
pytest.raises(ValueError, match="Invalid length of color hash"),
),
(
"0000000",
None,
pytest.raises(ValueError, match="Invalid length of color hash"),
),
(
"000000000",
None,
pytest.raises(ValueError, match="Invalid length of color hash"),
),
("ffg", None, pytest.raises(ValueError, match="Invalid characters in color")),
("fffg", None, pytest.raises(ValueError, match="Invalid characters in color")),
(
"ff00ffgg",
None,
pytest.raises(ValueError, match="Invalid characters in color"),
),
],
)
def test_color_from_hex(
color_hex, expected_result: Color | None, exception: Exception
) -> None:
"""
Verify that Color.from_hex correctly parses various hex string formats.
Scenario: Creating a `Color` object from various hex string formats (3-, 4-, 6-,
and 8-digit, with/without # prefix).
Expected: Correct RGBA values are parsed, and invalid hex strings raise
`ValueError`. This allows users to define colors using familiar web formats
including alpha.
"""
with exception:
result = Color.from_hex(color_hex=color_hex)
assert result == expected_result
@pytest.mark.parametrize(
("color", "expected_result", "exception"),
[
(Color.WHITE, "#ffffff", DoesNotRaise()),
(Color.BLACK, "#000000", DoesNotRaise()),
(Color.RED, "#ff0000", DoesNotRaise()),
(Color.GREEN, "#00ff00", DoesNotRaise()),
(Color.BLUE, "#0000ff", DoesNotRaise()),
(Color(r=128, g=128, b=0), "#808000", DoesNotRaise()),
# With alpha channel
(Color(r=255, g=0, b=255, a=128), "#ff00ff80", DoesNotRaise()),
(Color(r=255, g=255, b=255, a=255), "#ffffff", DoesNotRaise()),
(Color(r=0, g=255, b=0, a=0), "#00ff0000", DoesNotRaise()),
(Color(r=128, g=128, b=0, a=200), "#808000c8", DoesNotRaise()),
],
)
def test_color_as_hex(
color: Color, expected_result: str | None, exception: Exception
) -> None:
"""
Verify that Color.as_hex correctly converts Color objects to hex strings.
Scenario: Converting a `Color` object back to a hex string.
Expected: 6-digit hex (#RRGGBB) when alpha is 255, 8-digit hex (#RRGGBBAA)
otherwise, ensuring round-trip consistency for color definitions.
"""
with exception:
result = color.as_hex()
assert result == expected_result
@pytest.mark.parametrize(
("color_tuple", "expected_result", "exception"),
[
((255, 255, 255), Color.WHITE, DoesNotRaise()),
((0, 0, 0), Color.BLACK, DoesNotRaise()),
((255, 0, 0), Color.RED, DoesNotRaise()),
((0, 255, 0), Color.GREEN, DoesNotRaise()),
((0, 0, 255), Color.BLUE, DoesNotRaise()),
((128, 128, 0), Color(r=128, g=128, b=0), DoesNotRaise()),
(
(300, 0, 0),
None,
pytest.raises(ValueError, match=r"RGB values must be in range.*300, 0, 0"),
), # R out of range
(
(0, -10, 0),
None,
pytest.raises(ValueError, match=r"RGB values must be in range.*0, -10, 0"),
), # G out of range
(
(0, 0, 500),
None,
pytest.raises(ValueError, match=r"RGB values must be in range.*0, 0, 500"),
), # B out of range
(
(300, -10, 500),
None,
pytest.raises(
ValueError, match=r"RGB values must be in range.*300, -10, 500"
),
), # All out of range
],
)
def test_color_from_rgb_tuple(
color_tuple: tuple[int, int, int],
expected_result: Color | None,
exception: Exception,
) -> None:
with exception:
result = Color.from_rgb_tuple(color_tuple=color_tuple)
assert result == expected_result
@pytest.mark.parametrize(
("color_tuple", "expected_result", "exception"),
[
((255, 255, 255), Color.WHITE, DoesNotRaise()),
((0, 0, 0), Color.BLACK, DoesNotRaise()),
((0, 0, 255), Color.RED, DoesNotRaise()), # BGR format
((0, 255, 0), Color.GREEN, DoesNotRaise()), # BGR format
((255, 0, 0), Color.BLUE, DoesNotRaise()), # BGR format
((0, 128, 128), Color(r=128, g=128, b=0), DoesNotRaise()), # BGR format
(
(300, 0, 0),
None,
pytest.raises(ValueError, match=r"BGR values must be in range.*300, 0, 0"),
), # B out of range
(
(0, -10, 0),
None,
pytest.raises(ValueError, match=r"BGR values must be in range.*0, -10, 0"),
), # G out of range
(
(0, 0, 500),
None,
pytest.raises(ValueError, match=r"BGR values must be in range.*0, 0, 500"),
), # R out of range
(
(300, -10, 500),
None,
pytest.raises(
ValueError, match=r"BGR values must be in range.*300, -10, 500"
),
), # All out of range
],
)
def test_color_from_bgr_tuple(
color_tuple: tuple[int, int, int],
expected_result: Color | None,
exception: Exception,
) -> None:
with exception:
result = Color.from_bgr_tuple(color_tuple=color_tuple)
assert result == expected_result
@pytest.mark.parametrize(
("color_tuple", "expected_result", "exception"),
[
((255, 255, 0, 128), Color(r=255, g=255, b=0, a=128), DoesNotRaise()),
((0, 255, 255, 255), Color(r=0, g=255, b=255, a=255), DoesNotRaise()),
((128, 0, 128, 0), Color(r=128, g=0, b=128, a=0), DoesNotRaise()),
(
(300, 0, 0, 128),
None,
pytest.raises(
ValueError, match=r"RGBA values must be in range.*300, 0, 0, 128"
),
),
(
(0, 0, 0, 300),
None,
pytest.raises(
ValueError, match=r"RGBA values must be in range.*0, 0, 0, 300"
),
),
],
)
def test_color_from_rgba_tuple(
color_tuple: tuple[int, int, int, int],
expected_result: Color | None,
exception: Exception,
) -> None:
with exception:
result = Color.from_rgba_tuple(color_tuple=color_tuple)
assert result == expected_result
@pytest.mark.parametrize(
("color_tuple", "expected_result", "exception"),
[
((0, 255, 255, 128), Color(r=255, g=255, b=0, a=128), DoesNotRaise()),
((255, 255, 0, 255), Color(r=0, g=255, b=255, a=255), DoesNotRaise()),
((128, 0, 128, 0), Color(r=128, g=0, b=128, a=0), DoesNotRaise()),
(
(300, 0, 0, 128),
None,
pytest.raises(
ValueError, match=r"BGRA values must be in range.*300, 0, 0, 128"
),
),
(
(0, 0, 0, 300),
None,
pytest.raises(
ValueError, match=r"BGRA values must be in range.*0, 0, 0, 300"
),
),
],
)
def test_color_from_bgra_tuple(
color_tuple: tuple[int, int, int, int],
expected_result: Color | None,
exception: Exception,
) -> None:
with exception:
result = Color.from_bgra_tuple(color_tuple=color_tuple)
assert result == expected_result
@pytest.mark.parametrize(
"kwargs",
[
pytest.param({"r": -1, "g": 0, "b": 0}, id="red-negative"),
pytest.param({"r": 0, "g": 256, "b": 0}, id="green-too-large"),
pytest.param({"r": 0, "g": 0, "b": 300}, id="blue-too-large"),
pytest.param({"r": 0, "g": 0, "b": 0, "a": -1}, id="alpha-negative"),
],
)
def test_color_constructor_rejects_out_of_range_channels(
kwargs: dict[str, int],
) -> None:
"""Direct Color construction rejects channels outside the byte range."""
with pytest.raises(ValueError, match="Color values must be in range"):
Color(**kwargs)
@pytest.mark.parametrize(
("color", "expected_result", "exception"),
[
(Color(r=255, g=255, b=0, a=128), (255, 255, 0, 128), DoesNotRaise()),
(Color(r=0, g=255, b=255, a=255), (0, 255, 255, 255), DoesNotRaise()),
(Color(r=128, g=0, b=128, a=0), (128, 0, 128, 0), DoesNotRaise()),
],
)
def test_color_as_rgba(
color: Color,
expected_result: tuple[int, int, int, int] | None,
exception: Exception,
) -> None:
with exception:
result = color.as_rgba()
assert result == expected_result
@pytest.mark.parametrize(
("color", "expected_result", "exception"),
[
(Color(r=255, g=255, b=0, a=128), (0, 255, 255, 128), DoesNotRaise()),
(Color(r=0, g=255, b=255, a=255), (255, 255, 0, 255), DoesNotRaise()),
(Color(r=128, g=0, b=128, a=0), (128, 0, 128, 0), DoesNotRaise()),
],
)
def test_color_as_bgra(
color: Color,
expected_result: tuple[int, int, int, int] | None,
exception: Exception,
) -> None:
with exception:
result = color.as_bgra()
assert result == expected_result
@pytest.mark.parametrize(
("color", "expected_repr"),
[
(Color(r=255, g=255, b=255), "Color(r=255, g=255, b=255)"),
(Color(r=0, g=0, b=0), "Color(r=0, g=0, b=0)"),
(Color(r=255, g=0, b=255, a=255), "Color(r=255, g=0, b=255)"),
(Color(r=255, g=0, b=255, a=128), "Color(r=255, g=0, b=255, a=128)"),
(Color(r=255, g=0, b=255, a=0), "Color(r=255, g=0, b=255, a=0)"),
],
)
def test_color_repr(color: Color, expected_repr: str) -> None:
assert repr(color) == expected_repr
@pytest.mark.parametrize(
("color_a", "color_b", "expect_equal_hash"),
[
(Color(r=255, g=0, b=0), Color(r=255, g=0, b=0), True),
(Color(r=255, g=0, b=0, a=255), Color(r=255, g=0, b=0), True),
(Color(r=255, g=0, b=0, a=128), Color(r=255, g=0, b=0, a=64), False),
(Color(r=255, g=0, b=0, a=0), Color(r=255, g=0, b=0, a=255), False),
],
)
def test_color_hash(color_a: Color, color_b: Color, expect_equal_hash: bool) -> None:
assert (hash(color_a) == hash(color_b)) == expect_equal_hash
assert (color_a == color_b) == expect_equal_hash
def test_palette_accepts_negative_indices() -> None:
"""Negative palette indices wrap like standard Python sequences."""
palette = ColorPalette.from_hex(["#ff0000", "#00ff00", "#0000ff"])
assert palette.by_idx(-1) == Color.from_hex("#0000ff")
def test_empty_palette_lookup_raises() -> None:
"""Empty palette lookup raises a clear API error."""
palette = ColorPalette.from_hex([])
with pytest.raises(ValueError, match="at least one color"):
palette.by_idx(0)
def test_unify_to_bgr_passes_through_bgr_tuple() -> None:
"""Tuple inputs are already BGR and must not be channel-swapped."""
assert unify_to_bgr((1, 2, 3)) == (1, 2, 3)
def test_from_matplotlib_accepts_single_color() -> None:
"""A one-color matplotlib palette is valid and must not divide by zero."""
palette = ColorPalette.from_matplotlib("jet", 1)
assert len(palette) == 1
def test_draw_color_import_does_not_import_matplotlib_pyplot() -> None:
"""Importing draw.color keeps pyplot lazy until from_matplotlib is called."""
sys.modules.pop("supervision.draw.color", None)
sys.modules.pop("matplotlib.pyplot", None)
importlib.import_module("supervision.draw.color")
assert "matplotlib.pyplot" not in sys.modules