Coverage for little_loops / logger.py: 42%

60 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-18 16:20 -0500

1"""Logging utilities for little-loops. 

2 

3Provides colorized console output with timestamps for automation tools. 

4""" 

5 

6from __future__ import annotations 

7 

8import os 

9import sys 

10from datetime import datetime 

11from typing import TYPE_CHECKING 

12 

13if TYPE_CHECKING: 

14 from little_loops.config import CliColorsConfig 

15 

16 

17class Logger: 

18 """Simple logger with timestamps and colors. 

19 

20 Provides info, success, warning, and error logging methods with 

21 optional colorized output for terminal display. 

22 

23 Attributes: 

24 verbose: Whether to output messages (False silences all output) 

25 use_color: Whether to use ANSI color codes 

26 """ 

27 

28 # ANSI color codes (defaults; may be overridden per-instance via colors param) 

29 CYAN = "\033[36m" 

30 GREEN = "\033[32m" 

31 YELLOW = "\033[33m" 

32 ORANGE = "\033[38;5;208m" # 256-color orange (replaces red for errors) 

33 RED = "\033[38;5;208m" # alias kept for backwards compatibility 

34 MAGENTA = "\033[35m" 

35 GRAY = "\033[90m" 

36 RESET = "\033[0m" 

37 

38 def __init__( 

39 self, 

40 verbose: bool = True, 

41 use_color: bool | None = None, 

42 colors: CliColorsConfig | None = None, 

43 ) -> None: 

44 """Initialize logger. 

45 

46 Args: 

47 verbose: Whether to output messages 

48 use_color: Whether to use ANSI color codes. Defaults to True unless 

49 the NO_COLOR environment variable is set. 

50 colors: Optional CliColorsConfig to override default color codes. 

51 """ 

52 self.verbose = verbose 

53 if use_color is None: 

54 use_color = os.environ.get("NO_COLOR", "") == "" 

55 self.use_color = use_color 

56 

57 # Apply color overrides from config 

58 if colors is not None: 

59 self.CYAN = f"\033[{colors.logger.info}m" 

60 self.GREEN = f"\033[{colors.logger.success}m" 

61 self.YELLOW = f"\033[{colors.logger.warning}m" 

62 self.ORANGE = f"\033[{colors.logger.error}m" 

63 self.RED = self.ORANGE # keep alias in sync 

64 

65 def _timestamp(self) -> str: 

66 """Get current timestamp string.""" 

67 return datetime.now().strftime("%H:%M:%S") 

68 

69 def _format(self, color: str, msg: str) -> str: 

70 """Format message with timestamp and optional color.""" 

71 ts = self._timestamp() 

72 if self.use_color: 

73 return f"{color}[{ts}]{self.RESET} {msg}" 

74 return f"[{ts}] {msg}" 

75 

76 def info(self, msg: str) -> None: 

77 """Log an info message.""" 

78 if self.verbose: 

79 print(self._format(self.CYAN, msg)) 

80 

81 def debug(self, msg: str) -> None: 

82 """Log a debug message (gray/dim).""" 

83 if self.verbose: 

84 print(self._format(self.GRAY, msg)) 

85 

86 def success(self, msg: str) -> None: 

87 """Log a success message.""" 

88 if self.verbose: 

89 print(self._format(self.GREEN, msg)) 

90 

91 def warning(self, msg: str) -> None: 

92 """Log a warning message.""" 

93 if self.verbose: 

94 print(self._format(self.YELLOW, msg)) 

95 

96 def error(self, msg: str) -> None: 

97 """Log an error message to stderr.""" 

98 if self.verbose: 

99 print(self._format(self.RED, msg), file=sys.stderr) 

100 

101 def timing(self, msg: str) -> None: 

102 """Log timing information.""" 

103 if self.verbose: 

104 print(self._format(self.MAGENTA, msg)) 

105 

106 def header(self, msg: str, char: str = "=", width: int = 60) -> None: 

107 """Log a header with separators.""" 

108 if self.verbose: 

109 line = char * width 

110 print(line) 

111 print(msg) 

112 print(line) 

113 

114 

115def format_duration(seconds: float) -> str: 

116 """Format duration in human-readable form. 

117 

118 Args: 

119 seconds: Duration in seconds 

120 

121 Returns: 

122 Human-readable string like "5.2 seconds" or "3.5 minutes" 

123 """ 

124 if seconds >= 60: 

125 return f"{seconds / 60:.1f} minutes" 

126 return f"{seconds:.1f} seconds"