Skip to content

Commit

Permalink
rename file, add default styles, add console handler
Browse files Browse the repository at this point in the history
  • Loading branch information
ITProKyle committed Mar 5, 2024
1 parent f6c253c commit d8e2c59
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 6 deletions.
7 changes: 5 additions & 2 deletions f_lib/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Logging utilities."""

from ._constants import DEFAULT_LOG_FORMAT, DEFAULT_LOG_FORMAT_VERBOSE
from ._highlighters import ExtendableHighlighter, HighlightTypedDict
from ._console_handler import ConsoleHandler
from ._constants import DEFAULT_LOG_FORMAT, DEFAULT_LOG_FORMAT_VERBOSE, DEFAULT_STYLES
from ._extendable_highlighter import ExtendableHighlighter, HighlightTypedDict
from ._log_level import LogLevel
from ._logger import Logger, LoggerSettings
from ._prefix_adaptor import PrefixAdaptor

__all__ = [
"DEFAULT_LOG_FORMAT",
"DEFAULT_LOG_FORMAT_VERBOSE",
"DEFAULT_STYLES",
"ConsoleHandler",
"ExtendableHighlighter",
"HighlightTypedDict",
"LogLevel",
Expand Down
68 changes: 68 additions & 0 deletions f_lib/logging/_console_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Custom console :class:`~rich.logging.RichHandler`."""

from __future__ import annotations

from typing import TYPE_CHECKING

from rich.logging import RichHandler
from rich.markup import escape
from rich.text import Text

if TYPE_CHECKING:
import logging

from rich.console import ConsoleRenderable


class ConsoleHandler(RichHandler):
"""Custom console :class:`~rich.logging.RichHandler`."""

def _determine_should_escape(self, record: logging.LogRecord) -> bool:
"""Determine if a log message should be passed to :function:`~rich.markup.escape`.
This can be overridden in subclasses for more control.
"""
return self._determine_use_markup(record) and getattr(record, "escape", False)

def _determine_use_markup(self, record: logging.LogRecord) -> bool:
"""Determine if markup should be used for a log record."""
return getattr(record, "markup", self.markup)

def render_message(self, record: logging.LogRecord, message: str) -> ConsoleRenderable:
"""Render message text in to Text.
Args:
record: logging Record.
message: String containing log message.
Returns:
ConsoleRenderable: Renderable to display log message.
"""
if self._determine_should_escape(record):
message = escape(message)
return super().render_message(*self._style_message(record, message))

def _style_message(
self,
record: logging.LogRecord,
message: str,
) -> tuple[logging.LogRecord, str]:
"""Apply style to the message."""
if not self._determine_use_markup(record):
return record, message
return record, Text.from_markup(message, style=record.levelname.lower()).markup

def get_level_text(self, record: logging.LogRecord) -> Text:
"""Get the level name from the record.
Args:
record: LogRecord instance.
Returns:
Text: A tuple of the style and level name.
"""
level_name = record.levelname
return Text.styled(f"[{level_name}]".ljust(9), f"logging.level.{level_name.lower()}")
30 changes: 30 additions & 0 deletions f_lib/logging/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,38 @@

from __future__ import annotations

from typing import TYPE_CHECKING

from rich.style import Style

if TYPE_CHECKING:
from collections.abc import Mapping

DEFAULT_LOG_FORMAT = "%(message)s"
"""Default log format."""

DEFAULT_LOG_FORMAT_VERBOSE = "%(name)s:%(message)s"
"""Default log format when a verbose log level is used."""

DEFAULT_STYLES: Mapping[str, Style] = {
"error": Style(color="red"),
"info": Style(),
"notice": Style(color="yellow"),
"success": Style(color="green"),
"warning": Style(color="orange1"),
"logging.level.critical": Style(color="red", bold=True, reverse=True),
"logging.level.debug": Style(color="green"),
"logging.level.error": Style(color="red", bold=True),
"logging.level.info": Style(color="blue"),
"logging.level.notice": Style(color="yellow"),
"logging.level.notset": Style(dim=True),
"logging.level.spam": Style(color="green", dim=True),
"logging.level.success": Style(color="green", bold=True),
"logging.level.verbose": Style(color="cyan"),
"logging.level.warning": Style(color="orange1"),
"logging.keyword": Style(bold=True, color="blue"),
"repr.brace": Style(),
"repr.call": Style(),
"repr.ellipsis": Style(dim=True, italic=True),
}
"""Default :class:`rich.style.Style` overrides."""
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Custom :class:`rich.highlighter.Highlighter`."""
"""Custom :class:`~rich.highlighter.Highlighter`."""

from __future__ import annotations

Expand Down Expand Up @@ -26,7 +26,7 @@ class HighlightTypedDict(TypedDict):


class ExtendableHighlighter(Highlighter):
"""Extendable :class:`rich.highlighter.Highlighter`."""
"""Extendable :class:`~rich.highlighter.Highlighter`."""

__slots__ = ()

Expand Down
104 changes: 104 additions & 0 deletions tests/unit/logging/test__console_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Test f_lib.logging._console_handler."""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest

