From ae483b306fa97b0b06a4494992515ddb1ab31f8c Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 8 Apr 2026 20:26:26 +0200 Subject: [PATCH] feat(log): add SimpleLogger for terminal logging without external backends --- src/experiment_logger/__init__.py | 2 + src/experiment_logger/simple_logger.py | 83 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/experiment_logger/simple_logger.py diff --git a/src/experiment_logger/__init__.py b/src/experiment_logger/__init__.py index 93b1de3..64d4be4 100644 --- a/src/experiment_logger/__init__.py +++ b/src/experiment_logger/__init__.py @@ -6,10 +6,12 @@ This package provides a unified interface for logging to multiple backends from experiment_logger.config_utils import load_yaml_config, merge_config_with_cli from experiment_logger.unified_logger import UnifiedLogger, get_logger +from experiment_logger.simple_logger import SimpleLogger from experiment_logger.wandb_utils import finish_wandb, init_wandb __all__ = [ "UnifiedLogger", + "SimpleLogger", "get_logger", "init_wandb", "finish_wandb", diff --git a/src/experiment_logger/simple_logger.py b/src/experiment_logger/simple_logger.py new file mode 100644 index 0000000..0ed1ab5 --- /dev/null +++ b/src/experiment_logger/simple_logger.py @@ -0,0 +1,83 @@ +"""Simple terminal logger for running without external backends. + +This is used for standalone package usage where WandB or TensorBoard are not desired. +It preserves the same API as UnifiedLogger but simply prints to stdout. +""" + +import logging +from typing import Any, Dict, Optional + + +class SimpleLogger: + """Simple logger that implements the UnifiedLogger interface via print statements.""" + + def __init__( + self, + run_name: str = "simple_run", + config: Optional[Dict[str, Any]] = None, + project_name: str = "none", + entity: Optional[str] = None, + base_dir: str = "runs", + use_wandb: bool = False, + save_code: bool = False, + log_level: int = logging.INFO, + _set_as_global: bool = False, + ): + self.is_interactive = True + self.run_name = run_name + self.config = config or {} + print(f"[INIT] SimpleLogger initialized for run: {run_name}") + + def set_level(self, level: int): + pass + + def log_non_interactive(self, msg: str, *args, **kwargs): + """In SimpleLogger, we just print everything as we assume interactive use.""" + self.info(msg, *args, **kwargs) + + def progress_bar(self, iterable=None, *args, **kwargs): + """Standard tqdm wrapper that falls back to range if tqdm is missing.""" + try: + import tqdm + + return tqdm.tqdm(iterable, *args, **kwargs) + except ImportError: + return iterable + + def info(self, msg: str, *args, **kwargs): + print(f"[INFO] {msg}") + + def warning(self, msg: str, *args, **kwargs): + print(f"[WARNING] {msg}") + + def error(self, msg: str, *args, **kwargs): + print(f"[ERROR] {msg}") + + def debug(self, msg: str, *args, **kwargs): + print(f"[DEBUG] {msg}") + + def log(self, metrics: Dict[str, Any], step: Optional[int] = None, commit: bool = True): + step_str = f"Step {step}" if step is not None else "Log" + metric_str = ", ".join(f"{k}: {v}" for k, v in metrics.items()) + print(f"[{step_str}] {metric_str}") + + def save_checkpoint( + self, + params: Any, + step: int, + prefix: str = "checkpoint", + metadata: Optional[Dict[str, Any]] = None, + ): + print(f"[SAVE] Checkpoint '{prefix}' would be saved at step {step} (SimpleLogger: No-Op)") + + def save_final_model(self, params: Any, metadata: Optional[Dict[str, Any]] = None): + print("[SAVE] Final model would be saved (SimpleLogger: No-Op)") + + def finish(self): + print(f"[FINISH] SimpleLogger finished for run: {self.run_name}") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.finish()