Source code for python_utils.logger

"""
This module provides a base class `LoggerBase` and a derived class `Logged`
for adding logging capabilities to classes. The `LoggerBase` class expects
a `logger` attribute to be a `logging.Logger` or compatible instance and
provides methods for logging at various levels. The `Logged` class
automatically adds a named logger to the class.

Classes:
    LoggerBase:
        A base class that adds logging utilities to a class.
    Logged:
        A derived class that automatically adds a named logger to a class.

Example:
    >>> class MyClass(Logged):
    ...     def __init__(self):
    ...         Logged.__init__(self)

    >>> my_class = MyClass()
    >>> my_class.debug('debug')
    >>> my_class.info('info')
    >>> my_class.warning('warning')
    >>> my_class.error('error')
    >>> my_class.exception('exception')
    >>> my_class.log(0, 'log')
"""

from __future__ import annotations

import abc
import collections.abc
import logging
import types
import typing

if typing.TYPE_CHECKING:
    import typing_extensions

from . import decorators

__all__ = ['Logged']

#: Accepted values for the logging ``exc_info`` parameter: a bool, an
#: ``(type, value, traceback)`` triple, a bare exception instance, or ``None``.
# From the logging typeshed, converted to be compatible with Python 3.8
# https://github.com/python/typeshed/blob/main/stdlib/logging/__init__.pyi
_ExcInfoType: typing.TypeAlias = (
    bool
    | tuple[
        type[BaseException],
        BaseException,
        types.TracebackType | None,
    ]
    | tuple[None, None, None]
    | BaseException
    | None
)
#: Parameter specification capturing a wrapped logger method's arguments.
_P = typing.ParamSpec('_P')
#: Covariant return-type variable for wrapped logger methods.
_T = typing.TypeVar('_T', covariant=True)


