Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/supervision/annotators/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
overlay_image,
scale_image,
)
from supervision.utils.logger import _get_logger

logger = _get_logger(__name__)


@overload
Expand Down Expand Up @@ -1724,7 +1727,9 @@ def load_default_font(
try:
return ImageFont.truetype(font_path, font_size)
except OSError:
print(f"Font path '{font_path}' not found. Using PIL's default font.")
logger.warning(
"Font path '%s' not found. Using PIL's default font.", font_path
)
return load_default_font(font_size)


Expand Down
9 changes: 6 additions & 3 deletions src/supervision/assets/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from tqdm.auto import tqdm

from supervision.assets.list import MEDIA_ASSETS, Assets
from supervision.utils.logger import _get_logger

logger = _get_logger(__name__)


def is_md5_hash_matching(filename: str, original_md5_hash: str) -> bool:
Expand Down Expand Up @@ -61,7 +64,7 @@ def download_assets(asset_name: Assets | str) -> str:

if filename in MEDIA_ASSETS:
if not Path(filename).exists():
print(f"Downloading {filename} assets \n")
logger.info("Downloading %s assets", filename)
response = get(
MEDIA_ASSETS[filename][0], stream=True, allow_redirects=True, timeout=30
)
Expand All @@ -78,11 +81,11 @@ def download_assets(asset_name: Assets | str) -> str:
copyfileobj(raw_resp, file)
else:
if not is_md5_hash_matching(filename, MEDIA_ASSETS[filename][1]):
print("File corrupted. Re-downloading... \n")
logger.warning("File corrupted. Re-downloading...")
os.remove(filename)
return download_assets(filename)

print(f"{filename} asset download complete. \n")
logger.info("%s asset download complete.", filename)
else:
valid_assets = ", ".join(filename for filename in MEDIA_ASSETS.keys())
raise ValueError(
Expand Down
10 changes: 7 additions & 3 deletions src/supervision/detection/tools/csv_sink.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from typing import Any

from supervision.detection.core import Detections
from supervision.utils.logger import _get_logger

logger = _get_logger(__name__)

BASE_HEADER = [
"x_min",
Expand Down Expand Up @@ -168,9 +171,10 @@ def append(
self.header_written = True

if field_names != self.field_names:
print(
f"Field names do not match the header. "
f"Expected: {self.field_names}, given: {field_names}"
logger.warning(
"Field names do not match the header. Expected: %s, given: %s",
self.field_names,
field_names,
)

parsed_rows = CSVSink.parse_detection_data(detections, custom_data)
Expand Down
7 changes: 6 additions & 1 deletion src/supervision/metrics/mean_average_precision.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from supervision.draw.color import LEGACY_COLOR_PALETTE
from supervision.metrics.core import Metric, MetricTarget
from supervision.metrics.utils.utils import ensure_pandas_installed
from supervision.utils.logger import _get_logger

logger = _get_logger(__name__)

if TYPE_CHECKING:
import pandas as pd
Expand Down Expand Up @@ -1098,7 +1101,9 @@ def _summarize(
mean_s = -1.0
else:
mean_s = float(np.mean(s[s > -1]))
print(iStr.format(titleStr, typeStr, iou_str, area_range, max_dets, mean_s))
logger.info(
iStr.format(titleStr, typeStr, iou_str, area_range, max_dets, mean_s)
)
return mean_s

def _summarize_predictions() -> npt.NDArray[np.float64]:
Expand Down
51 changes: 51 additions & 0 deletions src/supervision/utils/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

import logging
import os
import sys


def _get_logger(name: str = "supervision", level: int | None = None) -> logging.Logger:
"""Creates and configures a logger with stdout and stderr handlers.

This function creates a logger that sends INFO and DEBUG level logs to stdout,
and WARNING, ERROR, and CRITICAL level logs to stderr. If the logger already
has handlers, it returns the existing logger without adding new handlers.

The log level can be specified directly or through the `LOG_LEVEL` environment
variable.

Args:
name: The name of the logger. Defaults to `"supervision"`.
level: The logging level to set. If `None`, uses the `LOG_LEVEL` environment
variable, defaulting to `INFO` if not set.

Returns:
A configured `logging.Logger` instance.
"""
if level is None:
level = getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO)

logger = logging.getLogger(name)
logger.setLevel(level)

if not logger.handlers:
formatter = logging.Formatter(
"[%(asctime)s] [%(levelname)s] %(name)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)

stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.DEBUG)
stdout_handler.addFilter(lambda r: r.levelno <= logging.INFO)
stdout_handler.setFormatter(formatter)

stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(logging.WARNING)
stderr_handler.setFormatter(formatter)

logger.addHandler(stdout_handler)
logger.addHandler(stderr_handler)
logger.propagate = False

return logger
6 changes: 5 additions & 1 deletion src/supervision/utils/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
import numpy.typing as npt
from tqdm.auto import tqdm

from supervision.utils.logger import _get_logger

logger = _get_logger(__name__)


@dataclass
class VideoInfo:
Expand Down Expand Up @@ -97,7 +101,7 @@ def __enter__(self) -> VideoSink:
try:
self.__fourcc = cv2.VideoWriter_fourcc(*self.__codec)
except TypeError as e:
print(str(e) + ". Defaulting to mp4v...")
logger.warning("%s. Defaulting to mp4v...", str(e))
self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v")
self.__writer = cv2.VideoWriter(
self.target_path,
Expand Down
22 changes: 12 additions & 10 deletions tests/assets/test_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ def test_file_not_exists(self):


class TestDownloadAssets:
@patch("builtins.print")
@patch("supervision.assets.downloader.logger")
@patch("supervision.assets.downloader.is_md5_hash_matching", return_value=True)
@patch("pathlib.Path.exists", return_value=True)
def test_already_exists_and_valid(self, mock_exists, mock_md5, mock_print):
def test_already_exists_and_valid(self, mock_exists, mock_md5, mock_logger):
"""Test download_assets when file already exists and is valid."""
filename = "vehicles.mp4"
result = download_assets(filename)
assert result == filename
mock_print.assert_called_with(f"{filename} asset download complete. \n")
mock_logger.info.assert_called_with("%s asset download complete.", filename)

@patch("supervision.assets.downloader.download_assets", return_value="vehicles.mp4")
@patch("os.remove")
Expand All @@ -59,7 +59,7 @@ def test_already_exists_but_corrupted(
assert result == filename
mock_download.assert_called_with(filename)

@patch("builtins.print")
@patch("supervision.assets.downloader.logger")
@patch("pathlib.Path.open", new_callable=mock_open)
@patch("pathlib.Path.mkdir")
@patch("pathlib.Path.exists", return_value=False)
Expand All @@ -74,7 +74,7 @@ def test_download_new_file(
mock_exists,
mock_mkdir,
mock_open_file,
mock_print,
mock_logger,
):
"""Test download_assets downloading a new file."""
filename = "vehicles.mp4"
Expand All @@ -92,7 +92,7 @@ def test_download_new_file(

result = download_assets(filename)
assert result == filename
mock_print.assert_called_with(f"Downloading {filename} assets \n")
mock_logger.info.assert_called_with("Downloading %s assets", filename)
mock_get.assert_called_once()
mock_response.raise_for_status.assert_called_once_with()
mock_copyfileobj.assert_called_once()
Expand All @@ -119,7 +119,7 @@ def test_invalid_asset_when_file_exists(self, mock_exists):
assert "Invalid asset" in str(exc_info.value)
assert "vehicles.mp4" in str(exc_info.value)

@patch("builtins.print")
@patch("supervision.assets.downloader.logger")
@patch("pathlib.Path.open", new_callable=mock_open)
@patch("pathlib.Path.mkdir")
@patch("supervision.assets.downloader.copyfileobj")
Expand All @@ -134,7 +134,7 @@ def test_with_video_enum(
mock_copyfileobj,
mock_mkdir,
mock_open_file,
mock_print,
mock_logger,
):
"""Test download_assets with VideoAssets enum."""
asset = VideoAssets.VEHICLES
Expand All @@ -150,8 +150,9 @@ def test_with_video_enum(

result = download_assets(asset)
assert result == asset.filename
mock_logger.info.assert_called_with("Downloading %s assets", asset.filename)

@patch("builtins.print")
@patch("supervision.assets.downloader.logger")
@patch("pathlib.Path.open", new_callable=mock_open)
@patch("pathlib.Path.mkdir")
@patch("supervision.assets.downloader.copyfileobj")
Expand All @@ -166,7 +167,7 @@ def test_with_image_enum(
mock_copyfileobj,
mock_mkdir,
mock_open_file,
mock_print,
mock_logger,
):
"""Test download_assets with ImageAssets enum."""
asset = ImageAssets.SOCCER
Expand All @@ -182,3 +183,4 @@ def test_with_image_enum(

result = download_assets(asset)
assert result == asset.filename
mock_logger.info.assert_called_with("Downloading %s assets", asset.filename)