Coverage for src / qdrant_loader / config / error_formatter.py: 94%

69 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-10 09:40 +0000

1"""User-friendly configuration error formatting with Rich.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from rich.console import Console 

8from rich.panel import Panel 

9from rich.table import Table 

10 

11from qdrant_loader.utils.sensitive import sanitize_exception_message 

12 

13_stderr_console = Console(stderr=True) 

14 

15 

16def format_validation_errors(errors: list[dict[str, Any]]) -> Table: 

17 """Format Pydantic validation errors as a Rich table.""" 

18 table = Table(title="Configuration Errors", show_lines=True) 

19 table.add_column("Field", style="red", no_wrap=True) 

20 table.add_column("Error", style="yellow") 

21 table.add_column("Suggestion", style="green") 

22 

23 for err in errors: 

24 loc = " -> ".join(str(part) for part in err.get("loc", [])) 

25 msg = err.get("msg", "Unknown error") 

26 suggestion = _suggest_fix(loc, msg) 

27 table.add_row(loc or "(root)", msg, suggestion) 

28 

29 return table 

30 

31 

32def print_config_error(error: Exception) -> None: 

33 """Print a user-friendly configuration error to stderr using Rich.""" 

34 try: 

35 from pydantic import ValidationError 

36 

37 if isinstance(error, ValidationError): 

38 _stderr_console.print( 

39 Panel( 

40 "[bold red]Configuration validation failed[/bold red]", 

41 style="red", 

42 ) 

43 ) 

44 table = format_validation_errors(error.errors()) 

45 _stderr_console.print(table) 

46 _stderr_console.print( 

47 "\n[dim]Tip: Run 'qdrant-loader setup' to generate a valid configuration.[/dim]" 

48 ) 

49 return 

50 except ImportError: 

51 pass 

52 

53 try: 

54 import yaml 

55 

56 if isinstance(error, yaml.YAMLError): 

57 msg = "YAML syntax error in configuration file" 

58 mark = getattr(error, "problem_mark", None) 

59 if mark is not None: 

60 msg += f" at line {mark.line + 1}, column {mark.column + 1}" 

61 _stderr_console.print( 

62 Panel( 

63 f"[bold red]{msg}[/bold red]\n\n" 

64 f"[yellow]{sanitize_exception_message(error)}[/yellow]\n\n" 

65 "[green]Suggestion:[/green] Check YAML syntax. Common issues:\n" 

66 " - Incorrect indentation (use spaces, not tabs)\n" 

67 " - Missing colons after keys\n" 

68 " - Unquoted special characters", 

69 title="YAML Error", 

70 style="red", 

71 ) 

72 ) 

73 return 

74 except ImportError: 

75 pass 

76 

77 if isinstance(error, FileNotFoundError): 

78 _stderr_console.print( 

79 Panel( 

80 "[bold red]Configuration file not found[/bold red]\n\n" 

81 f"[yellow]{sanitize_exception_message(error)}[/yellow]\n\n" 

82 "[green]Suggestions:[/green]\n" 

83 " - Run 'qdrant-loader setup' to generate configuration files\n" 

84 " - Create config.yaml manually\n" 

85 " - Specify path with --config flag", 

86 title="File Not Found", 

87 style="red", 

88 ) 

89 ) 

90 return 

91 

92 if isinstance(error, ValueError): 

93 safe_message = sanitize_exception_message(error) 

94 raw_message = str(error) 

95 

96 _stderr_console.print( 

97 Panel( 

98 "[bold red]Configuration error[/bold red]\n\n" 

99 f"[yellow]{safe_message}[/yellow]\n\n" 

100 f"[green]Suggestion:[/green] {_suggest_fix('', raw_message)}", 

101 title="Validation Error", 

102 style="red", 

103 ) 

104 ) 

105 return 

106 

107 # Generic fallback for any other exception type 

108 _stderr_console.print( 

109 Panel( 

110 "[bold red]Configuration error[/bold red]\n\n" 

111 f"[yellow]{type(error).__name__}: {sanitize_exception_message(error)}[/yellow]", 

112 title="Error", 

113 style="red", 

114 ) 

115 ) 

116 

117 

118def _suggest_fix(field: str, message: str) -> str: 

119 """Return a human-readable fix suggestion based on the error field and message.""" 

120 msg_lower = message.lower() 

121 field_lower = field.lower() 

122 

123 if "qdrant" in field_lower and "api_key" in field_lower: 

124 return "Set QDRANT_API_KEY in .env or environment" 

125 if "api_key" in field_lower or "api_key" in msg_lower: 

126 return "Set OPENAI_API_KEY in .env or environment" 

127 if "collection_name" in field_lower: 

128 return "Set QDRANT_COLLECTION_NAME or use default 'documents'" 

129 if "url" in field_lower and "qdrant" in field_lower: 

130 return "Set QDRANT_URL or use default http://localhost:6333" 

131 if "sources" in field_lower or "source" in msg_lower: 

132 return "Add at least one source under 'sources:' in config.yaml" 

133 if "database_path" in field_lower: 

134 return "Set STATE_DB_PATH or use default ./state.db" 

135 if "chunk_size" in field_lower or "chunk" in msg_lower: 

136 return "chunk_size must be a positive integer (recommended: 1000-2000)" 

137 if "required" in msg_lower: 

138 return f"Provide a value for '{field}' in config.yaml or .env" 

139 

140 return "Check the configuration documentation"