"""Unified logging utility for automatic file and function name tracking.""" import sys import inspect import threading from pathlib import Path _DEBUG_ENABLED = False _thread_local = threading.local() def set_thread_stream(stream): """Set a custom output stream for the current thread.""" _thread_local.stream = stream def get_thread_stream(): """Get the custom output stream for the current thread, if any.""" return getattr(_thread_local, 'stream', None) def set_debug(enabled: bool) -> None: """Enable or disable debug logging.""" global _DEBUG_ENABLED _DEBUG_ENABLED = enabled def is_debug_enabled() -> bool: """Check if debug logging is enabled.""" return _DEBUG_ENABLED def debug(*args, **kwargs) -> None: """Print debug message if debug logging is enabled. Automatically prepends [filename.function_name] to all output. """ if not _DEBUG_ENABLED: return # Check if stderr has been redirected to /dev/null (quiet mode) # If so, skip output to avoid queuing in background worker's capture try: stderr_name = getattr(sys.stderr, 'name', '') if 'nul' in str(stderr_name).lower() or '/dev/null' in str(stderr_name): return except Exception: pass # Check for thread-local stream first stream = get_thread_stream() if stream: kwargs['file'] = stream # Set default to stderr for debug messages elif 'file' not in kwargs: kwargs['file'] = sys.stderr # Prepend DEBUG label args = ("DEBUG:", *args) # Use the same logic as log() log(*args, **kwargs) def log(*args, **kwargs) -> None: """Print with automatic file.function prefix. Automatically prepends [filename.function_name] to all output. Defaults to stdout if not specified. Example: log("Upload started") # Output: [add_file.run] Upload started """ # When debug is disabled, suppress the automatic prefix for cleaner user-facing output. add_prefix = _DEBUG_ENABLED # Get the calling frame frame = inspect.currentframe() if frame is None: print(*args, **kwargs) return caller_frame = frame.f_back if caller_frame is None: print(*args, **kwargs) return try: # Get file name without extension file_name = Path(caller_frame.f_code.co_filename).stem # Get function name func_name = caller_frame.f_code.co_name # Check for thread-local stream first stream = get_thread_stream() if stream: kwargs['file'] = stream # Set default to stdout if not specified elif 'file' not in kwargs: kwargs['file'] = sys.stdout if add_prefix: prefix = f"[{file_name}.{func_name}]" print(prefix, *args, **kwargs) else: print(*args, **kwargs) finally: del frame del caller_frame