Coverage for src/pdfbaker/logging.py: 93%
70 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-20 04:55 +1200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-20 04:55 +1200
1"""Logging mixin for pdfbaker classes."""
3import logging
4import sys
5from typing import Any
7TRACE = 5
8logging.addLevelName(TRACE, "TRACE")
10__all__ = ["LoggingMixin", "setup_logging", "truncate_strings"]
13class LoggingMixin:
14 """Mixin providing consistent logging functionality across pdfbaker classes."""
16 def __init__(self) -> None:
17 """Initialize logger for the class."""
18 self.logger = logging.getLogger(self.__class__.__module__)
20 def log_trace(self, msg: str, *args: Any, **kwargs: Any) -> None:
21 """Log a trace message (more detailed than debug)."""
22 self.logger.log(TRACE, msg, *args, **kwargs)
24 def log_trace_preview(
25 self, msg: str, *args: Any, max_chars: int = 500, **kwargs: Any
26 ) -> None:
27 """Log a trace preview of a potentially large message, truncating if needed."""
28 self.logger.log(
29 TRACE, truncate_strings(msg, max_chars=max_chars), *args, **kwargs
30 )
32 def log_trace_section(self, msg: str, *args: Any, **kwargs: Any) -> None:
33 """Log a trace message as a main section header."""
34 self.logger.log(TRACE, f"──── {msg} ────", *args, **kwargs)
36 def log_trace_subsection(self, msg: str, *args: Any, **kwargs: Any) -> None:
37 """Log a trace message as a subsection header."""
38 self.logger.log(TRACE, f" ── {msg} ──", *args, **kwargs)
40 def log_debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
41 """Log a debug message."""
42 self.logger.debug(msg, *args, **kwargs)
44 def log_debug_section(self, msg: str, *args: Any, **kwargs: Any) -> None:
45 """Log a debug message as a main section header."""
46 self.logger.debug(f"──── {msg} ────", *args, **kwargs)
48 def log_debug_subsection(self, msg: str, *args: Any, **kwargs: Any) -> None:
49 """Log a debug message as a subsection header."""
50 self.logger.debug(f" ── {msg} ──", *args, **kwargs)
52 def log_info(self, msg: str, *args: Any, **kwargs: Any) -> None:
53 """Log an info message."""
54 self.logger.info(msg, *args, **kwargs)
56 def log_info_section(self, msg: str, *args: Any, **kwargs: Any) -> None:
57 """Log an info message as a main section header."""
58 self.logger.info(f"──── {msg} ────", *args, **kwargs)
60 def log_info_subsection(self, msg: str, *args: Any, **kwargs: Any) -> None:
61 """Log an info message as a subsection header."""
62 self.logger.info(f" ── {msg} ──", *args, **kwargs)
64 def log_warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
65 """Log a warning message."""
66 self.logger.warning(msg, *args, **kwargs)
68 def log_error(self, msg: str, *args: Any, **kwargs: Any) -> None:
69 """Log an error message."""
70 self.logger.error(f"**** {msg} ****", *args, **kwargs)
72 def log_critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
73 """Log a critical message."""
74 self.logger.critical(msg, *args, **kwargs)
77def setup_logging(quiet=False, trace=False, verbose=False) -> None:
78 """Set up logging for the application."""
79 logger = logging.getLogger()
80 logger.setLevel(logging.INFO)
81 formatter = logging.Formatter("%(levelname)s: %(message)s")
83 # stdout handler for TRACE/DEBUG/INFO
84 stdout_handler = logging.StreamHandler(sys.stdout)
85 stdout_handler.setFormatter(formatter)
86 stdout_handler.setLevel(TRACE)
87 stdout_handler.addFilter(lambda record: record.levelno < logging.WARNING)
89 # stderr handler for WARNING and above
90 stderr_handler = logging.StreamHandler(sys.stderr)
91 stderr_handler.setFormatter(formatter)
92 stderr_handler.setLevel(logging.WARNING)
94 # Remove existing console handlers, add ours
95 for handler in logger.handlers[:]:
96 if isinstance(handler, logging.StreamHandler) and not isinstance(
97 handler, logging.FileHandler
98 ):
99 logger.removeHandler(handler)
100 logger.addHandler(stdout_handler)
101 logger.addHandler(stderr_handler)
103 if quiet:
104 logger.setLevel(logging.ERROR)
105 elif trace:
106 logger.setLevel(TRACE)
107 elif verbose:
108 logger.setLevel(logging.DEBUG)
109 else:
110 logger.setLevel(logging.INFO)
113def truncate_strings(obj, max_chars: int) -> Any:
114 """Recursively truncate strings in nested structures."""
115 if isinstance(obj, str):
116 return obj if len(obj) <= max_chars else obj[:max_chars] + "…"
117 if isinstance(obj, dict):
118 return {
119 truncate_strings(k, max_chars): truncate_strings(v, max_chars)
120 for k, v in obj.items()
121 }
122 if isinstance(obj, list):
123 return [truncate_strings(item, max_chars) for item in obj]
124 if isinstance(obj, tuple):
125 return tuple(truncate_strings(item, max_chars) for item in obj)
126 if isinstance(obj, set):
127 return {truncate_strings(item, max_chars) for item in obj}
128 return obj