diff --git a/src/spatialdata_plot/__init__.py b/src/spatialdata_plot/__init__.py index fd8c82c0..76c7d18c 100644 --- a/src/spatialdata_plot/__init__.py +++ b/src/spatialdata_plot/__init__.py @@ -1,7 +1,9 @@ from importlib.metadata import version from . import pl +from ._logging import set_verbosity +from ._settings import Verbosity -__all__ = ["pl"] +__all__ = ["pl", "set_verbosity", "Verbosity"] __version__ = version("spatialdata-plot") diff --git a/src/spatialdata_plot/_logging.py b/src/spatialdata_plot/_logging.py index 364cba27..eba0877f 100644 --- a/src/spatialdata_plot/_logging.py +++ b/src/spatialdata_plot/_logging.py @@ -1,21 +1,25 @@ # from https://github.com/scverse/spatialdata/blob/main/src/spatialdata/_logging.py +from __future__ import annotations + import logging import re from collections.abc import Iterator from contextlib import contextmanager from typing import TYPE_CHECKING +from ._settings import _VERBOSITY_TO_LOGLEVEL, Verbosity + if TYPE_CHECKING: # pragma: no cover from _pytest.logging import LogCaptureFixture -def _setup_logger() -> "logging.Logger": +def _setup_logger() -> logging.Logger: from rich.console import Console from rich.logging import RichHandler logger = logging.getLogger(__name__) - logger.setLevel(logging.INFO) + logger.setLevel(logging.WARNING) console = Console(force_terminal=True) if console.is_jupyter is True: console.is_jupyter = False @@ -30,9 +34,31 @@ def _setup_logger() -> "logging.Logger": logger = _setup_logger() +def set_verbosity(verbosity: Verbosity | int | str) -> None: + """Set the verbosity level of the spatialdata-plot logger. + + Mirrors scanpy's verbosity convention. + + Parameters + ---------- + verbosity + The verbosity level. Accepts a :class:`Verbosity` enum member, + an ``int`` (0–3), or a ``str`` (e.g. ``"warning"``, ``"info"``). + """ + if isinstance(verbosity, str): + try: + verbosity = Verbosity[verbosity.lower()] + except KeyError: + msg = f"Cannot set verbosity to {verbosity!r}. Accepted string values are: {list(Verbosity.__members__)}" + raise ValueError(msg) from None + else: + verbosity = Verbosity(verbosity) + logger.setLevel(_VERBOSITY_TO_LOGLEVEL[verbosity]) + + @contextmanager def logger_warns( - caplog: "LogCaptureFixture", + caplog: LogCaptureFixture, logger: logging.Logger, match: str | None = None, level: int = logging.WARNING, diff --git a/src/spatialdata_plot/_settings.py b/src/spatialdata_plot/_settings.py new file mode 100644 index 00000000..2618c49a --- /dev/null +++ b/src/spatialdata_plot/_settings.py @@ -0,0 +1,33 @@ +"""Settings for spatialdata-plot, mirroring scanpy's verbosity pattern.""" + +from __future__ import annotations + +import logging +from enum import IntEnum + + +class Verbosity(IntEnum): + """Verbosity levels, mirroring scanpy's convention. + + ======== ===== ================= + Level Value Logging level + ======== ===== ================= + error 0 ``logging.ERROR`` + warning 1 ``logging.WARNING`` + info 2 ``logging.INFO`` + debug 3 ``logging.DEBUG`` + ======== ===== ================= + """ + + error = 0 + warning = 1 + info = 2 + debug = 3 + + +_VERBOSITY_TO_LOGLEVEL: dict[Verbosity, int] = { + Verbosity.error: logging.ERROR, + Verbosity.warning: logging.WARNING, + Verbosity.info: logging.INFO, + Verbosity.debug: logging.DEBUG, +} diff --git a/tests/pl/test_logging.py b/tests/pl/test_logging.py new file mode 100644 index 00000000..cac19ce3 --- /dev/null +++ b/tests/pl/test_logging.py @@ -0,0 +1,65 @@ +import logging + +import pytest + +import spatialdata_plot +from spatialdata_plot._logging import logger +from spatialdata_plot._settings import Verbosity + + +class TestSetVerbosity: + @pytest.fixture(autouse=True) + def _restore_verbosity(self): + """Restore default verbosity after each test.""" + yield + spatialdata_plot.set_verbosity(Verbosity.warning) + + def test_default_level_is_warning(self): + assert logger.level == logging.WARNING + + @pytest.mark.parametrize( + ("input_value", "expected_level"), + [ + (Verbosity.error, logging.ERROR), + (Verbosity.warning, logging.WARNING), + (Verbosity.info, logging.INFO), + (Verbosity.debug, logging.DEBUG), + ], + ) + def test_set_verbosity_with_enum(self, input_value, expected_level): + spatialdata_plot.set_verbosity(input_value) + assert logger.level == expected_level + + @pytest.mark.parametrize( + ("input_value", "expected_level"), + [ + (0, logging.ERROR), + (1, logging.WARNING), + (2, logging.INFO), + (3, logging.DEBUG), + ], + ) + def test_set_verbosity_with_int(self, input_value, expected_level): + spatialdata_plot.set_verbosity(input_value) + assert logger.level == expected_level + + @pytest.mark.parametrize( + ("input_value", "expected_level"), + [ + ("error", logging.ERROR), + ("WARNING", logging.WARNING), + ("Info", logging.INFO), + ("debug", logging.DEBUG), + ], + ) + def test_set_verbosity_with_string(self, input_value, expected_level): + spatialdata_plot.set_verbosity(input_value) + assert logger.level == expected_level + + def test_set_verbosity_invalid_string_raises(self): + with pytest.raises(ValueError, match="Cannot set verbosity"): + spatialdata_plot.set_verbosity("verbose") + + def test_set_verbosity_invalid_int_raises(self): + with pytest.raises(ValueError): + spatialdata_plot.set_verbosity(99)