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
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:20 -0500
1"""Logging utilities for little-loops.
3Provides colorized console output with timestamps for automation tools.
4"""
6from __future__ import annotations
8import os
9import sys
10from datetime import datetime
11from typing import TYPE_CHECKING
13if TYPE_CHECKING:
14 from little_loops.config import CliColorsConfig
17class Logger:
18 """Simple logger with timestamps and colors.
20 Provides info, success, warning, and error logging methods with
21 optional colorized output for terminal display.
23 Attributes:
24 verbose: Whether to output messages (False silences all output)
25 use_color: Whether to use ANSI color codes
26 """
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"
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.
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
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
65 def _timestamp(self) -> str:
66 """Get current timestamp string."""
67 return datetime.now().strftime("%H:%M:%S")
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}"
76 def info(self, msg: str) -> None:
77 """Log an info message."""
78 if self.verbose:
79 print(self._format(self.CYAN, msg))
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))
86 def success(self, msg: str) -> None:
87 """Log a success message."""
88 if self.verbose:
89 print(self._format(self.GREEN, msg))
91 def warning(self, msg: str) -> None:
92 """Log a warning message."""
93 if self.verbose:
94 print(self._format(self.YELLOW, msg))
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)
101 def timing(self, msg: str) -> None:
102 """Log timing information."""
103 if self.verbose:
104 print(self._format(self.MAGENTA, msg))
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)
115def format_duration(seconds: float) -> str:
116 """Format duration in human-readable form.
118 Args:
119 seconds: Duration in seconds
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"