Coverage for src/pdfbaker/logging.py: 93%

70 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-20 14:42 +1200

1"""Logging mixin for pdfbaker classes.""" 

2 

3import logging 

4import sys 

5from typing import Any 

6 

7TRACE = 5 

8logging.addLevelName(TRACE, "TRACE") 

9 

10__all__ = ["LoggingMixin", "setup_logging", "truncate_strings"] 

11 

12 

13class LoggingMixin: 

14 """Mixin providing consistent logging functionality across pdfbaker classes.""" 

15 

16 def __init__(self) -> None: 

17 """Initialize logger for the class.""" 

18 self.logger = logging.getLogger(self.__class__.__module__) 

19 

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) 

23 

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 ) 

31 

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) 

35 

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) 

39 

40 def log_debug(self, msg: str, *args: Any, **kwargs: Any) -> None: 

41 """Log a debug message.""" 

42 self.logger.debug(msg, *args, **kwargs) 

43 

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) 

47 

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) 

51 

52 def log_info(self, msg: str, *args: Any, **kwargs: Any) -> None: 

53 """Log an info message.""" 

54 self.logger.info(msg, *args, **kwargs) 

55 

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) 

59 

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) 

63 

64 def log_warning(self, msg: str, *args: Any, **kwargs: Any) -> None: 

65 """Log a warning message.""" 

66 self.logger.warning(msg, *args, **kwargs) 

67 

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) 

71 

72 def log_critical(self, msg: str, *args: Any, **kwargs: Any) -> None: 

73 """Log a critical message.""" 

74 self.logger.critical(msg, *args, **kwargs) 

75 

76 

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") 

82 

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) 

88 

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) 

93 

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) 

102 

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) 

111 

112 

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