Coverage for lintro / utils / unified_config_manager.py: 100%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Unified configuration manager class for Lintro. 

2 

3This module provides the central configuration manager that coordinates 

4loading, merging, and applying configurations from multiple sources. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass, field 

10from typing import Any 

11 

12from loguru import logger 

13 

14from lintro.utils.config import load_lintro_global_config, load_lintro_tool_config 

15from lintro.utils.config_constants import ToolConfigInfo 

16from lintro.utils.config_priority import get_effective_line_length, get_ordered_tools 

17from lintro.utils.config_validation import ( 

18 get_tool_config_summary, 

19 is_tool_injectable, # Used in get_tool_config() fallback 

20 validate_config_consistency, 

21) 

22from lintro.utils.native_parsers import _load_native_tool_config 

23 

24 

25@dataclass 

26class UnifiedConfigManager: 

27 """Central configuration manager for Lintro. 

28 

29 This class provides a unified interface for: 

30 - Loading and merging configurations from multiple sources. 

31 - Computing effective configurations for each tool. 

32 - Validating configuration consistency. 

33 - Managing tool execution order. 

34 

35 Attributes: 

36 global_config: Global Lintro configuration from [tool.lintro]. 

37 tool_configs: Per-tool configuration info. 

38 warnings: List of configuration warnings. 

39 """ 

40 

41 global_config: dict[str, Any] = field(default_factory=dict) 

42 tool_configs: dict[str, ToolConfigInfo] = field(default_factory=dict) 

43 warnings: list[str] = field(default_factory=list) 

44 

45 def __post_init__(self) -> None: 

46 """Initialize configuration manager.""" 

47 self.refresh() 

48 

49 def refresh(self) -> None: 

50 """Reload all configuration from files.""" 

51 self.global_config = load_lintro_global_config() 

52 self.tool_configs = get_tool_config_summary() 

53 self.warnings = validate_config_consistency() 

54 

55 def get_effective_line_length(self, tool_name: str) -> int | None: 

56 """Get effective line length for a tool. 

57 

58 Args: 

59 tool_name: Name of the tool. 

60 

61 Returns: 

62 Effective line length or None. 

63 """ 

64 return get_effective_line_length(tool_name) 

65 

66 def get_tool_config(self, tool_name: str) -> ToolConfigInfo: 

67 """Get configuration info for a specific tool. 

68 

69 Args: 

70 tool_name: Name of the tool. 

71 

72 Returns: 

73 Tool configuration info. 

74 """ 

75 if tool_name not in self.tool_configs: 

76 self.tool_configs[tool_name] = ToolConfigInfo( 

77 tool_name=tool_name, 

78 native_config=_load_native_tool_config(tool_name), 

79 lintro_tool_config=load_lintro_tool_config(tool_name), 

80 is_injectable=is_tool_injectable(tool_name), 

81 ) 

82 return self.tool_configs[tool_name] 

83 

84 def get_ordered_tools(self, tool_names: list[str]) -> list[str]: 

85 """Get tools in execution order. 

86 

87 Args: 

88 tool_names: List of tool names. 

89 

90 Returns: 

91 List of tool names in execution order. 

92 """ 

93 return get_ordered_tools(tool_names) 

94 

95 def apply_config_to_tool( 

96 self, 

97 tool: Any, 

98 cli_overrides: dict[str, Any] | None = None, 

99 ) -> None: 

100 """Apply effective configuration to a tool instance. 

101 

102 Priority order: 

103 1. CLI overrides (if provided). 

104 2. [tool.lintro.<tool>] config. 

105 3. Global [tool.lintro] settings. 

106 

107 Args: 

108 tool: Tool instance with set_options method. 

109 cli_overrides: Optional CLI override options. 

110 

111 Raises: 

112 TypeError: If tool configuration has type mismatches. 

113 ValueError: If tool configuration has invalid values. 

114 """ 

115 tool_name = getattr(tool, "name", "").lower() 

116 if not tool_name: 

117 return 

118 

119 # Start with global settings 

120 effective_opts: dict[str, Any] = {} 

121 

122 # Get cached tool config to avoid repeated file reads 

123 tool_config = self.get_tool_config(tool_name) 

124 

125 # Apply global line_length if tool supports it 

126 if tool_config.is_injectable: 

127 line_length = self.get_effective_line_length(tool_name) 

128 if line_length is not None: 

129 effective_opts["line_length"] = line_length 

130 

131 # Apply tool-specific lintro config from cache 

132 effective_opts.update(tool_config.lintro_tool_config) 

133 

134 # Apply CLI overrides last (highest priority) 

135 if cli_overrides: 

136 effective_opts.update(cli_overrides) 

137 

138 # Apply to tool 

139 if effective_opts: 

140 try: 

141 tool.set_options(**effective_opts) 

142 logger.debug(f"Applied config to {tool_name}: {effective_opts}") 

143 except (ValueError, TypeError) as e: 

144 # Configuration errors should be visible and re-raised 

145 logger.warning( 

146 f"Configuration error for {tool_name}: {e}", 

147 exc_info=True, 

148 ) 

149 raise 

150 except (AttributeError, KeyError, RuntimeError) as e: 

151 # Other unexpected errors - log at warning but allow execution 

152 logger.warning( 

153 f"Failed to apply config to {tool_name}: {type(e).__name__}: {e}", 

154 exc_info=True, 

155 ) 

156 

157 def get_report(self) -> str: 

158 """Get configuration report. 

159 

160 Returns: 

161 Formatted configuration report string. 

162 """ 

163 # Late import to avoid circular dependency 

164 from lintro.utils.config_reporting import get_config_report 

165 

166 return str(get_config_report()) 

167 

168 def print_report(self) -> None: 

169 """Print configuration report.""" 

170 # Late import to avoid circular dependency 

171 from lintro.utils.config_reporting import print_config_report 

172 

173 print_config_report()