Source code for spacr.logger

"""Legacy stdout logger compatibility helpers."""

from __future__ import annotations

import functools
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any

from spacr.logging_util import _quicken

_LOGGER_NAME = "spacr"
_HANDLER_KIND = "_spacr_handler_kind"


[docs] def configure_logger( name: str = _LOGGER_NAME, log_file_name: str = "spacr.log", level: int = logging.INFO, stream: bool = False, ) -> logging.Logger: """Return a named logger backed by a rotating file in the user's home. Repeated calls reuse handlers installed here, update their levels, and may add the optional stream handler later. Handlers installed by callers are preserved; a bootstrap :class:`logging.NullHandler` does not count as file logging. :param name: Logger name to fetch or create. Default ``"spacr"``. :param log_file_name: file name under the user's home for the first file handler installed for ``name``. Later calls keep that file. :param level: Logging level applied to the logger and its handlers. :param stream: When True, also attach a stderr stream handler. :returns: Configured ``logging.Logger`` instance. """ logger = logging.getLogger(name) logger.setLevel(level) logger.propagate = False file_handlers = [ handler for handler in logger.handlers if getattr(handler, _HANDLER_KIND, None) == "file" ] if not file_handlers: log_path = Path.home() / log_file_name file_handler = _quicken(RotatingFileHandler( log_path, maxBytes=2_000_000, backupCount=3, encoding="utf-8", )) setattr(file_handler, _HANDLER_KIND, "file") file_handler.setFormatter( logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) ) logger.addHandler(file_handler) file_handlers.append(file_handler) stream_handlers = [ handler for handler in logger.handlers if getattr(handler, _HANDLER_KIND, None) == "stream" ] if stream and not stream_handlers: stream_handler = logging.StreamHandler() setattr(stream_handler, _HANDLER_KIND, "stream") stream_handler.setFormatter( logging.Formatter("%(levelname)s - %(message)s") ) logger.addHandler(stream_handler) stream_handlers.append(stream_handler) for handler in (*file_handlers, *stream_handlers): handler.setLevel(level) return logger
logger = logging.getLogger(_LOGGER_NAME) logger.addHandler(logging.NullHandler()) def _safe_repr(value: Any, max_length: int = 200) -> str: """Return a truncated ``repr`` that never raises. :param value: object to represent for a log record. :param max_length: largest returned character count. :returns: ordinary or truncated representation, or a type-naming fallback when ``repr`` raises. """ try: text = repr(value) except Exception: text = f"<unreprable {type(value).__name__}>" if len(text) > max_length: return text[: max_length - 3] + "..." return text
[docs] def log_function_call(func): """Decorator that logs call arguments, return value, and exceptions. :param func: Callable to wrap. :returns: Wrapped callable that emits INFO-level trace entries. """ @functools.wraps(func) def wrapper(*args, **kwargs): """Emit call/return/exception log lines around the wrapped call.""" active_logger = configure_logger(name=func.__module__) args_repr = ", ".join(_safe_repr(arg) for arg in args) kwargs_repr = ", ".join( f"{key}={_safe_repr(value)}" for key, value in kwargs.items() ) signature = ", ".join(part for part in (args_repr, kwargs_repr) if part) active_logger.info("Calling %s(%s)", func.__name__, signature) try: result = func(*args, **kwargs) active_logger.info("%s returned %s", func.__name__, _safe_repr(result)) return result except Exception: active_logger.exception("Exception occurred in %s", func.__name__) raise return wrapper