"""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