Coverage for src / lexigram / contracts / exceptions / base.py: 71%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Base exception classes for Lexigram Framework.
3This module contains the canonical exception hierarchy for the entire Lexigram
4ecosystem. All framework exceptions should inherit from LexigramError.
5"""
7from __future__ import annotations
9from typing import Any, Self
12class LexigramError(Exception):
13 """Root exception for the entire Lexigram ecosystem.
15 All Lexigram exceptions inherit from this class. This ensures that
16 isinstance checks work identically across core, events, web, and
17 all subpackages.
19 Attributes:
20 code: Machine-readable error code (e.g., 'LEX_CONTAINER_001')
21 message: Human-readable error message
22 details: Additional context dictionary
23 cause: Original exception that triggered this one
24 hint: Optional suggestion for fixing the error
25 """
27 _code: str = "LEX_ERR_CORE_001"
29 def __init__(
30 self,
31 message: str | None = None,
32 details: dict[str, Any] | None = None,
33 cause: BaseException | None = None,
34 hint: str | None = None,
35 **kwargs: Any,
36 ) -> None:
37 self.message = message or "An internal error occurred"
38 self.code = self._code
39 self.details = details or {}
40 self.cause = cause
41 self.hint = hint
42 super().__init__(self.message)
44 def __repr__(self) -> str:
45 return (
46 f"<{self.__class__.__name__}(code={self.code!r}, message={self.message!r})"
47 )
49 @property
50 def docs_url(self) -> str:
51 """Return the documentation URL for this error code."""
52 return f"https://docs.lexigram.dev/reference/errors/{self.code}"
54 def __str__(self) -> str:
55 base = f"[{self.code}] {self.message}"
56 if self.hint:
57 base += f"\n → Fix: {self.hint}"
58 if self.code != "LEX_ERR_CORE_001":
59 base += f"\n → See: {self.docs_url}"
60 return base
62 def to_dict(self) -> dict[str, Any]:
63 """Serialize exception to dictionary for logging/API responses."""
64 result: dict[str, Any] = {
65 "code": self.code,
66 "message": self.message,
67 "details": self.details,
68 }
69 if self.hint:
70 result["hint"] = self.hint
71 if self.cause is not None:
72 result["cause"] = {
73 "type": type(self.cause).__name__,
74 "message": str(self.cause),
75 }
76 return result
78 def with_details(self, **kwargs: Any) -> Self:
79 """Return self with additional detail key-value pairs merged in.
81 Mutates and returns self to preserve the exception subtype.
82 """
83 self.details = {**self.details, **kwargs}
84 return self
86 def with_hint(self, hint: str) -> Self:
87 """Return self with a human-readable hint attached."""
88 self.hint = hint
89 return self
91 def with_cause(self, cause: BaseException) -> Self:
92 """Return self with ``__cause__`` set to ``cause``."""
93 self.cause = cause
94 self.__cause__ = cause
95 return self
97 def format(self) -> str:
98 """Format for developer-friendly console output.
100 Returns a multi-line string containing the error class name, code,
101 message, details, hint, and cause chain when present.
102 """
103 lines = []
104 lines.append(f"\n{type(self).__name__} [{self.code}]\n")
105 lines.append(f" {self.message}\n")
107 if self.details:
108 lines.append("")
109 for key, value in self.details.items():
110 lines.append(f" {key}: {value}")
111 lines.append("")
113 if self.hint:
114 lines.append(f" Hint: {self.hint}\n")
116 if self.__cause__:
117 lines.append(
118 f" Caused by: {type(self.__cause__).__name__}: {self.__cause__}",
119 )
121 return "\n".join(lines)
124__all__ = ["LexigramError"]