Extend docstrings to test cases for enhanced readability (#2130)

* Extend docstrings to test cases for enhanced readability and consistency across testing modules
* Extend test case docstrings across multiple modules for improved clarity and consistency
* Refactor test case docstrings for clarity and consistency in `test_annotators.py`

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jirka Borovec 2026-02-04 01:28:33 +09:00 committed by GitHub
parent d89ebde4d1
commit 2d0404e67c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 261 additions and 28 deletions

View File

@ -59,17 +59,33 @@ def gradient_image() -> np.ndarray:
class TestBoxAnnotator:
"""Tests for BoxAnnotator class"""
"""
Verify that BoxAnnotator correctly draws bounding boxes on an image.
def test_annotate_with_no_detections(self, test_image):
"""Test that annotate method returns unmodified image when no detections"""
Ensures that `BoxAnnotator` correctly draws bounding boxes on an image, which is
essential for users to visualize detection results.
"""
def test_annotate_with_no_detections(self, test_image: np.ndarray) -> None:
"""
Verify that annotation with no detections does not change the image.
Scenario: Annotating an image with an empty set of detections.
Expected: The scene remains unchanged, ensuring no ghost boxes are drawn.
"""
detections = Detections.empty()
annotator = BoxAnnotator()
result = annotator.annotate(scene=test_image.copy(), detections=detections)
assert np.array_equal(test_image, result)
def test_annotate_with_single_detection(self, test_image):
"""Test that annotate method correctly draws a single bounding box"""
def test_annotate_with_single_detection(self, test_image: np.ndarray) -> None:
"""
Verify that annotation with a single detection draws a bounding box.
Scenario: Annotating an image with a single bounding box.
Expected: The scene is modified by drawing a box, allowing users to identify
a single detected object.
"""
detections = _create_detections(xyxy=[[10, 10, 90, 90]], class_id=[0])
annotator = BoxAnnotator(
color=Color.WHITE, thickness=2, color_lookup=ColorLookup.INDEX
@ -77,8 +93,14 @@ class TestBoxAnnotator:
result = annotator.annotate(scene=test_image.copy(), detections=detections)
assert_image_mostly_same(test_image, result, similarity_threshold=0.85)
def test_annotate_with_multiple_detections(self, test_image):
"""Test that annotate method correctly draws multiple bounding boxes"""
def test_annotate_with_multiple_detections(self, test_image: np.ndarray) -> None:
"""
Verify that annotation with multiple detections draws all bounding boxes.
Scenario: Annotating an image with multiple bounding boxes of different classes.
Expected: All boxes are drawn, enabling visualization of complex scenes with
multiple objects.
"""
detections = _create_detections(
xyxy=[[10, 10, 40, 40], [60, 60, 90, 90], [10, 60, 40, 90]],
class_id=[0, 1, 2],
@ -89,8 +111,14 @@ class TestBoxAnnotator:
result = annotator.annotate(scene=test_image.copy(), detections=detections)
assert_image_mostly_same(test_image, result, similarity_threshold=0.85)
def test_annotate_with_numpy_color_lookup(self, test_image):
"""Test that annotate works when color lookup is a NumPy array"""
def test_annotate_with_numpy_color_lookup(self, test_image: np.ndarray) -> None:
"""
Verify that annotation respects custom NumPy color lookup array.
Scenario: Providing a custom NumPy array for color lookup instead of class IDs.
Expected: Annotator respects the custom mapping, giving users flexible control
over box colors (e.g., coloring by tracking ID or custom criteria).
"""
detections = Detections(
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]], dtype=np.float32),
confidence=np.array([0.38, 0.21], dtype=np.float32),

View File

@ -177,6 +177,13 @@ def test_dataset_merge(
expected_result: DetectionDataset | None,
exception: Exception,
) -> None:
"""
Verify that multiple DetectionDataset objects can be successfully merged.
Ensures that multiple `DetectionDataset` objects can be merged into single dataset.
This is vital for users who need to combine data from different sources or
augment their datasets with additional labeled examples.
"""
with exception:
result = DetectionDataset.merge(dataset_list=dataset_list)
assert result == expected_result

View File

@ -132,6 +132,8 @@ TEST_DET_DIFFERENT_METADATA = Detections(
@pytest.mark.parametrize(
("detections", "index", "expected_result", "exception"),
[
# Scenario: Filter detections by class ID using a boolean mask.
# Expected: Only detections matching the class ID are retained.
(
DETECTIONS,
DETECTIONS.class_id == 0,
@ -139,7 +141,9 @@ TEST_DET_DIFFERENT_METADATA = Detections(
xyxy=[[2254, 906, 2447, 1353]], confidence=[0.90538], class_id=[0]
),
DoesNotRaise(),
), # take only detections with class_id = 0
),
# Scenario: Filter detections by confidence score threshold.
# Expected: Only high-confidence detections are kept, filtering out noise.
(
DETECTIONS,
DETECTIONS.confidence > 0.5,
@ -153,7 +157,9 @@ TEST_DET_DIFFERENT_METADATA = Detections(
class_id=[0, 56, 39],
),
DoesNotRaise(),
), # take only detections with confidence > 0.5
),
# Scenario: Select all detections using a full boolean mask.
# Expected: Result is identical to input.
(
DETECTIONS,
np.array(
@ -161,7 +167,9 @@ TEST_DET_DIFFERENT_METADATA = Detections(
),
DETECTIONS,
DoesNotRaise(),
), # take all detections
),
# Scenario: Select no detections using an empty boolean mask.
# Expected: An empty Detections object with correct shapes.
(
DETECTIONS,
np.array(
@ -174,7 +182,9 @@ TEST_DET_DIFFERENT_METADATA = Detections(
class_id=np.array([], dtype=int),
),
DoesNotRaise(),
), # take no detections
),
# Scenario: Select specific detections using a list of integer indices.
# Expected: Only requested indices are returned in specified order.
(
DETECTIONS,
[0, 2],
@ -184,7 +194,9 @@ TEST_DET_DIFFERENT_METADATA = Detections(
class_id=[0, 39],
),
DoesNotRaise(),
), # take only first and third detection using List[int] index
),
# Scenario: Select specific detections using a NumPy array of indices.
# Expected: Only requested indices are returned.
(
DETECTIONS,
np.array([0, 2]),
@ -194,7 +206,9 @@ TEST_DET_DIFFERENT_METADATA = Detections(
class_id=[0, 39],
),
DoesNotRaise(),
), # take only first and third detection using np.ndarray index
),
# Scenario: Select a single detection using an integer index.
# Expected: A Detections object containing only that element.
(
DETECTIONS,
0,
@ -202,7 +216,9 @@ TEST_DET_DIFFERENT_METADATA = Detections(
xyxy=[[2254, 906, 2447, 1353]], confidence=[0.90538], class_id=[0]
),
DoesNotRaise(),
), # take only first detection by index
),
# Scenario: Select a range of detections using a slice.
# Expected: Detections within the slice range are returned.
(
DETECTIONS,
slice(1, 3),
@ -212,9 +228,11 @@ TEST_DET_DIFFERENT_METADATA = Detections(
class_id=[56, 39],
),
DoesNotRaise(),
), # take only first detection by index slice (1, 3)
(DETECTIONS, 10, None, pytest.raises(IndexError)), # index out of range
(DETECTIONS, [0, 2, 10], None, pytest.raises(IndexError)), # index out of range
),
# Scenario: Index out of range.
# Expected: IndexError is raised.
(DETECTIONS, 10, None, pytest.raises(IndexError)),
(DETECTIONS, [0, 2, 10], None, pytest.raises(IndexError)),
(DETECTIONS, np.array([0, 2, 10]), None, pytest.raises(IndexError)),
(
DETECTIONS,
@ -224,12 +242,14 @@ TEST_DET_DIFFERENT_METADATA = Detections(
None,
pytest.raises(IndexError),
),
# Scenario: Filter an empty Detections object.
# Expected: Returns an empty Detections object without crashing.
(
Detections.empty(),
np.isin(Detections.empty()["class_name"], ["cat", "dog"]),
Detections.empty(),
DoesNotRaise(),
), # Filter an empty detections by specific class names
),
],
)
def test_getitem(
@ -238,6 +258,11 @@ def test_getitem(
expected_result: Detections | None,
exception: Exception,
) -> None:
"""
Ensures that `Detections.__getitem__` (indexing/slicing) works correctly for various
input types. This is a core feature that allows users to filter and manipulate
detection results easily.
"""
with exception:
result = detections[index]
assert result == expected_result

View File

@ -28,6 +28,14 @@ from supervision.draw.color import 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-digit,
6-digit, with/without # prefix).
Expected: Correct RGB values are parsed, and invalid hex strings raise `ValueError`.
This allows users to define colors using familiar web formats.
"""
with exception:
result = Color.from_hex(color_hex=color_hex)
assert result == expected_result
@ -47,6 +55,13 @@ def test_color_from_hex(
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: Correct 6-digit hex string with # prefix is returned, ensuring
round-trip consistency for color definitions.
"""
with exception:
result = color.as_hex()
assert result == expected_result

View File

@ -29,6 +29,13 @@ from supervision.geometry.core import Point, Vector
def test_vector_cross_product(
vector: Vector, point: Point, expected_result: float
) -> None:
"""
Verify that Vector.cross_product correctly calculates the scalar value.
Scenario: Computing the cross product between a vector and a point.
Expected: Correct scalar value is returned, which is used to determine which side
of a line a point lies on (essential for line crossing counting).
"""
result = vector.cross_product(point=point)
assert result == expected_result
@ -55,5 +62,12 @@ def test_vector_cross_product(
],
)
def test_vector_magnitude(vector: Vector, expected_result: float) -> None:
"""
Verify that Vector.magnitude correctly calculates Euclidean distance.
Scenario: Calculating the magnitude (length) of a vector.
Expected: Correct Euclidean distance between start and end points is returned,
fundamental for various spatial calculations.
"""
result = vector.magnitude
assert result == expected_result

