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

66 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 04:48 +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 

11_stderr_console = Console(stderr=True) 

12 

13 

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

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

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

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

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

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

20 

21 for err in errors: 

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

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

24 suggestion = _suggest_fix(loc, msg) 

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

26 

27 return table 

28 

29 

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

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

32 try: 

33 from pydantic import ValidationError 

34 

35 if isinstance(error, ValidationError): 

36 _stderr_console.print( 

37 Panel( 

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

39 style="red", 

40 ) 

41 ) 

42 table = format_validation_errors(error.errors()) 

43 _stderr_console.print(table) 

44 _stderr_console.print( 

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

46 ) 

47 return 

48 except ImportError: 

49 pass 

50 

51 try: 

52 import yaml 

53 

54 if isinstance(error, yaml.YAMLError): 

55 msg = "YAML syntax error in configuration file" 

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

57 if mark is not None: 

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

59 _stderr_console.print( 

60 Panel( 

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

62 f"[yellow]{str(error)}[/yellow]\n\n" 

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

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

65 " - Missing colons after keys\n" 

66 " - Unquoted special characters", 

67 title="YAML Error", 

68 style="red", 

69 ) 

70 ) 

71 return 

72 except ImportError: 

73 pass 

74 

75 if isinstance(error, FileNotFoundError): 

76 _stderr_console.print( 

77 Panel( 

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

79 f"[yellow]{str(error)}[/yellow]\n\n" 

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

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

82 " - Create config.yaml manually\n" 

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

84 title="File Not Found", 

85 style="red", 

86 ) 

87 ) 

88 return 

89 

90 if isinstance(error, ValueError): 

91 _stderr_console.print( 

92 Panel( 

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

94 f"[yellow]{str(error)}[/yellow]\n\n" 

95 f"[green]Suggestion:[/green] {_suggest_fix('', str(error))}", 

96 title="Validation Error", 

97 style="red", 

98 ) 

99 ) 

100 return 

101 

102 # Generic fallback for any other exception type 

103 _stderr_console.print( 

104 Panel( 

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

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

107 title="Error", 

108 style="red", 

109 ) 

110 ) 

111 

112 

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

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

115 msg_lower = message.lower() 

116 field_lower = field.lower() 

117 

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

119 return "Set QDRANT_API_KEY in .env or environment" 

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

121 return "Set OPENAI_API_KEY in .env or environment" 

122 if "collection_name" in field_lower: 

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

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

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

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

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

128 if "database_path" in field_lower: 

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

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

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

132 if "required" in msg_lower: 

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

134 

135 return "Check the configuration documentation"