class LoggerProtocol(typing.Protocol):
    """Structural type for any ``logging.Logger``-compatible logger.

    Objects that provide these methods (such as a ``logging.Logger`` or a
    ``loguru`` logger) can be used as the ``logger`` attribute of a
    ``LoggerBase`` subclass. Only the shape matters; no inheritance required.
    """

    def debug(
        self,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``DEBUG`` level."""

    def info(
        self,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``INFO`` level."""

    def warning(
        self,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``WARNING`` level."""

    def error(
        self,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``ERROR`` level."""

    def critical(
        self,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``CRITICAL`` level."""

    def exception(
        self,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``ERROR`` level, including exception information."""

    def log(
        self,
        level: int,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at the integer ``level``."""


class LoggerBase(abc.ABC):
    """Class which automatically adds logging utilities to your class when
    inheriting. Expects `logger` to be a logging.Logger or compatible instance.

    Adds easy access to debug, info, warning, error, exception and log methods

    >>> class MyClass(LoggerBase):
    ...     logger = logging.getLogger(__name__)
    ...
    ...     def __init__(self):
    ...         Logged.__init__(self)

    >>> my_class = MyClass()
    >>> my_class.debug('debug')
    >>> my_class.info('info')
    >>> my_class.warning('warning')
    >>> my_class.error('error')
    >>> my_class.exception('exception')
    >>> my_class.log(0, 'log')
    """

    # I've tried using a protocol to properly type the logger but it gave all
    # sorts of issues with mypy so we're using the lazy solution for now. The
    # actual classes define the correct type anyway
    logger: typing.Any
    # logger: LoggerProtocol

    @classmethod
    def __get_name(  # pyright: ignore[reportUnusedFunction]
        cls, *name_parts: str
    ) -> str:
        """Join the non-empty, stripped ``name_parts`` into a dotted name."""
        return '.'.join(n.strip() for n in name_parts if n.strip())

    @decorators.wraps_classmethod(logging.Logger.debug)
    @classmethod
    def debug(
        cls,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``DEBUG`` level on the class logger."""
        return cls.logger.debug(  # type: ignore[no-any-return]
            msg,
            *args,
            exc_info=exc_info,
            stack_info=stack_info,
            stacklevel=stacklevel,
            extra=extra,
        )

    @decorators.wraps_classmethod(logging.Logger.info)
    @classmethod
    def info(
        cls,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``INFO`` level on the class logger."""
        return cls.logger.info(  # type: ignore[no-any-return]
            msg,
            *args,
            exc_info=exc_info,
            stack_info=stack_info,
            stacklevel=stacklevel,
            extra=extra,
        )

    @decorators.wraps_classmethod(logging.Logger.warning)
    @classmethod
    def warning(
        cls,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``WARNING`` level on the class logger."""
        return cls.logger.warning(  # type: ignore[no-any-return]
            msg,
            *args,
            exc_info=exc_info,
            stack_info=stack_info,
            stacklevel=stacklevel,
            extra=extra,
        )

    @decorators.wraps_classmethod(logging.Logger.error)
    @classmethod
    def error(
        cls,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``ERROR`` level on the class logger."""
        return cls.logger.error(  # type: ignore[no-any-return]
            msg,
            *args,
            exc_info=exc_info,
            stack_info=stack_info,
            stacklevel=stacklevel,
            extra=extra,
        )

    @decorators.wraps_classmethod(logging.Logger.critical)
    @classmethod
    def critical(
        cls,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``CRITICAL`` level on the class logger."""
        return cls.logger.critical(  # type: ignore[no-any-return]
            msg,
            *args,
            exc_info=exc_info,
            stack_info=stack_info,
            stacklevel=stacklevel,
            extra=extra,
        )

    @decorators.wraps_classmethod(logging.Logger.exception)
    @classmethod
    def exception(
        cls,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at ``ERROR`` level with exception info attached."""
        return cls.logger.exception(  # type: ignore[no-any-return]
            msg,
            *args,
            exc_info=exc_info,
            stack_info=stack_info,
            stacklevel=stacklevel,
            extra=extra,
        )

    @decorators.wraps_classmethod(logging.Logger.log)
    @classmethod
    def log(
        cls,
        level: int,
        msg: object,
        *args: object,
        exc_info: _ExcInfoType = None,
        stack_info: bool = False,
        stacklevel: int = 1,
        extra: collections.abc.Mapping[str, object] | None = None,
    ) -> None:
        """Log ``msg`` at the integer ``level`` on the class logger."""
        return cls.logger.log(  # type: ignore[no-any-return]
            level,
            msg,
            *args,
            exc_info=exc_info,
            stack_info=stack_info,
            stacklevel=stacklevel,
            extra=extra,
        )


[docs] class Logged(LoggerBase): """Class which automatically adds a named logger to your class when inheriting. Adds easy access to debug, info, warning, error, exception and log methods >>> class MyClass(Logged): ... def __init__(self): ... Logged.__init__(self) >>> my_class = MyClass() >>> my_class.debug('debug') >>> my_class.info('info') >>> my_class.warning('warning') >>> my_class.error('error') >>> my_class.exception('exception') >>> my_class.log(0, 'log') >>> my_class._Logged__get_name('spam') 'spam' """ logger: logging.Logger # pragma: no cover @classmethod def __get_name(cls, *name_parts: str) -> str: """Build the dotted logger name via ``LoggerBase``'s joiner.""" return typing.cast( str, LoggerBase._LoggerBase__get_name(*name_parts), # type: ignore[attr-defined] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue] ) def __new__( cls, *args: typing.Any, **kwargs: typing.Any ) -> typing_extensions.Self: """ Create a new instance of the class and initialize the logger. The logger is named using the module and class name. Args: *args: Variable length argument list. **kwargs: Arbitrary keyword arguments. Returns: An instance of the class. """ cls.logger = logging.getLogger( cls.__get_name(cls.__module__, cls.__name__) ) return super().__new__(cls)