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

1"""Base exception classes for Lexigram Framework. 

2 

3This module contains the canonical exception hierarchy for the entire Lexigram 

4ecosystem. All framework exceptions should inherit from LexigramError. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import Any, Self 

10 

11 

12class LexigramError(Exception): 

13 """Root exception for the entire Lexigram ecosystem. 

14 

15 All Lexigram exceptions inherit from this class. This ensures that 

16 isinstance checks work identically across core, events, web, and 

17 all subpackages. 

18 

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 """ 

26 

27 _code: str = "LEX_ERR_CORE_001" 

28 

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) 

43 

44 def __repr__(self) -> str: 

45 return ( 

46 f"<{self.__class__.__name__}(code={self.code!r}, message={self.message!r})" 

47 ) 

48 

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}" 

53 

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 

61 

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 

77 

78 def with_details(self, **kwargs: Any) -> Self: 

79 """Return self with additional detail key-value pairs merged in. 

80 

81 Mutates and returns self to preserve the exception subtype. 

82 """ 

83 self.details = {**self.details, **kwargs} 

84 return self 

85 

86 def with_hint(self, hint: str) -> Self: 

87 """Return self with a human-readable hint attached.""" 

88 self.hint = hint 

89 return self 

90 

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 

96 

97 def format(self) -> str: 

98 """Format for developer-friendly console output. 

99 

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") 

106 

107 if self.details: 

108 lines.append("") 

109 for key, value in self.details.items(): 

110 lines.append(f" {key}: {value}") 

111 lines.append("") 

112 

113 if self.hint: 

114 lines.append(f" Hint: {self.hint}\n") 

115 

116 if self.__cause__: 

117 lines.append( 

118 f" Caused by: {type(self.__cause__).__name__}: {self.__cause__}", 

119 ) 

120 

121 return "\n".join(lines) 

122 

123 

124__all__ = ["LexigramError"]