From 4e682145817cbe2db9d60645fca0d1b17846b7a7 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Sun, 10 Dec 2023 07:59:38 -0600 Subject: [PATCH] Refactored logging to provide indented output --- bumpversion/indented_logger.py | 73 ++++++++++++++++++++++++++++++++++ bumpversion/ui.py | 9 ++++- tests/test_indented_logger.py | 71 +++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 bumpversion/indented_logger.py create mode 100644 tests/test_indented_logger.py diff --git a/bumpversion/indented_logger.py b/bumpversion/indented_logger.py new file mode 100644 index 00000000..3ff1b281 --- /dev/null +++ b/bumpversion/indented_logger.py @@ -0,0 +1,73 @@ +"""A logger adapter that adds an indent to the beginning of each message.""" +import logging +from contextvars import ContextVar +from typing import Any, MutableMapping, Optional, Tuple + +CURRENT_INDENT = ContextVar("current_indent", default=0) + + +class IndentedLoggerAdapter(logging.LoggerAdapter): + """ + Logger adapter that adds an indent to the beginning of each message. + + Parameters: + logger: The logger to adapt. + extra: Extra values to add to the logging context. + depth: The number of `indent_char` to generate for each indent level. + indent_char: The character or string to use for indenting. + reset: `True` if the indent level should be reset to zero. + """ + + def __init__( + self, + logger: logging.Logger, + extra: Optional[dict] = None, + depth: int = 2, + indent_char: str = " ", + reset: bool = False, + ): + super().__init__(logger, extra or {}) + self._depth = depth + self._indent_char = indent_char + if reset: + self.reset() + + def indent(self, amount: int = 1) -> None: + """ + Increase the indent level by `amount`. + """ + CURRENT_INDENT.set(CURRENT_INDENT.get() + amount) + + def dedent(self, amount: int = 1) -> None: + """ + Decrease the indent level by `amount`. + """ + CURRENT_INDENT.set(max(0, CURRENT_INDENT.get() - amount)) + + def reset(self) -> None: + """ + Reset the indent level to zero. + """ + CURRENT_INDENT.set(0) + + @property + def indent_str(self) -> str: + """ + The indent string. + """ + return (self._indent_char * self._depth) * CURRENT_INDENT.get() + + def process(self, msg: str, kwargs: Optional[MutableMapping[str, Any]]) -> Tuple[str, MutableMapping[str, Any]]: + """ + Process the message and add the indent. + + Args: + msg: The logging message. + kwargs: Keyword arguments passed to the logger. + + Returns: + A tuple containing the message and keyword arguments. + """ + msg = self.indent_str + msg + + return msg, kwargs diff --git a/bumpversion/ui.py b/bumpversion/ui.py index fa0278a3..a1a367e3 100644 --- a/bumpversion/ui.py +++ b/bumpversion/ui.py @@ -5,6 +5,8 @@ from click import UsageError, secho from rich.logging import RichHandler +from bumpversion.indented_logger import IndentedLoggerAdapter + logger = logging.getLogger("bumpversion") VERBOSITY = { @@ -14,6 +16,11 @@ } +def get_indented_logger(name: str) -> "IndentedLoggerAdapter": + """Get a logger with indentation.""" + return IndentedLoggerAdapter(logging.getLogger(name)) + + def setup_logging(verbose: int = 0) -> None: """Configure the logging.""" logging.basicConfig( @@ -26,7 +33,7 @@ def setup_logging(verbose: int = 0) -> None: ) ], ) - root_logger = logging.getLogger("") + root_logger = get_indented_logger("") root_logger.setLevel(VERBOSITY.get(verbose, logging.DEBUG)) diff --git a/tests/test_indented_logger.py b/tests/test_indented_logger.py new file mode 100644 index 00000000..404e37c5 --- /dev/null +++ b/tests/test_indented_logger.py @@ -0,0 +1,71 @@ +import pytest + +from bumpversion.indented_logger import IndentedLoggerAdapter +import logging + + +class TestIndentedLogger: + def test_does_not_indent_without_intent(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.debug("test debug") + logger.info("test info") + logger.warning("test warning") + logger.error("test error") + logger.critical("test critical") + + assert caplog.record_tuples == [ + ("root", 10, "test debug"), + ("root", 20, "test info"), + ("root", 30, "test warning"), + ("root", 40, "test error"), + ("root", 50, "test critical"), + ] + + def test_indents(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.info("test 1") + logger.indent(2) + logger.error("test %d", 2) + logger.indent() + logger.debug("test 3") + logger.warning("test 4") + logger.indent() + logger.critical("test 5") + logger.critical("test 6") + + assert caplog.record_tuples == [ + ("root", 20, "test 1"), + ("root", 40, " test 2"), + ("root", 10, " test 3"), + ("root", 30, " test 4"), + ("root", 50, " test 5"), + ("root", 50, " test 6"), + ] + + def test_dedents(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger(), reset=True) + logger.indent(3) + logger.info("test 1") + logger.dedent(2) + logger.error("test %d", 2) + logger.dedent() + logger.debug("test 3") + logger.warning("test 4") + + assert caplog.record_tuples == [ + ("root", 20, " test 1"), + ("root", 40, " test 2"), + ("root", 10, "test 3"), + ("root", 30, "test 4"), + ] + + def test_cant_dedent_below_zero(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.DEBUG) + logger = IndentedLoggerAdapter(logging.getLogger()) + logger.dedent(4) + logger.info("test 1") + + assert caplog.record_tuples == [("root", 20, "test 1")]