Coverage for dataclasses_struct/field.py: 99%

97 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-15 09:30 +1200

1import abc 

2import ctypes 

3from types import GenericAlias 

4from typing import Any, ClassVar, Generic, Literal, TypeVar, Union 

5 

6T = TypeVar("T") 

7 

8 

9class Field(abc.ABC, Generic[T]): 

10 is_native: bool = True 

11 is_std: bool = True 

12 field_type: Union[type[T], tuple[type[T], ...], GenericAlias] 

13 

14 @abc.abstractmethod 

15 def format(self) -> str: ... 

16 

17 def validate_default(self, val: T) -> None: 

18 pass 

19 

20 def __repr__(self) -> str: 

21 return f"{type(self).__name__}" 

22 

23 

24class BoolField(Field[bool]): 

25 field_type = bool 

26 

27 def format(self) -> str: 

28 return "?" 

29 

30 

31class CharField(Field[bytes]): 

32 field_type = bytes 

33 

34 def format(self) -> str: 

35 return "c" 

36 

37 def validate_default(self, val: bytes) -> None: 

38 if len(val) != 1: 

39 raise ValueError("value must be a single byte") 

40 

41 

42class IntField(Field[int]): 

43 field_type = int 

44 

45 def __init__( 

46 self, 

47 fmt: str, 

48 signed: bool, 

49 size: int, 

50 ): 

51 if signed and fmt.isupper(): 

52 raise ValueError( 

53 "signed integer should have lowercase format string" 

54 ) 

55 

56 self.signed = signed 

57 self.size = size 

58 self._format = fmt 

59 

60 nbits = self.size * 8 

61 if signed: 

62 exp = 1 << (nbits - 1) 

63 self.min_ = -exp 

64 self.max_ = exp - 1 

65 else: 

66 self.min_ = 0 

67 self.max_ = (1 << nbits) - 1 

68 

69 def format(self) -> str: 

70 return self._format 

71 

72 def validate_default(self, val: int) -> None: 

73 if not (self.min_ <= val <= self.max_): 

74 sign = "signed" if self.signed else "unsigned" 

75 n = self.size * 8 

76 raise ValueError(f"value out of range for {n}-bit {sign} integer") 

77 

78 def __repr__(self) -> str: 

79 sign = "signed" if self.signed else "unsigned" 

80 return f"{super().__repr__()}({sign}, {self.size * 8}-bit)" 

81 

82 

83class StdIntField(IntField): 

84 is_native = False 

85 _unsigned_formats: ClassVar = { 

86 1: "B", 

87 2: "H", 

88 4: "I", 

89 8: "Q", 

90 } 

91 

92 def __init__(self, signed: bool, size: Literal[1, 2, 4, 8]): 

93 fmt = self._unsigned_formats[size] 

94 if signed: 

95 fmt = fmt.lower() 

96 super().__init__(fmt, signed, size) 

97 

98 

99class SignedStdIntField(StdIntField): 

100 def __init__(self, size: Literal[1, 2, 4, 8]): 

101 super().__init__(True, size) 

102 

103 

104class UnsignedStdIntField(StdIntField): 

105 def __init__(self, size: Literal[1, 2, 4, 8]): 

106 super().__init__(False, size) 

107 

108 

109class FloatingPointField(Field[float]): 

110 field_type = (int, float) 

111 

112 def __init__(self, format: str): 

113 self._format = format 

114 

115 def format(self) -> str: 

116 return self._format 

117 

118 

119class NativeIntField(IntField): 

120 is_std = False 

121 

122 def __init__(self, fmt: str, ctype_name: str): 

123 size = ctypes.sizeof(getattr(ctypes, f"c_{ctype_name}")) 

124 signed = not ctype_name.startswith("u") 

125 super().__init__(fmt, signed, size) 

126 

127 

128class SizeField(IntField): 

129 is_std = False 

130 

131 def __init__(self, signed: bool): 

132 fmt = "n" if signed else "N" 

133 size = ctypes.sizeof(ctypes.c_ssize_t if signed else ctypes.c_size_t) 

134 super().__init__(fmt, signed, size) 

135 

136 def validate_default(self, val: int) -> None: 

137 if not (self.min_ <= val <= self.max_): 

138 sign = "signed" if self.signed else "unsigned" 

139 raise ValueError(f"value out of range for {sign} size type") 

140 

141 

142class PointerField(IntField): 

143 is_std = False 

144 

145 def __init__(self): 

146 super().__init__("P", False, ctypes.sizeof(ctypes.c_void_p)) 

147 

148 def format(self) -> str: 

149 return "P" 

150 

151 def validate_default(self, val: int) -> None: 

152 if not (self.min_ <= val <= self.max_): 

153 raise ValueError("value out of range for system pointer") 

154 

155 

156builtin_fields: dict[type[Any], Field[Any]] = { 

157 int: NativeIntField("i", "int"), 

158 float: FloatingPointField("d"), 

159 bool: BoolField(), 

160 bytes: CharField(), 

161}