from f_lib.logging._console_handler import ConsoleHandler

if TYPE_CHECKING:
from pytest_mock import MockerFixture

MODULE = "f_lib.logging._console_handler"


class TestConsoleHandler:
"""Test ConsoleHandler."""

@pytest.mark.parametrize(
("escape", "markup", "expected"),
[(False, False, False), (False, True, False), (True, False, False), (True, True, True)],
)
def test__determine_should_escape(
self, escape: bool, expected: bool, markup: bool, mocker: MockerFixture
) -> None:
"""Test _determine_should_escape."""
determine_use_markup = mocker.patch.object(
ConsoleHandler, "_determine_use_markup", return_value=markup
)
record = Mock(escape=escape)
assert ConsoleHandler()._determine_should_escape(record) is expected
determine_use_markup.assert_called_once_with(record)

@pytest.mark.parametrize(
("handler_markup", "record_markup", "expected"),
[
(False, False, False),
(False, True, True),
(True, False, False),
(True, True, True),
(False, None, False),
(True, None, True),
],
)
def test__determine_use_markup(
self, expected: bool, handler_markup: bool, record_markup: bool | None
) -> None:
"""Test _determine_use_markup."""
record = Mock(markup=record_markup)
if record_markup is None:
del record.markup
assert ConsoleHandler(markup=handler_markup)._determine_use_markup(record) is expected

def test__style_message(self, mocker: MockerFixture) -> None:
"""Test _style_message."""
record = Mock()
determine_use_markup = mocker.patch.object(
ConsoleHandler, "_determine_use_markup", return_value=False
)
from_markup = mocker.patch(f"{MODULE}.Text.from_markup")
assert ConsoleHandler()._style_message(record, "msg") == (record, "msg")
determine_use_markup.assert_called_once_with(record)
from_markup.assert_not_called()

def test__style_message_markup(self, mocker: MockerFixture) -> None:
"""Test _style_message with markup."""
record = Mock(levelname="INFO")
determine_use_markup = mocker.patch.object(
ConsoleHandler, "_determine_use_markup", return_value=True
)
from_markup = mocker.patch(
f"{MODULE}.Text.from_markup", return_value=Mock(markup="success")
)
assert ConsoleHandler()._style_message(record, "msg") == (record, "success")
determine_use_markup.assert_called_once_with(record)
from_markup.assert_called_once_with("msg", style="info")

def test_get_level_text(self, mocker: MockerFixture) -> None:
"""Test get_level_text."""
record = Mock(levelname="INFO")
styled = mocker.patch(f"{MODULE}.Text.styled", return_value="styled")
assert ConsoleHandler().get_level_text(record) == styled.return_value
styled.assert_called_once_with("[INFO] ", "logging.level.info")

def test_render_message(self, mocker: MockerFixture) -> None:
"""Test render_message."""
record = Mock(name="record")
determine_should_escape = mocker.patch.object(
ConsoleHandler, "_determine_should_escape", return_value=True
)
escape = mocker.patch(f"{MODULE}.escape", return_value="escaped")
render_message = mocker.patch(
f"{MODULE}.RichHandler.render_message", return_value="rendered"
)
style_message = mocker.patch.object(
ConsoleHandler, "_style_message", return_value=("style", "message")
)
assert ConsoleHandler().render_message(record, "message") == render_message.return_value
determine_should_escape.assert_called_once_with(record)
escape.assert_called_once_with("message")
style_message.assert_called_once_with(record, "escaped")
render_message.assert_called_once_with("style", "message")
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Test f_lib.logging._highlighters."""
"""Test f_lib.logging._extendable_highlighter."""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import Mock, call

from f_lib.logging._highlighters import ExtendableHighlighter
from f_lib.logging._extendable_highlighter import ExtendableHighlighter

if TYPE_CHECKING:
from pytest_mock import MockerFixture
Expand Down

0 comments on commit d8e2c59

Please sign in to comment.