Coverage for pyTooling / Warning / __init__.py: 85%
87 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 12:48 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 12:48 +0000
1# ==================================================================================================================== #
2# _____ _ _ __ __ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /_ _ _ __ _ __ (_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ /\ / / _` | '__| '_ \| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V V / (_| | | | | | | | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/\_/ \__,_|_| |_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2025-2025 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
42try:
43 from pyTooling.Decorators import export, readonly
44 from pyTooling.Common import getFullyQualifiedName
45 from pyTooling.Exceptions import ExceptionBase
46except ModuleNotFoundError: # pragma: no cover
47 print("[pyTooling.Warning] Could not import from 'pyTooling.*'!")
49 try:
50 from Decorators import export, readonly
51 from Common import getFullyQualifiedName
52 from Exceptions import ExceptionBase
53 except ModuleNotFoundError as ex: # pragma: no cover
54 print("[pyTooling.Warning] Could not import directly!")
55 raise ex
58__all__ = ["_threadLocalData"]
60_threadLocalData = local()
61"""A reference to the thread local data needed by the pyTooling.Warning classes."""
64@export
65class Warning(BaseException):
66 """
67 Base-exception of all warnings handled by :class:`WarningCollector`.
69 .. tip::
71 Warnings can be unhandled within a call hierarchy.
72 """
75@export
76class CriticalWarning(BaseException):
77 """
78 Base-exception of all critical warnings handled by :class:`WarningCollector`.
80 .. tip::
82 Critical warnings must be unhandled within a call hierarchy, otherwise a :exc:`UnhandledCriticalWarningException`
83 will be raised.
84 """
87@export
88class UnhandledWarningException(ExceptionBase): # FIXME: to be removed in v9.0.0
89 """
90 Deprecated.
92 .. deprecated:: v9.0.0
94 Please use :exc:`UnhandledCriticalWarningException`.
95 """
98@export
99class UnhandledCriticalWarningException(UnhandledWarningException):
100 """
101 This exception is raised when a critical warning isn't handled by a :class:`WarningCollector` within the
102 call-hierarchy.
103 """
106@export
107class UnhandledExceptionException(UnhandledWarningException):
108 """
109 This exception is raised when an exception isn't handled by a :class:`WarningCollector` within the call-hierarchy.
110 """
113@export
114class WarningCollector:
115 """
116 A context manager to collect warnings within the call hierarchy.
117 """
118 _parent: Nullable["WarningCollector"] #: Parent WarningCollector
119 _warnings: List[BaseException] #: List of collected warnings (and exceptions).
120 _handler: Nullable[Callable[[BaseException], bool]] #: Optional handler function, which is called per collected warning.
122 def __init__(
123 self,
124 warnings: Nullable[List[BaseException]] = None,
125 handler: Nullable[Callable[[BaseException], bool]] = None
126 ) -> None:
127 """
128 Initializes a warning collector.
130 :param warnings: An optional reference to a list of warnings, which can be modified (appended) by this warning
131 collector. If ``None``, an internal list is created and can be referenced by the collector's
132 instance.
133 :param handler: An optional handler function, which processes the current warning and decides if a warning should
134 be reraised as an exception.
135 :raises TypeError: If optional parameter 'warnings' is not of type list.
136 :raises TypeError: If optional parameter 'handler' is not a callable.
137 """
138 if warnings is None:
139 warnings = []
140 elif not isinstance(warnings, list): 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 ex = TypeError(f"Parameter 'warnings' is not list.")
142 ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.")
143 raise ex
145 if handler is not None and not isinstance(handler, Callable): 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 ex = TypeError(f"Parameter 'handler' is not callable.")
147 ex.add_note(f"Got type '{getFullyQualifiedName(handler)}'.")
148 raise ex
150 self._parent = None
151 self._warnings = warnings
152 self._handler = handler
154 def __len__(self) -> int:
155 """
156 Returns the number of collected warnings.
158 :returns: Number of collected warnings.
159 """
160 return len(self._warnings)
162 def __iter__(self) -> Iterator[BaseException]:
163 return iter(self._warnings)
165 def __getitem__(self, index: int) -> BaseException:
166 return self._warnings[index]
168 def __enter__(self) -> Self:
169 """
170 Enter the warning collector context.
172 :returns: The warning collector instance.
173 """
174 global _threadLocalData
176 try:
177 self.Parent = _threadLocalData.warningCollector
178 except AttributeError:
179 pass
181 _threadLocalData.warningCollector = self
183 return self
185 def __exit__(
186 self,
187 exc_type: Nullable[Type[BaseException]] = None,
188 exc_val: Nullable[BaseException] = None,
189 exc_tb: Nullable[TracebackType] = None
190 ) -> Nullable[bool]:
191 """
192 Exit the warning collector context.
194 :param exc_type: Exception type
195 :param exc_val: Exception instance
196 :param exc_tb: Exception's traceback.
197 :returns: ``None``
198 """
199 global _threadLocalData
201 _threadLocalData.warningCollector = self._parent
203 @property
204 def Parent(self) -> Nullable["WarningCollector"]:
205 """
206 Property to access the parent warning collected.
208 :returns: The parent warning collector or ``None``.
209 """
210 return self._parent
212 @Parent.setter
213 def Parent(self, value: "WarningCollector") -> None:
214 self._parent = value
216 @readonly
217 def Warnings(self) -> List[BaseException]:
218 """
219 Read-only property to access the list of collected warnings.
221 :returns: A list of collected warnings.
222 """
223 return self._warnings
225 def AddWarning(self, warning: BaseException) -> bool:
226 """
227 Add a warning to the list of warnings managed by this warning collector.
229 :param warning: The warning to add to the collectors internal warning list.
230 :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned
231 ``True``; otherwise ``False``.
232 :raises ValueError: If parameter ``warning`` is None.
233 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`.
234 """
235 if warning is None: 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true
236 raise ValueError("Parameter 'warning' is None.")
237 elif not isinstance(warning, (Warning, CriticalWarning, Exception)): 237 ↛ 238line 237 didn't jump to line 238 because the condition on line 237 was never true
238 ex = TypeError(f"Parameter 'warning' is not of type 'Warning', 'CriticalWarning' or 'Exception'.")
239 ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.")
240 raise ex
242 self._warnings.append(warning)
244 return False if self._handler is None else self._handler(warning)
246 @classmethod
247 def Raise(cls, warning: BaseException) -> None:
248 """
249 Walk the callstack frame by frame upwards and search for the first warning collector.
251 :param warning: Warning to send upwards in the call stack.
252 :raises Exception: If warning should be converted to an exception.
253 :raises Exception: If the call-stack walk couldn't find a warning collector.
254 """
255 global _threadLocalData
256 try:
257 warningCollector = _threadLocalData.warningCollector
258 if warningCollector.AddWarning(warning):
259 raise Exception(f"Warning: {warning}") from warning
260 except AttributeError:
261 ex = None
262 if isinstance(warning, Exception):
263 ex = UnhandledExceptionException(f"Unhandled Exception: {warning}")
264 elif isinstance(warning, CriticalWarning):
265 ex = UnhandledCriticalWarningException(f"Unhandled Critical Warning: {warning}")
267 if ex is not None:
268 ex.add_note(f"Add a 'with'-statement using '{cls.__name__}' somewhere up the call-hierarchy to receive and collect warnings.")
269 raise ex from warning