"""WizardKit: Log Functions""" # vim: sts=2 sw=2 ts=2 import atexit import logging import os import pathlib import shutil import time from typing import Union from wk import cfg from wk.io import non_clobber_path # STATIC VARIABLES if os.name == 'nt': # Example: "C:\WK\1955-11-05\WizardKit" DEFAULT_LOG_DIR = ( f'{os.environ.get("SYSTEMDRIVE", "C:")}/' f'{cfg.main.KIT_NAME_SHORT}/Logs/' ) else: # Example: "/home/tech/Logs" DEFAULT_LOG_DIR = f'{os.path.expanduser("~")}/Logs' DEFAULT_LOG_NAME = cfg.main.KIT_NAME_FULL # Functions def enable_debug_mode() -> None: """Configures logging for better debugging.""" root_logger = logging.getLogger() for handler in root_logger.handlers: formatter = logging.Formatter( datefmt=cfg.log.DEBUG['datefmt'], fmt=cfg.log.DEBUG['format'], ) handler.setFormatter(formatter) root_logger.setLevel('DEBUG') def format_log_path( log_dir: Union[None, pathlib.Path, str] = None, log_name: Union[None, str] = None, timestamp: bool = False, kit: bool = False, tool: bool = False, append: bool = False, ) -> pathlib.Path: """Format path based on args passed, returns pathlib.Path obj.""" log_path = pathlib.Path( f'{log_dir if log_dir else DEFAULT_LOG_DIR}/' f'{cfg.main.KIT_NAME_FULL+"/" if kit else ""}' f'{"Tools/" if tool else ""}' f'{log_name if log_name else DEFAULT_LOG_NAME}' f'{"_" if timestamp else ""}' f'{time.strftime("%Y-%m-%d_%H%M%S%z") if timestamp else ""}' '.log' ) log_path = log_path.resolve() # Avoid clobbering if not append: log_path = non_clobber_path(log_path) # Done return log_path def get_root_logger_path() -> pathlib.Path: """Get the log filepath from the root logger, returns pathlib.Path obj. NOTE: This will use the first handler baseFilename it finds (if any). """ root_logger = logging.getLogger() # Check handlers for handler in root_logger.handlers: if hasattr(handler, 'baseFilename'): log_file = handler.baseFilename # type: ignore[reportGeneralTypeIssues] return pathlib.Path(log_file).resolve() # No log file found raise RuntimeError('Log path not found.') def remove_empty_log(log_path: Union[None, pathlib.Path] = None) -> None: """Remove log if empty. NOTE: Under Windows an empty log is 2 bytes long. """ is_empty = False # Get log path if not log_path: log_path = get_root_logger_path() # Check if log is empty try: is_empty = log_path and log_path.exists() and log_path.stat().st_size <= 2 except (FileNotFoundError, AttributeError): # File doesn't exist or couldn't verify it's empty pass # Delete log if is_empty: log_path.unlink() def start(config: Union[dict[str, str], None] = None) -> None: """Configure and start logging using safe defaults.""" log_path = format_log_path(timestamp=os.name != 'nt') root_logger = logging.getLogger() # Safety checks if not config: config = cfg.log.DEFAULT if root_logger.hasHandlers(): raise UserWarning('Logging already started, results may be unpredictable.') # Create log_dir os.makedirs(log_path.parent, exist_ok=True) # Config logger logging.basicConfig(filename=log_path, **config) # Register shutdown to run atexit atexit.register(remove_empty_log) atexit.register(logging.shutdown) def update_log_path( dest_dir: Union[None, pathlib.Path, str] = None, dest_name: Union[None, str] = None, keep_history: bool = True, timestamp: bool = True, append: bool = False, ) -> None: """Moves current log file to new path and updates the root logger.""" root_logger = logging.getLogger() new_path = format_log_path(dest_dir, dest_name, timestamp=timestamp, append=append) old_handler = None old_path = get_root_logger_path() os.makedirs(new_path.parent, exist_ok=True) # Get current logging file handler for handler in root_logger.handlers: if isinstance(handler, logging.FileHandler): old_handler = handler break if not old_handler: raise RuntimeError('Logging FileHandler not found') # Copy original log to new location if keep_history: if new_path.exists(): raise FileExistsError(f'Refusing to clobber: {new_path}') shutil.copy(old_path, new_path) # Create new handler (preserving formatter settings) new_handler = logging.FileHandler(new_path, mode='a') new_handler.setFormatter(old_handler.formatter) # Remove old_handler root_logger.removeHandler(old_handler) old_handler.close() # Delete orignal log if needed if keep_history: remove_empty_log(old_path) else: old_path.unlink() # Add new handler root_logger.addHandler(new_handler) if __name__ == '__main__': print("This file is not meant to be called directly.")