View File

@ -48,5 +48,13 @@ def generate_test_polygon(n: int) -> np.ndarray:
],
)
def test_get_polygon_center(polygon: np.ndarray, expected_result: Point) -> None:
"""
Verify that get_polygon_center correctly calculates the centroid of a polygon.
Scenario: Calculating the center point (centroid) of various polygons.
Expected: The returned `Point` correctly represents the average position of all
polygon vertices, which is used for placing labels or markers at the center
of detected objects.
"""
result = get_polygon_center(polygon)
assert result == expected_result

View File

@ -5,8 +5,20 @@ from tests.helpers import assert_image_mostly_same
class TestVertexAnnotator:
"""
Verify that VertexAnnotator correctly draws keypoints on an image.
Ensures that `VertexAnnotator` correctly draws keypoints (vertices) on an image,
which is essential for human pose estimation or similar tasks.
"""
def test_annotate_with_default_parameters(self, scene, sample_key_points):
"""Test annotation with default parameters."""
"""
Verify that VertexAnnotator correctly draws keypoints with default parameters.
Scenario: Annotating a scene using default vertex parameters.
Expected: Scene is modified, showing keypoints at their detected locations.
"""
annotator = sv.VertexAnnotator()
result = annotator.annotate(scene=scene.copy(), key_points=sample_key_points)
@ -16,7 +28,13 @@ class TestVertexAnnotator:
)
def test_annotate_with_custom_color_and_radius(self, scene, sample_key_points):
"""Test annotation with custom color and radius."""
"""
Verify that VertexAnnotator respects custom color and radius settings.
Scenario: Annotating a scene with user-specified color and radius.
Expected: Scene is modified according to custom style, allowing users to
distinguish keypoints more clearly or match specific branding.
"""
color = sv.Color.RED
radius = 5
annotator = sv.VertexAnnotator(color=color, radius=radius)
@ -28,7 +46,12 @@ class TestVertexAnnotator:
)
def test_annotate_empty_key_points(self, scene, empty_key_points):
"""Test annotation with empty key points returns unchanged scene."""
"""
Verify that VertexAnnotator handles empty keypoints without modifying the scene.
Scenario: Annotating a scene with no key points detected.
Expected: Original scene is returned untouched, preventing phantom annotations.
"""
annotator = sv.VertexAnnotator()
result = annotator.annotate(scene=scene.copy(), key_points=empty_key_points)
@ -37,8 +60,20 @@ class TestVertexAnnotator:
class TestEdgeAnnotator:
"""
Verify that EdgeAnnotator correctly draws skeleton edges between keypoints.
Ensures that `EdgeAnnotator` correctly draws connections (edges) between keypoints,
forming skeletons that help users interpret spatial relationships.
"""
def test_annotate_with_default_parameters(self, scene, sample_key_points):
"""Test annotation with default parameters using COCO skeleton."""
"""
Verify correctly draw skeleton edges with default parameters.
Scenario: Annotating a scene with default skeleton (e.g., COCO).
Expected: Skeleton edges are drawn between corresponding keypoints.
"""
annotator = sv.EdgeAnnotator()
result = annotator.annotate(scene=scene.copy(), key_points=sample_key_points)
@ -48,7 +83,13 @@ class TestEdgeAnnotator:
)
def test_annotate_with_custom_edges(self, scene, sample_key_points):
"""Test annotation with custom edge definitions."""
"""
Verify that EdgeAnnotator respects custom-defined skeleton structures.
Scenario: Annotating a scene with a custom-defined skeleton structure.
Expected: Only the specified connections are drawn, giving users flexibility
for non-standard keypoint models.
"""
edges = [(1, 2), (2, 3)]
annotator = sv.EdgeAnnotator(edges=edges)
result = annotator.annotate(scene=scene.copy(), key_points=sample_key_points)
@ -59,7 +100,12 @@ class TestEdgeAnnotator:
)
def test_annotate_empty_key_points(self, scene, empty_key_points):
"""Test annotation with empty key points returns unchanged scene."""
"""
Verify that EdgeAnnotator handles empty keypoints without modifying the scene.
Scenario: Annotating a scene with no key points for edge drawing.
Expected: Original scene is returned untouched.
"""
annotator = sv.EdgeAnnotator()
result = annotator.annotate(scene=scene.copy(), key_points=empty_key_points)
@ -67,7 +113,13 @@ class TestEdgeAnnotator:
assert np.array_equal(result, scene)
def test_annotate_no_edges_found(self, scene):
"""Test annotation when no matching skeleton is found."""
"""
Verify returning unmodified scene when no known skeleton matches.
Scenario: Key points provided don't match any known or provided skeleton.
Expected: No edges are drawn, and the original scene is returned, avoiding
incorrect or nonsensical connections.
"""
# Key points with more vertices than any skeleton
large_key_points = sv.KeyPoints(
xy=np.array([[[i * 10, i * 10] for i in range(100)]], dtype=np.float32),

View File

@ -16,6 +16,14 @@ from tests.helpers import _create_detections, assert_almost_equal
class TestDetectionMetrics:
"""
Verify that detection metrics are computed accurately.
Ensures that detection metrics (mAP, Conf. Matrix, etc.) are computed accurately.
These metrics are the primary way users evaluate the performance of their models
within the `supervision` ecosystem.
"""
CLASSES = np.arange(80)
NUM_CLASSES = len(CLASSES)
@ -188,7 +196,14 @@ class TestDetectionMetrics:
with_confidence: bool,
expected_result: np.ndarray | None,
exception: Exception,
):
) -> None:
"""
Verify that Detections objects are correctly converted to NumPy tensors.
Scenario: Converting Detections objects to NumPy tensors.
Expected: Tensors are correctly formatted for consumption by metric functions,
preserving coordinates, class IDs, and optionally confidence scores.
"""
with exception:
result = detections_to_tensor(
detections=detections, with_confidence=with_confidence
@ -436,6 +451,13 @@ class TestDetectionMetrics:
expected_result: float,
exception: Exception,
) -> None:
"""
Verify that Average Precision is correctly calculated from PR curve points.
Scenario: Computing Average Precision (AP) from PR curve points.
Expected: AP is correctly calculated using the area under the curve, which is
the standard for evaluating detection models (mAP components).
"""
with exception:
result = MeanAveragePrecision.compute_average_precision(
recall=recall, precision=precision

View File

@ -20,6 +20,13 @@ def dummy_video_path(tmp_path):
def test_process_video_exception_handling(dummy_video_path, tmp_path):
"""
Verify that process_video correctly propagates exceptions from the callback.
Scenario: Processing a video where the callback raises an exception.
Expected: `process_video` should propagate the exception, allowing users to
handle errors during video processing.
"""
target_path = str(tmp_path / "target.mp4")
def callback_with_exception(frame, index):
@ -36,6 +43,13 @@ def test_process_video_exception_handling(dummy_video_path, tmp_path):
def test_process_video_success(dummy_video_path, tmp_path):
"""
Verify successful video processing with a pass-through callback.
Scenario: Successfully processing a video with a simple pass-through callback.
Expected: The video is processed without error and the target file is created,
verifying the core functionality of `process_video`.
"""
target_path = str(tmp_path / "target_success.mp4")
def callback_success(frame, index):
@ -50,6 +64,12 @@ def test_process_video_success(dummy_video_path, tmp_path):
def test_process_video_exception_with_small_buffer(dummy_video_path, tmp_path):
"""
Verify that process_video handles exceptions correctly even with small buffers.
Scenario: Processing a video with minimal buffering where an exception occurs.
Expected: The exception is still correctly propagated even with low memory settings.
"""
target_path = str(tmp_path / "target_exception_small_buffer.mp4")
def callback_with_exception(frame, index):
@ -68,6 +88,13 @@ def test_process_video_exception_with_small_buffer(dummy_video_path, tmp_path):
def test_process_video_max_frames(dummy_video_path, tmp_path):
"""
Verify that process_video respects the max_frames parameter.
Scenario: Processing only a limited number of frames using `max_frames`.
Expected: Only the specified number of frames are processed, which is useful for
quick testing or sampling.
"""
target_path = str(tmp_path / "target_max_frames.mp4")
processed_indices = []
@ -87,6 +114,13 @@ def test_process_video_max_frames(dummy_video_path, tmp_path):
def test_process_video_custom_params(dummy_video_path, tmp_path):
"""
Verify that process_video works correctly with custom performance parameters.
Scenario: Processing video with custom prefetch and buffer parameters.
Expected: Video is processed successfully, showing that these performance-tuning
parameters are correctly handled.
"""
target_path = str(tmp_path / "target_custom_params.mp4")
def callback(frame, index):
@ -105,6 +139,13 @@ def test_process_video_custom_params(dummy_video_path, tmp_path):
def test_video_info(dummy_video_path):
"""
Verify that VideoInfo correctly retrieves metadata from a video file.
Scenario: Retrieving metadata from a video file using `VideoInfo`.
Expected: Correct width, height, fps, and frame count are returned, which is
essential for initializing annotators or calculating statistics.
"""
video_info = VideoInfo.from_video_path(dummy_video_path)
assert video_info.width == 640
assert video_info.height == 480
@ -114,6 +155,13 @@ def test_video_info(dummy_video_path):
def test_get_video_frames_generator(dummy_video_path):
"""
Verify that get_video_frames_generator yields frames with correct shapes.
Scenario: Iterating over video frames using a generator.
Expected: All frames are yielded in order as NumPy arrays with correct shapes,
enabling frame-by-frame processing loops.
"""
generator = get_video_frames_generator(dummy_video_path)
frames = list(generator)
assert len(frames) == 10
@ -122,12 +170,26 @@ def test_get_video_frames_generator(dummy_video_path):
def test_get_video_frames_generator_with_stride(dummy_video_path):
"""
Verify that get_video_frames_generator correctly handles the stride parameter.
Scenario: Iterating over video frames with specified stride (e.g., every 2nd frame).
Expected: The generator correctly skips frames according to the stride, allowing
for faster processing of high-FPS videos.
"""
generator = get_video_frames_generator(dummy_video_path, stride=2)
frames = list(generator)
assert len(frames) == 5
def test_get_video_frames_generator_with_start_end(dummy_video_path):
"""
Verify that get_video_frames_generator respects start and end frame indices.
Scenario: Iterating over a specific range of video frames using `start` and `end`.
Expected: Only frames within the specified range are yielded, enabling targeted
analysis of video segments.
"""
generator = get_video_frames_generator(dummy_video_path, start=2, end=5)
frames = list(generator)
assert len(frames) == 3