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
« 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.
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
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 """
64@export
65class CriticalWarning(BaseException):
66 """
67 Base-exception of all critical warnings handled by :class:`WarningCollector`.
69 .. tip::
71 Critical warnings must be unhandled within a call hierarchy, otherwise a :exc:`UnhandledCriticalWarningException`
72 will be raised.
73 """
76@export
77class UnhandledWarningException(ExceptionBase): # FIXME: to be removed in v9.0.0
78 """
79 Deprecated.
81 .. deprecated:: v9.0.0
83 Please use :exc:`UnhandledCriticalWarningException`.
84 """
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 """
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 """
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.
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.
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
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
139 self._parent = None
140 self._warnings = warnings
141 self._handler = handler
143 def __len__(self) -> int:
144 """
145 Returns the number of collected warnings.
147 :returns: Number of collected warnings.
148 """
149 return len(self._warnings)
151 def __iter__(self) -> Iterator[BaseException]:
152 return iter(self._warnings)
154 def __getitem__(self, index: int) -> BaseException:
155 return self._warnings[index]
157 def __enter__(self) -> Self:
158 """
159 Enter the warning collector context.
161 :returns: The warning collector instance.
162 """
163 global _threadLocalData
165 try:
166 self.Parent = _threadLocalData.warningCollector
167 except AttributeError:
168 pass
170 _threadLocalData.warningCollector = self
172 return self
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.
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
190 _threadLocalData.warningCollector = self._parent
192 @property
193 def Parent(self) -> Nullable["WarningCollector"]:
194 """
195 Property to access the parent warning collected.
197 :returns: The parent warning collector or ``None``.
198 """
199 return self._parent
201 @Parent.setter
202 def Parent(self, value: "WarningCollector") -> None:
203 self._parent = value
205 @readonly
206 def Warnings(self) -> List[BaseException]:
207 """
208 Read-only property to access the list of collected warnings.
210 :returns: A list of collected warnings.
211 """
212 return self._warnings
214 def AddWarning(self, warning: BaseException) -> bool:
215 """
216 Add a warning to the list of warnings managed by this warning collector.
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
231 self._warnings.append(warning)
233 return False if self._handler is None else self._handler(warning)
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.
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}")
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