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
« 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.
34.. hint::
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
42from pyTooling.Decorators import export, readonly
43from pyTooling.Common import getFullyQualifiedName
44from pyTooling.Exceptions import ExceptionBase
47__all__ = ["_threadLocalData"]
49_threadLocalData = local()
50"""A reference to the thread local data needed by the pyTooling.Warning classes."""
53@export
54class Warning(BaseException):
55 """
56 Base-exception of all warnings handled by :class:`WarningCollector`.
58 .. tip::
60 Warnings can be unhandled within a call hierarchy.
61 """
63 @readonly
64 def HasNotes(self) -> bool:
65 """
66 Read-only property to return if the warning has attached notes.
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
72 @readonly
73 def Notes(self) -> Tuple[str, ...]:
74 """
75 Read-only property to access warning's attached notes.
77 :returns: Attached notes.
78 """
79 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()
82@export
83class CriticalWarning(BaseException):
84 """
85 Base-exception of all critical warnings handled by :class:`WarningCollector`.
87 .. tip::
89 Critical warnings must be unhandled within a call hierarchy, otherwise a :exc:`UnhandledCriticalWarningException`
90 will be raised.
91 """
93 @readonly
94 def HasNotes(self) -> bool:
95 """
96 Read-only property to return if the warning has attached notes.
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
102 @readonly
103 def Notes(self) -> Tuple[str, ...]:
104 """
105 Read-only property to access warning's attached notes.
107 :returns: Attached notes.
108 """
109 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()
112@export
113class UnhandledWarningException(ExceptionBase): # FIXME: to be removed in v9.0.0
114 """
115 Deprecated.
117 .. deprecated:: v9.0.0
119 Please use :exc:`UnhandledCriticalWarningException`.
120 """
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 """
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 """
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.
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.
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
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
175 self._parent = None
176 self._warnings = warnings
177 self._handler = handler
179 def __len__(self) -> int:
180 """
181 Returns the number of collected warnings.
183 :returns: Number of collected warnings.
184 """
185 return len(self._warnings)
187 def __iter__(self) -> Iterator[Warning | CriticalWarning | Exception]:
188 """
189 Return an iterator over all collected warnings.
191 :returns: Iterator over the collected warnings.
192 """
193 return iter(self._warnings)
195 def __getitem__(self, index: int) -> Warning | CriticalWarning | Exception:
196 """
197 Access a collected warning by index.
199 :param index: Index of the warning.
200 :returns: Collected warning.
201 """
202 return self._warnings[index]
204 def __enter__(self) -> Self:
205 """
206 Enter the warning collector context.
208 :returns: The warning collector instance.
209 """
210 global _threadLocalData
212 try:
213 self.Parent = _threadLocalData.warningCollector
214 except AttributeError:
215 pass
217 _threadLocalData.warningCollector = self
219 return self
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.
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
237 _threadLocalData.warningCollector = self._parent
239 @property
240 def Parent(self) -> Nullable[Self]:
241 """
242 Property to access the parent warning collector.
244 :returns: The parent warning collector or ``None``.
245 """
246 return self._parent
248 @Parent.setter
249 def Parent(self, value: Self) -> None:
250 self._parent = value
252 @readonly
253 def Warnings(self) -> List[Warning | CriticalWarning | Exception]:
254 """
255 Read-only property to access the list of collected warnings.
257 :returns: A list of collected warnings.
258 """
259 return self._warnings
261 def AddWarning(self, warning: Warning | CriticalWarning | Exception) -> bool:
262 """
263 Add a warning to the list of warnings managed by this warning collector.
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
278 self._warnings.append(warning)
280 return False if self._handler is None else self._handler(warning)
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.
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
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
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)
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}")
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