Coverage for pyEDAA/OutputFilter/__init__.py: 35%

158 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-20 22:02 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ___ _ _ _____ _ _ _ # 

3# _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| # 

7# |_| |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Boetzingen, Germany # 

15# Copyright 2014-2016 Technische Universitaet Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # 

16# # 

17# Licensed under the Apache License, Version 2.0 (the "License"); # 

18# you may not use this file except in compliance with the License. # 

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

23# Unless required by applicable law or agreed to in writing, software # 

24# distributed under the License is distributed on an "AS IS" BASIS, # 

25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32"""An abstraction layer of EDA tool output filters.""" 

33__author__ = "Patrick Lehmann" 

34__email__ = "Paebbels@gmail.com" 

35__copyright__ = "2014-2026, Patrick Lehmann" 

36__license__ = "Apache License, Version 2.0" 

37__version__ = "0.16.0" 

38__keywords__ = ["cli", "abstraction layer", "eda", "filter", "classification"] 

39__project_url__ = "https://github.com/edaa-org/pyEDAA.OutputFilter" 

40__documentation_url__ = "https://edaa-org.github.io/pyEDAA.OutputFilter" 

41__issue_tracker_url__ = "https://GitHub.com/edaa-org/pyEDAA.OutputFilter/issues" 

42 

43from datetime import datetime 

44from enum import Flag 

45from typing import Any, Generator, Callable, Tuple, Union, Optional as Nullable, Generic, TypeVar 

46 

47from pyTooling.Common import getFullyQualifiedName 

48from pyTooling.Decorators import export, readonly 

49from pyTooling.Exceptions import ExceptionBase 

50from pyTooling.MetaClasses import ExtendedType 

51 

52 

53@export 

54class OutputFilterException(ExceptionBase): 

55 """Base-class for all pyEDAA.OutputFilter specific exceptions.""" 

56 

57 

58LineClassification = TypeVar("LineClassification", bound=Flag) 

59LineProcessingAction = TypeVar("LineProcessingAction", bound=Flag) 

60 

61@export 

62class Line(Generic[LineClassification, LineProcessingAction], metaclass=ExtendedType, slots=True): 

63 """ 

64 This class represents any line in a log file. 

65 

66 A line has a line number (:attr:`_lineNumber`), a message (:attr:`__message`) and a message kind (:attr:`__kind`). In 

67 addition, all line objects in a log file form a doubly 

68 linked list. 

69 """ 

70 _lineNumber: int 

71 _timestamp: Nullable[datetime] 

72 _document: "Document" 

73 _kind: LineClassification 

74 _action: LineProcessingAction 

75 _message: str 

76 _previousLine: Nullable["Line"] 

77 _nextLine: Nullable["Line"] 

78 

79 def __init__( 

80 self, 

81 lineNumber: int, 

82 kind: LineClassification, 

83 action: LineProcessingAction, 

84 message: str, 

85 previousLine: Nullable["Line"] = None 

86 ) -> None: 

87 self._lineNumber = lineNumber 

88 self._kind = kind 

89 self._action = action 

90 self._message = message 

91 self._previousLine = previousLine 

92 self._nextLine = None 

93 

94 if previousLine is not None: 

95 previousLine._nextLine = self 

96 

97 if not isinstance(message, str): 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true

98 pass 

99 

100 @readonly 

101 def LineNumber(self) -> int: 

102 return self._lineNumber 

103 

104 @readonly 

105 def Kind(self) -> LineClassification: 

106 return self._kind 

107 

108 @readonly 

109 def Action(self) -> LineProcessingAction: 

110 return self._action 

111 

112 @readonly 

113 def Message(self) -> str: 

114 return self._message 

115 

116 @property 

117 def PreviousLine(self) -> Nullable["Line"]: 

118 return self._previousLine 

119 

120 @PreviousLine.setter 

121 def PreviousLine(self, line: "Line") -> None: 

122 self._previousLine = line 

123 if line is not None: 

124 line._nextLine = self 

125 

126 @readonly 

127 def NextLine(self) -> Nullable["Line"]: 

128 return self._nextLine 

129 

130 def StartsWith(self, prefix: Union[str, Tuple[str, ...]]): 

131 return self._message.startswith(prefix) 

132 

133 def Partition(self, separator: str) -> Tuple[str, str, str]: 

134 return self._message.partition(separator) 

135 

136 def GetIterator( 

137 self, 

138 stopPredicate: Nullable[Callable[["Line"], bool]] = None, 

139 *, 

140 reverse: bool = False, 

141 inclusive: bool = True, 

142 maxLines: Nullable[int] = None, 

143 ) -> Generator["Line", None, None]: 

