"""Unified logging utility for automatic file and function name tracking.""" import sys import inspect import threading from pathlib import Path from SYS.rich_display import console_for _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 debug_inspect( obj, *, title: str | None = None, file=None, methods: bool = False, docs: bool = False, private: bool = False, dunder: bool = False, sort: bool = True, all: bool = False, value: bool = True, ) -> None: """Rich-inspect an object when debug logging is enabled. Uses the same stream / quiet-mode behavior as `debug()` and prepends a `[file.function]` prefix when debug is enabled. """ if not _DEBUG_ENABLED: return # Mirror debug() quiet-mode guard. 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 # Resolve destination stream. stream = get_thread_stream() if stream is not None: file = stream elif file is None: file = sys.stderr # Compute caller prefix (same as log()). prefix = None frame = inspect.currentframe() if frame is not None and frame.f_back is not None: caller_frame = frame.f_back try: file_name = Path(caller_frame.f_code.co_filename).stem func_name = caller_frame.f_code.co_name prefix = f"[{file_name}.{func_name}]" finally: del caller_frame if frame is not None: del frame # Render. from rich import inspect as rich_inspect console = console_for(file) # If the caller provides a title, treat it as authoritative. # Only fall back to the automatic [file.func] prefix when no title is supplied. effective_title = title if not effective_title and prefix: effective_title = prefix # Show full identifiers (hashes/paths) without Rich shortening. # Guard for older Rich versions which may not support max_* parameters. try: rich_inspect( obj, console=console, title=effective_title, methods=methods, docs=docs, private=private, dunder=dunder, sort=sort, all=all, value=value, max_string=100_000, max_length=100_000, ) except TypeError: rich_inspect( obj, console=console, title=effective_title, methods=methods, docs=docs, private=private, dunder=dunder, sort=sort, all=all, value=value, ) 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: file = kwargs.pop("file", sys.stdout) sep = kwargs.pop("sep", " ") end = kwargs.pop("end", "\n") console_for(file).print(*args, sep=sep, end=end) return caller_frame = frame.f_back if caller_frame is None: file = kwargs.pop("file", sys.stdout) sep = kwargs.pop("sep", " ") end = kwargs.pop("end", "\n") console_for(file).print(*args, sep=sep, end=end) 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 file = kwargs.pop("file", sys.stdout) sep = kwargs.pop("sep", " ") end = kwargs.pop("end", "\n") if add_prefix: prefix = f"[{file_name}.{func_name}]" console_for(file).print(prefix, *args, sep=sep, end=end) else: console_for(file).print(*args, sep=sep, end=end) finally: del frame del caller_frame