Coverage for pyTooling/Warning/__init__.py: 76%

102 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-11 14:13 +0000

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

2# _____ _ _ __ __ _ # 

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

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

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V V / (_| | | | | | | | | | | (_| | # 

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

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

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

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

19# # 

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

21# # 

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

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

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

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

26# limitations under the License. # 

27# # 

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

29# ==================================================================================================================== # 

30# 

31""" 

32A solution to send warnings like exceptions to a handler in the upper part of the call-stack. 

33 

34.. hint:: 

35 

36 See :ref:`high-level help <WARNING>` for explanations and usage examples. 

37""" 

38from threading import local 

39from types import TracebackType 

40from typing import List, Callable, Optional as Nullable, Type, Iterator, Self, Iterable, Tuple 

41 

42from pyTooling.Decorators import export, readonly 

43from pyTooling.Common import getFullyQualifiedName 

44from pyTooling.Exceptions import ExceptionBase 

45 

46 

47__all__ = ["_threadLocalData"] 

48 

49_threadLocalData = local() 

50"""A reference to the thread local data needed by the pyTooling.Warning classes.""" 

51 

52 

53@export 

54class Warning(BaseException): 

55 """ 

56 Base-exception of all warnings handled by :class:`WarningCollector`. 

57 

58 .. tip:: 

59 

60 Warnings can be unhandled within a call hierarchy. 

61 """ 

62 

63 @readonly 

64 def HasNotes(self) -> bool: 

65 """ 

66 Read-only property to return if the warning has attached notes. 

67 

68 :returns: True, if the warning has attached notes. 

69 """ 

70 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0 

71 

72 @readonly 

73 def Notes(self) -> Tuple[str, ...]: 

74 """ 

75 Read-only property to access warning's attached notes. 

76 

77 :returns: Attached notes. 

78 """ 

79 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple() 

80 

81 

82@export 

83class CriticalWarning(BaseException): 

84 """ 

85 Base-exception of all critical warnings handled by :class:`WarningCollector`. 

86 

87 .. tip:: 

88 

89 Critical warnings must be unhandled within a call hierarchy, otherwise a :exc:`UnhandledCriticalWarningException` 

90 will be raised. 

91 """ 

92 

93 @readonly 

94 def HasNotes(self) -> bool: 

95 """ 

96 Read-only property to return if the warning has attached notes. 

97 

98 :returns: True, if the warning has attached notes. 

99 """ 

100 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0 

101 

102 @readonly 

103 def Notes(self) -> Tuple[str, ...]: 

104 """ 

105 Read-only property to access warning's attached notes. 

106 

107 :returns: Attached notes. 

108 """ 

109 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple() 

110 

111 

112@export 

113class UnhandledWarningException(ExceptionBase): # FIXME: to be removed in v9.0.0 

114 """ 

115 Deprecated. 

116 

117 .. deprecated:: v9.0.0 

118 

119 Please use :exc:`UnhandledCriticalWarningException`. 

120 """ 

121 

122 

123@export 

124class UnhandledCriticalWarningException(UnhandledWarningException): 

125 """ 

126 This exception is raised when a critical warning isn't handled by a :class:`WarningCollector` within the 

127 call-hierarchy. 

128 """ 

129 

130 

131@export 

132class UnhandledExceptionException(UnhandledWarningException): 

133 """ 

134 This exception is raised when an exception isn't handled by a :class:`WarningCollector` within the call-hierarchy. 

135 """ 

136 

137 

138@export 

139class WarningCollector: 

140 """ 

141 A context manager to collect warnings within the call hierarchy. 

142 """ 

143 _parent: Nullable["WarningCollector"] #: Parent WarningCollector 

144 _warnings: List[BaseException] #: List of collected warnings (and exceptions). 

145 _handler: Nullable[Callable[[BaseException], bool]] #: Optional handler function, which is called per collected warning. 

146 

147 def __init__( 

148 self, 

149 warnings: Nullable[List[BaseException]] = None, 

150 handler: Nullable[Callable[[BaseException], bool]] = None 

151 ) -> None: 

152 """ 

153 Initializes a warning collector. 

154 

155 :param warnings: An optional reference to a list of warnings, which can be modified (appended) by this warning 

156 collector. If ``None``, an internal list is created and can be referenced by the collector's 

157 instance. 

158 :param handler: An optional handler function, which processes the current warning and decides if a warning should 

159 be reraised as an exception. 

160 :raises TypeError: If optional parameter 'warnings' is not of type list. 

161 :raises TypeError: If optional parameter 'handler' is not a callable. 

162 """ 

163 if warnings is None: 

164 warnings = [] 

165 elif not isinstance(warnings, list): 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true

166 ex = TypeError(f"Parameter 'warnings' is not a list.") 

167 ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.") 

168 raise ex 

169 

170 if handler is not None and not isinstance(handler, Callable): 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true

171 ex = TypeError(f"Parameter 'handler' is not callable.") 

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

173 raise ex 

174 

175 self._parent = None 

176 self._warnings = warnings 

177 self._handler = handler 

178 

179 def __len__(self) -> int: 

180 """ 

181 Returns the number of collected warnings. 

182 

183 :returns: Number of collected warnings. 

184 """ 

185 return len(self._warnings) 

186 

187 def __iter__(self) -> Iterator[Warning | CriticalWarning | Exception]: 

188 """ 

189 Return an iterator over all collected warnings. 

190 

191 :returns: Iterator over the collected warnings. 

192 """ 

193 return iter(self._warnings) 

194 

195 def __getitem__(self, index: int) -> Warning | CriticalWarning | Exception: 

196 """ 

197 Access a collected warning by index. 

198 

199 :param index: Index of the warning. 

200 :returns: Collected warning. 

201 """ 

202 return self._warnings[index] 

203 

204 def __enter__(self) -> Self: 

205 """ 

206 Enter the warning collector context. 

207 

208 :returns: The warning collector instance. 

209 """ 

210 global _threadLocalData 

211 

212 try: 

213 self.Parent = _threadLocalData.warningCollector 

214 except AttributeError: 

215 pass 

216 

217 _threadLocalData.warningCollector = self 

218 

219 return self 

220 

221 def __exit__( 

222 self, 

223 exc_type: Nullable[Type[BaseException]] = None, 

224 exc_val: Nullable[BaseException] = None, 

225 exc_tb: Nullable[TracebackType] = None 

226 ) -> Nullable[bool]: 

227 """ 

228 Exit the warning collector context. 

229 

230 :param exc_type: Exception type 

231 :param exc_val: Exception instance 

232 :param exc_tb: Exception's traceback. 

233 :returns: ``None`` 

234 """ 

235 global _threadLocalData 

236 

237 _threadLocalData.warningCollector = self._parent 

238 

239 @property 

240 def Parent(self) -> Nullable[Self]: 

241 """ 

242 Property to access the parent warning collector. 

243 

244 :returns: The parent warning collector or ``None``. 

245 """ 

246 return self._parent 

247 

248 @Parent.setter 

249 def Parent(self, value: Self) -> None: 

250 self._parent = value 

251 

252 @readonly 

253 def Warnings(self) -> List[Warning | CriticalWarning | Exception]: 

254 """ 

255 Read-only property to access the list of collected warnings. 

256 

257 :returns: A list of collected warnings. 

258 """ 

259 return self._warnings 

260 

261 def AddWarning(self, warning: Warning | CriticalWarning | Exception) -> bool: 

262 """ 

263 Add a warning to the list of warnings managed by this warning collector. 

264 

265 :param warning: The warning to add to the collectors internal warning list. 

266 :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned 

267 ``True``; otherwise ``False``. 

268 :raises ValueError: If parameter ``warning`` is None. 

269 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`. 

270 """ 

271 if warning is None: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true

272 raise ValueError("Parameter 'warning' is None.") 

273 elif not isinstance(warning, (Warning, CriticalWarning, Exception)): 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true

274 ex = TypeError(f"Parameter 'warning' is not of type 'Warning', 'CriticalWarning' or 'Exception'.") 

275 ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.") 

276 raise ex 

277 

278 self._warnings.append(warning) 

279 

280 return False if self._handler is None else self._handler(warning) 

281 

282 @classmethod 

283 def Raise( 

284 cls, 

285 warning: Warning | CriticalWarning | Exception, 

286 cause: Nullable[Exception] = None, 

287 *, 

288 notes: Nullable[str | Iterable[str]] = None 

289 ) -> None: 

290 """ 

291 Walk the callstack frame by frame upwards and search for the first warning collector. 

292 

293 :param warning: Warning to send upwards in the call stack. 

294 :param cause: Optional, root cause to be added to the warning. 

295 :param notes: optional, a single note or a list of notes to be added to the warning. 

296 :raises Exception: If warning should be converted to an exception. 

297 :raises UnhandledExceptionException: If no warning collector was found along the call-hierarchy to collect and 

298 handle an exception. 

299 :raises UnhandledCriticalWarningException: If no warning collector was found along the call-hierarchy to collect and 

300 handle a critical warning. 

301 """ 

302 global _threadLocalData 

303 

304 if cause is not None: 304 ↛ 305line 304 didn't jump to line 305 because the condition on line 304 was never true

305 warning.__cause__ = cause 

306 

307 if notes is not None: 307 ↛ 308line 307 didn't jump to line 308 because the condition on line 307 was never true

308 if isinstance(notes, str): 

309 warning.add_note(notes) 

310 else: 

311 for note in notes: 

312 warning.add_note(note) 

313 

314 try: 

315 warningCollector = _threadLocalData.warningCollector 

316 if warningCollector.AddWarning(warning): 

317 raise Exception(f"Warning: {warning}") from warning 

318 except AttributeError: 

319 ex = None 

320 if isinstance(warning, Exception): 

321 ex = UnhandledExceptionException(f"Unhandled Exception: {warning}") 

322 elif isinstance(warning, CriticalWarning): 

323 ex = UnhandledCriticalWarningException(f"Unhandled Critical Warning: {warning}") 

324 

325 if ex is not None: 

326 ex.add_note(f"Add a 'with'-statement using '{cls.__name__}' somewhere up the call-hierarchy to receive and collect warnings.") 

327 raise ex from warning