"""Logging utilities for the ``movement`` package."""
import inspect
import json
import sys
import warnings
from datetime import datetime
from functools import wraps
from pathlib import Path
from loguru import logger as loguru_logger
DEFAULT_LOG_DIRECTORY = Path.home() / ".movement"
[docs]
class MovementLogger:
"""A custom logger extending the :mod:`loguru logger <loguru._logger>`."""
def __init__(self):
"""Initialize the logger with the :mod:`loguru._logger`."""
self.logger = loguru_logger
def _log_and_return_exception(self, log_method, message, *args, **kwargs):
"""Log the message and return an Exception if specified."""
log_method(message, *args, **kwargs)
if isinstance(message, Exception):
return message
[docs]
def error(self, message, *args, **kwargs):
"""Log error message and optionally return an Exception.
This method overrides loguru's
:meth:`logger.error() <loguru._logger.Logger.error>` to optionally
return an Exception if the message is an Exception.
"""
return self._log_and_return_exception(
self.logger.error, message, *args, **kwargs
)
[docs]
def exception(self, message, *args, **kwargs):
"""Log error message with traceback and optionally return an Exception.
This method overrides loguru's
:meth:`logger.exception() <loguru._logger.Logger.exception>` to
optionally return an Exception if the message is an Exception.
"""
return self._log_and_return_exception(
self.logger.exception, message, *args, **kwargs
)
def __getattr__(self, name):
"""Redirect attribute access to the loguru logger."""
return getattr(self.logger, name)
def __repr__(self):
"""Return the loguru logger's representation."""
return repr(self.logger)
logger = MovementLogger()
[docs]
def showwarning(message, category, filename, lineno, file=None, line=None):
"""Redirect alerts from the :mod:`warnings` module to the logger.
This function replaces :func:`logging.captureWarnings` which redirects
warnings issued by the :mod:`warnings` module to the logging system.
"""
formatted_message = warnings.formatwarning(
message, category, filename, lineno, line
)
logger.opt(depth=2).warning(formatted_message)
[docs]
def log_to_attrs(func):
"""Log the operation performed by the wrapped function.
This decorator appends log entries to the data's ``log``
attribute. The wrapped function must accept an :class:`xarray.Dataset`
or :class:`xarray.DataArray` as its first argument and return an
object of the same type.
"""
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
log_entry = {
"operation": func.__name__,
"datetime": str(datetime.now()),
}
# Extract argument names from the function signature
signature = inspect.signature(func)
bound_args = signature.bind(*args, **kwargs)
bound_args.apply_defaults()
# Store each argument
# (excluding the first, which is the Dataset/DataArray itself)
for param_name, value in list(bound_args.arguments.items())[1:]:
if param_name == "kwargs" and not value:
continue # Skip empty kwargs
log_entry[param_name] = repr(value)
if result is not None and hasattr(result, "attrs"):
log_str = result.attrs.get("log", "[]")
try:
log_list = json.loads(log_str)
except json.JSONDecodeError:
log_list = []
logger.warning(
f"Failed to decode existing log in attributes: {log_str}. "
f"Overwriting with an empty list."
)
log_list.append(log_entry)
result.attrs["log"] = json.dumps(log_list, indent=2)
return result
return wrapper