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

86 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-20 22:29 +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 

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 

64@export 

65class CriticalWarning(BaseException): 

66 """ 

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

68 

69 .. tip:: 

70 

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

72 will be raised. 

73 """ 

74 

75 

76@export 

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

78 """ 

79 Deprecated. 

80 

81 .. deprecated:: v9.0.0 

82 

83 Please use :exc:`UnhandledCriticalWarningException`. 

84 """ 

85 

86 

87@export 

88class UnhandledCriticalWarningException(UnhandledWarningException): 

89 """ 

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

91 call-hierarchy. 

92 """ 

93 

94 

95@export 

96class UnhandledExceptionException(UnhandledWarningException): 

97 """ 

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

99 """ 

100 

101 

102@export 

103class WarningCollector: 

104 """ 

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

106 """ 

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

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

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

110 

111 def __init__( 

112 self, 

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

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

115 ) -> None: 

116 """ 

117 Initializes a warning collector. 

118 

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

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

121 instance. 

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

123 be reraised as an exception. 

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

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

126 """ 

127 if warnings is None: 

128 warnings = [] 

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

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

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

132 raise ex 

133 

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

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

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

137 raise ex 

138 

139 self._parent = None 

140 self._warnings = warnings 

141 self._handler = handler 

142 

143 def __len__(self) -> int: 

144 """ 

145 Returns the number of collected warnings. 

146 

147 :returns: Number of collected warnings. 

148 """ 

149 return len(self._warnings) 

150 

151 def __iter__(self) -> Iterator[BaseException]: 

152 return iter(self._warnings) 

153 

154 def __getitem__(self, index: int) -> BaseException: 

155 return self._warnings[index] 

156 

157 def __enter__(self) -> Self: 

158 """ 

159 Enter the warning collector context. 

160 

161 :returns: The warning collector instance. 

162 """ 

163 global _threadLocalData 

164 

165 try: 

166 self.Parent = _threadLocalData.warningCollector 

167 except AttributeError: 

168 pass 

169 

170 _threadLocalData.warningCollector = self 

171 

172 return self 

173 

174 def __exit__( 

175 self, 

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

177 exc_val: Nullable[BaseException] = None, 

178 exc_tb: Nullable[TracebackType] = None 

179 ) -> Nullable[bool]: 

180 """ 

181 Exit the warning collector context. 

182 

183 :param exc_type: Exception type 

184 :param exc_val: Exception instance 

185 :param exc_tb: Exception's traceback. 

186 :returns: ``None`` 

187 """ 

188 global _threadLocalData 

189 

190 _threadLocalData.warningCollector = self._parent 

191 

192 @property 

193 def Parent(self) -> Nullable["WarningCollector"]: 

194 """ 

195 Property to access the parent warning collected. 

196 

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

198 """ 

199 return self._parent 

200 

201 @Parent.setter 

202 def Parent(self, value: "WarningCollector") -> None: 

203 self._parent = value 

204 

205 @readonly 

206 def Warnings(self) -> List[BaseException]: 

207 """ 

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

209 

210 :returns: A list of collected warnings. 

211 """ 

212 return self._warnings 

213 

214 def AddWarning(self, warning: BaseException) -> bool: 

215 """ 

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

217 

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

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

220 ``True``; otherwise ``False``. 

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

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

223 """ 

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

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

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

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

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

229 raise ex 

230 

231 self._warnings.append(warning) 

232 

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

234 

235 @classmethod 

236 def Raise(cls, warning: BaseException) -> None: 

237 """ 

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

239 

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

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

242 :raises Exception: If the call-stack walk couldn't find a warning collector. 

243 """ 

244 global _threadLocalData 

245 try: 

246 warningCollector = _threadLocalData.warningCollector 

247 if warningCollector.AddWarning(warning): 

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

249 except AttributeError: 

250 ex = None 

251 if isinstance(warning, Exception): 

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

253 elif isinstance(warning, CriticalWarning): 

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

255 

256 if ex is not None: 

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

258 raise ex from warning