190 lines
5.3 KiB
Python
190 lines
5.3 KiB
Python
"""Unified logging utility for automatic file and function name tracking."""
|
|
|
|
import sys
|
|
import inspect
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
from 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
|
|
|
|
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
|