Coverage for src / lexigram / contracts / exceptions / config.py: 0%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-15 18:57 +0800

1"""Configuration-related exception classes.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from typing import TYPE_CHECKING, Any 

7 

8from lexigram.contracts.exceptions.base import LexigramError 

9 

10if TYPE_CHECKING: 

11 from lexigram.contracts.core.config import ConfigIssue 

12 

13_SECRET_PATTERN = re.compile( 

14 r"(password|secret|token|key|api_key|auth|credential|private)", 

15 re.IGNORECASE, 

16) 

17 

18 

19def _redact_value(field: str, value: Any) -> str: 

20 """Return redacted display of a field value if the field name looks secret.""" 

21 if _SECRET_PATTERN.search(field): 

22 return "***" 

23 if value is None: 

24 return "(not set)" 

25 text = str(value) 

26 return text if len(text) <= 80 else text[:77] + "..." 

27 

28 

29class ConfigurationError(LexigramError): 

30 """Developer configuration or runtime misconfiguration errors. 

31 

32 Supports optional structured validation errors for integration 

33 with validation frameworks. 

34 

35 Attributes: 

36 validation_errors: A list of per-field error dicts, each containing 

37 at minimum ``field``, ``message``, and ``type`` keys. 

38 issues: Typed ``ConfigIssue`` list produced by 

39 ``BaseConfig.validate_for_environment()``. 

40 """ 

41 

42 _code = "LEX_ERR_CFG_001" 

43 

44 validation_errors: list[dict[str, Any]] 

45 issues: list[ConfigIssue] 

46 

47 def __init__( 

48 self, 

49 message: str = "Configuration error", 

50 *, 

51 validation_errors: list[dict[str, Any]] | None = None, 

52 issues: list[ConfigIssue] | None = None, 

53 **kwargs: Any, 

54 ) -> None: 

55 self.validation_errors = validation_errors or [] 

56 self.issues = issues or [] 

57 super().__init__(message, **kwargs) 

58 

59 @classmethod 

60 def from_validation_error(cls, error: Exception) -> ConfigurationError: 

61 """Create a ``ConfigurationError`` from a ``ValidationError``. 

62 

63 Supports both the contracts ``ValidationError`` (with ``.errors`` 

64 attribute) and pydantic's ``ValidationError`` (with ``.errors()`` 

65 method) for backwards compatibility. 

66 

67 Args: 

68 error: A ``ValidationError`` instance from either 

69 ``lexigram.contracts.validation`` or ``pydantic``. 

70 

71 Returns: 

72 A new ``ConfigurationError`` with structured ``validation_errors``. 

73 """ 

74 # Get error list: .errors (attribute) or .errors() (pydantic method) 

75 if hasattr(error, "errors") and callable(error.errors): 

76 raw_errors = error.errors() 

77 elif hasattr(error, "errors"): 

78 raw_errors = error.errors 

79 else: 

80 return cls(str(error)) 

81 

82 messages: list[str] = [] 

83 details: list[dict[str, Any]] = [] 

84 for err in raw_errors: 

85 # Support both FieldError objects and pydantic-style dicts 

86 if hasattr(err, "field") and hasattr(err, "message"): 

87 loc_str = err.field 

88 msg = err.message 

89 err_type = getattr(err, "code", "validation_error") 

90 raw_value = getattr(err, "input", None) 

91 else: 

92 loc = err.get("loc", ()) 

93 loc_str = " → ".join(str(part) for part in loc) if loc else "unknown" 

94 msg = err.get("msg", str(err)) 

95 err_type = err.get("type", "validation_error") 

96 raw_value = err.get("input", None) 

97 displayed_value = _redact_value(loc_str, raw_value) 

98 entry: dict[str, Any] = { 

99 "field": loc_str, 

100 "message": msg, 

101 "type": err_type, 

102 "value": displayed_value, 

103 } 

104 details.append(entry) 

105 messages.append(f" {loc_str}: {msg} (got: {displayed_value})") 

106 

107 return cls( 

108 "Configuration validation failed:\n" + "\n".join(messages), 

109 validation_errors=details, 

110 ) 

111 

112 @classmethod 

113 def from_issues(cls, issues: list[ConfigIssue]) -> ConfigurationError: 

114 """Create a ``ConfigurationError`` from a list of ``ConfigIssue`` objects. 

115 

116 Only issues with ``severity == "error"`` contribute to the message. 

117 All issues (errors **and** warnings) are stored on ``self.issues``. 

118 

119 Args: 

120 issues: List of ``ConfigIssue`` instances produced by 

121 ``BaseConfig.validate_for_environment()``. 

122 

123 Returns: 

124 A new ``ConfigurationError`` carrying the typed issue list. 

125 """ 

126 error_issues = [i for i in issues if i.severity == "error"] 

127 lines = [f" {i.field}: {i.message}" for i in error_issues] 

128 message = "Configuration validation failed:\n" + "\n".join(lines) 

129 return cls(message, issues=list(issues)) 

130 

131 

132__all__ = ["ConfigurationError"]