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
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-15 18:57 +0800
1"""Configuration-related exception classes."""
3from __future__ import annotations
5import re
6from typing import TYPE_CHECKING, Any
8from lexigram.contracts.exceptions.base import LexigramError
10if TYPE_CHECKING:
11 from lexigram.contracts.core.config import ConfigIssue
13_SECRET_PATTERN = re.compile(
14 r"(password|secret|token|key|api_key|auth|credential|private)",
15 re.IGNORECASE,
16)
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] + "..."
29class ConfigurationError(LexigramError):
30 """Developer configuration or runtime misconfiguration errors.
32 Supports optional structured validation errors for integration
33 with validation frameworks.
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 """
42 _code = "LEX_ERR_CFG_001"
44 validation_errors: list[dict[str, Any]]
45 issues: list[ConfigIssue]
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)
59 @classmethod
60 def from_validation_error(cls, error: Exception) -> ConfigurationError:
61 """Create a ``ConfigurationError`` from a ``ValidationError``.
63 Supports both the contracts ``ValidationError`` (with ``.errors``
64 attribute) and pydantic's ``ValidationError`` (with ``.errors()``
65 method) for backwards compatibility.
67 Args:
68 error: A ``ValidationError`` instance from either
69 ``lexigram.contracts.validation`` or ``pydantic``.
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))
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})")
107 return cls(
108 "Configuration validation failed:\n" + "\n".join(messages),
109 validation_errors=details,
110 )
112 @classmethod
113 def from_issues(cls, issues: list[ConfigIssue]) -> ConfigurationError:
114 """Create a ``ConfigurationError`` from a list of ``ConfigIssue`` objects.
116 Only issues with ``severity == "error"`` contribute to the message.
117 All issues (errors **and** warnings) are stored on ``self.issues``.
119 Args:
120 issues: List of ``ConfigIssue`` instances produced by
121 ``BaseConfig.validate_for_environment()``.
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))
132__all__ = ["ConfigurationError"]