diff --git a/kedro/io/data_catalog.py b/kedro/io/data_catalog.py index f96d197f8d..3029fe9c04 100644 --- a/kedro/io/data_catalog.py +++ b/kedro/io/data_catalog.py @@ -29,7 +29,7 @@ ) from kedro.io.memory_dataset import MemoryDataset, _is_memory_dataset from kedro.io.shared_memory_dataset import SharedMemoryDataset -from kedro.utils import _format_rich, _has_rich_handler +from kedro.utils import _format_rich, _has_rich_handler, _has_only_rich_handlers if TYPE_CHECKING: from multiprocessing.managers import SyncManager @@ -266,7 +266,9 @@ def __init__( datasets, load_versions or {}, save_version ) - self._use_rich_markup = _has_rich_handler() + # Only enable rich markup if ALL handlers are RichHandlers to prevent markup leakage + # This avoids leaking rich markup tags to file handlers when mixed handlers are present + self._use_rich_markup = _has_only_rich_handlers() for ds_name in list(self._config_resolver.config): if ds_name in self._datasets: diff --git a/kedro/pipeline/node.py b/kedro/pipeline/node.py index 5e88134a8e..6ca663bf86 100644 --- a/kedro/pipeline/node.py +++ b/kedro/pipeline/node.py @@ -417,7 +417,7 @@ def run(self, inputs: dict[str, Any] | None = None) -> dict[str, Any]: keys are defined by the node outputs. """ - self._logger.info("Running node: %s", str(self)) + self._logger.info("Running node: %s", str(self), extra={"markup": False}) outputs = None @@ -446,7 +446,7 @@ def run(self, inputs: dict[str, Any] | None = None) -> dict[str, Any]: "Node %s failed with error: \n%s", str(self), str(exc), - extra={"markup": True}, + extra={"markup": False}, ) raise exc diff --git a/kedro/utils.py b/kedro/utils.py index ca4e941240..bc4b6c2cc8 100644 --- a/kedro/utils.py +++ b/kedro/utils.py @@ -160,6 +160,23 @@ def _has_rich_handler(logger: logging.Logger | None = None) -> bool: return any(isinstance(handler, RichHandler) for handler in logger.handlers) +def _has_only_rich_handlers(logger: logging.Logger | None = None) -> bool: + """Returns true if the logger has only RichHandler attached (no other handler types).""" + if not logger: + logger = logging.getLogger() # Use root by default + try: + from rich.logging import RichHandler + except ImportError: + # If Rich is not available, no RichHandlers exist + return False + + if not logger.handlers: + # If no handlers, return True (safe default) + return True + + return all(isinstance(handler, RichHandler) for handler in logger.handlers) + + def _format_rich(value: str, markup: str) -> str: """Format string with rich markup""" return f"[{markup}]{value}[/{markup}]"