Skip to content

Safe decorators

provide.foundation.errors.safe_decorators

TODO: Add module docstring.

Functions

log_only_error_context

log_only_error_context(
    *,
    context_provider: (
        Callable[[], dict[str, Any]] | None
    ) = None,
    log_level: str = "debug",
    log_success: bool = False
) -> Callable[[F], F]

Safe decorator that only adds logging context without changing error behavior.

This decorator preserves the exact original error message and type while adding structured logging context. It never suppresses errors or changes their behavior.

Parameters:

Name Type Description Default
context_provider Callable[[], dict[str, Any]] | None

Function that provides additional logging context.

None
log_level str

Level for operation logging ('debug', 'trace', etc.)

'debug'
log_success bool

Whether to log successful operations.

False

Returns:

Type Description
Callable[[F], F]

Decorated function that preserves all original error behavior.

Examples:

>>> @log_only_error_context(
...     context_provider=lambda: {"operation": "detect_launcher_type"},
...     log_level="trace"
... )
... def detect_launcher_type(self, path):
...     # Original error messages preserved exactly
...     return self._internal_detect(path)
Source code in provide/foundation/errors/safe_decorators.py
def log_only_error_context(
    *,
    context_provider: Callable[[], dict[str, Any]] | None = None,
    log_level: str = "debug",
    log_success: bool = False,
) -> Callable[[F], F]:
    """Safe decorator that only adds logging context without changing error behavior.

    This decorator preserves the exact original error message and type while adding
    structured logging context. It never suppresses errors or changes their behavior.

    Args:
        context_provider: Function that provides additional logging context.
        log_level: Level for operation logging ('debug', 'trace', etc.)
        log_success: Whether to log successful operations.

    Returns:
        Decorated function that preserves all original error behavior.

    Examples:
        >>> @log_only_error_context(
        ...     context_provider=lambda: {"operation": "detect_launcher_type"},
        ...     log_level="trace"
        ... )
        ... def detect_launcher_type(self, path):
        ...     # Original error messages preserved exactly
        ...     return self._internal_detect(path)

    """

    def decorator(func: F) -> F:
        if inspect.iscoroutinefunction(func):

            @functools.wraps(func)
            async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
                context = context_provider() if context_provider else {}
                logger = get_foundation_logger()

                _log_function_entry(logger, func, log_level, context)

                try:
                    result = await func(*args, **kwargs)

                    if log_success:
                        _log_function_success(logger, func, log_level, context)

                    return result

                except Exception as e:
                    _log_function_error(logger, func, e, context)
                    raise

            return async_wrapper  # type: ignore

        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            context = context_provider() if context_provider else {}
            logger = get_foundation_logger()

            _log_function_entry(logger, func, log_level, context)

            try:
                result = func(*args, **kwargs)

                if log_success:
                    _log_function_success(logger, func, log_level, context)

                return result

            except Exception as e:
                _log_function_error(logger, func, e, context)
                raise

        return wrapper  # type: ignore

    return decorator