144 """ 

145 Iterate consecutive lines starting from next line towards the end of the log. 

146 

147 If the order is reversed, iterate starting at the previous line towards the beginning of the log. The iteration ends 

148 either at the bounds of the log, by specifying a stop predicate or a maximum number of lines to return. When stopped 

149 this line is usually included in the iteration, but can be excluded. 

150 

151 :param stopPredicate: Optional, a callable receiving a :class:`Line` and returning ``True`` when iteration should 

152 stop at that line. 

153 :param reverse: Optional, reverse the iteration from previous line to the beginning of the log. 

154 :param inclusive: Optional, when ``True`` the line where ``stopPredicate`` or ``maxLines`` triggers, is 

155 included in the iteration, otherwise it's excluded. 

156 :param maxLines: Optional, maximum number of lines to yield. 

157 :returns: A generator yielding :class:`Line` in the requested direction, stopping at the log boundary, 

158 the predicate match, or the line limit — whichever comes first. 

159 :raises TypeError: When ``stopPredicate`` is not callable. 

160 :raises ValueError: When ``maxLines`` is not a positive integer. 

161 """ 

162 if stopPredicate is not None and not callable(stopPredicate): 

163 ex = TypeError("Parameter 'stopPredicate' is not a callable.") 

164 ex.add_note(f"Got type '{getFullyQualifiedName(stopPredicate)}'.") 

165 raise ex 

166 if not isinstance(reverse, bool): 

167 ex = TypeError("Parameter 'reverse' is not a boolean.") 

168 ex.add_note(f"Got type '{getFullyQualifiedName(reverse)}'.") 

169 raise ex 

170 if not isinstance(inclusive, bool): 

171 ex = TypeError("Parameter 'inclusive' is not a boolean.") 

172 ex.add_note(f"Got type '{getFullyQualifiedName(inclusive)}'.") 

173 raise ex 

174 if maxLines is not None: 

175 if not isinstance(maxLines, int): 

176 ex = TypeError("Parameter 'maxLines' is not a integer.") 

177 ex.add_note(f"Got type '{getFullyQualifiedName(maxLines)}'.") 

178 raise ex 

179 elif maxLines <= 0: 

180 ex = ValueError("Parameter 'maxLines' must be a positive integer.") 

181 ex.add_note(f"Got {maxLines!r}.") 

182 raise ex 

183 

184 current = self._previousLine if reverse else self._nextLine 

185 

186 if maxLines is None: 

187 if stopPredicate is None: 

188 if reverse: 

189 while current is not None: 

190 yield current 

191 current = current._previousLine 

192 else: 

193 while current is not None: 

194 yield current 

195 current = current._nextLine 

196 else: 

197 if reverse: 

198 while current is not None: 

199 if stopPredicate(current): 

200 if inclusive: 

201 yield current 

202 return 

203 yield current 

204 current = current._previousLine 

205 else: 

206 while current is not None: 

207 if stopPredicate(current): 

208 if inclusive: 

209 yield current 

210 return 

211 yield current 

212 current = current._nextLine 

213 

214 elif stopPredicate is None: 

215 remaining = maxLines 

216 if reverse: 

217 while current is not None and remaining > 0: 

218 yield current 

219 current = current._previousLine 

220 remaining -= 1 

221 else: 

222 while current is not None and remaining > 0: 

223 yield current 

224 current = current._nextLine 

225 remaining -= 1 

226 

227 else: 

228 remaining = maxLines 

229 if reverse: 

230 while current is not None and remaining > 0: 

231 if stopPredicate(current): 

232 if inclusive: 

233 yield current 

234 return 

235 yield current 

236 current = current._previousLine 

237 remaining -= 1 

238 else: 

239 while current is not None and remaining > 0: 

240 if stopPredicate(current): 

241 if inclusive: 

242 yield current 

243 return 

244 yield current 

245 current = current._nextLine 

246 remaining -= 1 

247 

248 def __getitem__(self, item: slice) -> str: 

249 return self._message[item] 

250 

251 def __eq__(self, other: Any): 

252 return self._message == other 

253 

254 def __ne__(self, other: Any): 

255 return self._message != other 

256 

257 def __str__(self) -> str: 

258 return self._message 

259 

260 def __repr__(self) -> str: 

261 return f"{self._lineNumber}: {self._message}" 

262 

263 

264@export 

265class InfoMessage(metaclass=ExtendedType, mixin=True): 

266 pass 

267 

268 

269@export 

270class WarningMessage(metaclass=ExtendedType, mixin=True): 

271 pass 

272 

273 

274@export 

275class CriticalWarningMessage(metaclass=ExtendedType, mixin=True): 

276 pass 

277 

278 

279@export 

280class ErrorMessage(metaclass=ExtendedType, mixin=True): 

281 pass