"""Custom logging adapter for Houdini."""
# Future
from __future__ import annotations
# Standard Library
import logging
from functools import wraps
from typing import Any, Callable, Self
# Houdini Logging Tools
from houdini_logging_tools.mappings import LOGGING_TO_SEVERITY_MAP
# Houdini
import hou
# Globals
# Call kwargs that should be moved into the extra data passed to process().
_KWARGS_TO_EXTRA_KEYS = (
"node",
"dialog",
"status_bar",
"title",
)
# Classes
[docs]
class HoudiniLoggerAdapter(logging.LoggerAdapter):
"""Custom LoggerAdapter for Houdini.
This adapter allows automated addition of node paths and log display in dialogs,
status bar, etc. Also allows for automated notification.
Args:
base_logger:
The base package logger.
node:
Optional node for prefixing messages with the path.
dialog:
Whether to always use the dialog option.
status_bar:
Whether to always use the status bar option.
extra:
Extra args to use to generate log messages.
"""
def __init__(
self,
base_logger: logging.Logger,
node: hou.Node = None,
*,
dialog: bool = False,
status_bar: bool = False,
extra: dict | None = None,
) -> None:
extra = extra or {}
super().__init__(base_logger, extra)
self._dialog = dialog
self._node = node
self._status_bar = status_bar
def __new__(
cls: type[Self],
*args: Any,
**kwargs: Any,
) -> Self: # pragma: no cover
"""Overridden __new__ that will wrap logging methods with custom function."""
inst = super().__new__(cls)
# We want to wrap various log calls to process args and set severities.
for key, severity in LOGGING_TO_SEVERITY_MAP.items():
if hasattr(inst, key):
attr = getattr(inst, key)
if callable(attr):
wrapped = _wrap_logger(attr, severity)
setattr(inst, key, wrapped)
return inst
# Class Methods
[docs]
@classmethod
def from_name(
cls: type[Self],
name: str,
node: hou.Node = None,
*,
dialog: bool = False,
status_bar: bool = False,
extra: dict | None = None,
) -> HoudiniLoggerAdapter:
"""Create a new HoudiniLoggerAdapter from a name.
This is a convenience function around the following code:
>>> base_log = logging.getLogger(name)
>>> logger = HoudiniLoggerAdapter(base_log)
Args:
name:
The name of the logger to use.
node:
Optional node for prefixing messages with the path.
dialog:
Whether to always use the dialog option.
status_bar:
Whether to always use the status bar option.
extra:
Extra args to use to generate log messages.
Returns:
An adapter wrapping a logger of the passed name.
"""
# Create a base logger
base_logger = logging.getLogger(name)
return cls(base_logger, node=node, dialog=dialog, status_bar=status_bar, extra=extra)
# Properties
@property
def dialog(self) -> bool:
"""Whether the dialog will be displayed."""
return self._dialog
@dialog.setter
def dialog(self, dialog: bool) -> None:
self._dialog = dialog
# --------------------------------------------------------------------------
@property
def node(self) -> hou.Node | None:
"""A node the logger is associated with."""
return self._node
@node.setter
def node(self, node: hou.Node | None) -> None:
self._node = node
# --------------------------------------------------------------------------
@property
def status_bar(self) -> bool:
"""Whether the message will be logged to the status bar."""
return self._status_bar
@status_bar.setter
def status_bar(self, status_bar: bool) -> None:
self._status_bar = status_bar
# Methods
[docs]
def process(self, msg: str, kwargs: Any) -> tuple[str, Any]:
"""Override function to handle custom logic.
This will possibly insert a node path or to display a dialog with the log
message before being passed to regular logging output.
Args:
msg:
The log message.
kwargs:
kwargs dict.
Returns:
The message and updated kwargs.
"""
extra: dict = self.extra # type: ignore
if "extra" in kwargs:
extra.update(kwargs["extra"])
node = extra.pop("node", self.node)
# Prepend the message with the node path.
if node is not None:
msg = f"{node.path()} - {msg}"
dialog = extra.pop("dialog", self.dialog)
status_bar = extra.pop("status_bar", self.status_bar)
if hou.isUIAvailable():
# Copy of the message for our display.
houdini_message = msg
# If we have message args we need to format the message with them.
if "message_args" in extra:
houdini_message %= extra["message_args"]
severity = extra.pop("severity", hou.severityType.Message)
# Display the message as a popup.
if dialog:
title = extra.pop("title", None)
hou.ui.displayMessage(houdini_message, severity=severity, title=title)
if status_bar:
hou.ui.setStatusMessage(houdini_message, severity=severity)
kwargs["extra"] = extra
return msg, kwargs
# Non-Public Functions
def _wrap_logger(func: Callable, severity: hou.severityType) -> Callable:
"""Function which wraps a logger method with custom code.
Args:
func:
The callable to wrap.
severity:
The corresponding hou.severityType value.
Returns:
The wrapped function.
"""
@wraps(func)
def func_wrapper(*args: Any, **kwargs: Any) -> Any:
# Get the extra dictionary, or an empty one if it doesn't exist.
extra = kwargs.setdefault("extra", {})
# Set the severity to our passed in value.
extra["severity"] = severity
for key in _KWARGS_TO_EXTRA_KEYS:
if key in kwargs:
extra[key] = kwargs.pop(key)
# If there is more than one arg, we want to pass them as extra data so that
# we can use it to format the message for extra outputs.
if len(args) > 1:
extra["message_args"] = args[1:]
if "stacklevel" not in kwargs:
# Set stacklevel=4 so that the module/file/line reporting will represent
# the calling point and not the function call inside the adapter.
kwargs["stacklevel"] = 4
return func(*args, **kwargs)
return func_wrapper