Coverage for pyTooling/Warning/__init__.py: 85%
90 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-10 11:38 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-10 11:38 +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:: See :ref:`high-level help <WARNING>` for explanations and usage examples.
35"""
36from threading import local
37from types import TracebackType
38from typing import List, Callable, Optional as Nullable, Type, Iterator
40try:
41 from pyTooling.Decorators import export, readonly
42 from pyTooling.Common import getFullyQualifiedName
43 from pyTooling.Exceptions import ExceptionBase
44except ModuleNotFoundError: # pragma: no cover
45 print("[pyTooling.Common] Could not import from 'pyTooling.*'!")
47 try:
48 from Decorators import export, readonly
49 from Common import getFullyQualifiedName
50 from Exceptions import ExceptionBase
51 except ModuleNotFoundError as ex: # pragma: no cover
52 print("[pyTooling.Common] Could not import directly!")
53 raise ex
56_threadLocalData = local()
59@export
60class Warning(BaseException):
61 pass
64@export
65class CriticalWarning(BaseException):
66 pass
69@export
70class UnhandledWarningException(ExceptionBase): # FIXME: to be removed in v9.0.0
71 pass
74@export
75class UnhandledCriticalWarningException(UnhandledWarningException):
76 pass
79@export
80class UnhandledExceptionException(UnhandledWarningException):
81 pass
84@export
85class WarningCollector:
86 """
87 A context manager to collect warnings within the call hierarchy.
88 """
89 _parent: Nullable["WarningCollector"] #: Parent WarningCollector
90 _warnings: List[BaseException] #: List of collected warnings (and exceptions).
91 _handler: Nullable[Callable[[BaseException], bool]] #: Optional handler function, which is called per collected warning.
93 def __init__(
94 self,
95 warnings: Nullable[List[BaseException]] = None,
96 handler: Nullable[Callable[[BaseException], bool]] = None
97 ) -> None:
98 """
99 Initializes a warning collector.
101 :param warnings: An optional reference to a list of warnings, which can be modified (appended) by this warning
102 collector. If ``None``, an internal list is created and can be referenced by the collector's
103 instance.
104 :param handler: An optional handler function, which processes the current warning and decides if a warning should
105 be reraised as an exception.
106 :raises TypeError: If optional parameter 'warnings' is not of type list.
107 :raises TypeError: If optional parameter 'handler' is not a callable.
108 """
109 if warnings is None:
110 warnings = []
111 elif not isinstance(warnings, list): 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 ex = TypeError(f"Parameter 'warnings' is not list.")
113 ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.")
114 raise ex
116 if handler is not None and not isinstance(handler, Callable): 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 ex = TypeError(f"Parameter 'handler' is not callable.")
118 ex.add_note(f"Got type '{getFullyQualifiedName(handler)}'.")
119 raise ex
121 self._parent = None
122 self._warnings = warnings
123 self._handler = handler
125 def __len__(self) -> int:
126 return len(self._warnings)
128 def __iter__(self) -> Iterator[BaseException]:
129 return iter(self._warnings)
131 def __getitem__(self, index: int) -> BaseException:
132 return self._warnings[index]
134 def __enter__(self) -> 'WarningCollector': # -> Self: needs Python 3.11
135 """
136 Enter the warning collector context.
138 :returns: The warning collector instance.
139 """
140 global _threadLocalData
142 try:
143 self.Parent = _threadLocalData.warningCollector
144 except AttributeError:
145 pass
147 _threadLocalData.warningCollector = self
149 return self
151 def __exit__(
152 self,
153 exc_type: Nullable[Type[BaseException]] = None,
154 exc_val: Nullable[BaseException] = None,
155 exc_tb: Nullable[TracebackType] = None
156 ) -> Nullable[bool]:
157 """
158 Exit the warning collector context.
160 :param exc_type: Exception type
161 :param exc_val: Exception instance
162 :param exc_tb: Exception's traceback.
163 """
164 global _threadLocalData
166 _threadLocalData.warningCollector = self._parent
168 @property
169 def Parent(self) -> Nullable["WarningCollector"]:
170 return self._parent
172 @Parent.setter
173 def Parent(self, value: "WarningCollector") -> None:
174 self._parent = value
176 @readonly
177 def Warnings(self) -> List[BaseException]:
178 """
179 Read-only property to access the list of collected warnings.
181 :returns: A list of collected warnings.
182 """
183 return self._warnings
185 def AddWarning(self, warning: BaseException) -> bool:
186 """
187 Add a warning to the list of warnings managed by this warning collector.
189 :param warning: The warning to add to the collectors internal warning list.
190 :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned
191 ``True``; otherwise ``False``.
192 :raises ValueError: If parameter ``warning`` is None.
193 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`.
194 """
195 if warning is None: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 raise ValueError("Parameter 'warning' is None.")
197 elif not isinstance(warning, (Warning, CriticalWarning, Exception)): 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true
198 ex = TypeError(f"Parameter 'warning' is not of type 'Warning', 'CriticalWarning' or 'Exception'.")
199 ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.")
200 raise ex
202 self._warnings.append(warning)
204 return False if self._handler is None else self._handler(warning)
206 @classmethod
207 def Raise(cls, warning: BaseException) -> None:
208 """
209 Walk the callstack frame by frame upwards and search for the first warning collector.
211 :param warning: Warning to send upwards in the call stack.
212 :raises Exception: If warning should be converted to an exception.
213 :raises Exception: If the call-stack walk couldn't find a warning collector.
214 """
215 global _threadLocalData
216 try:
217 warningCollector = _threadLocalData.warningCollector
218 if warningCollector.AddWarning(warning):
219 raise Exception(f"Warning: {warning}") from warning
220 except AttributeError:
221 ex = None
222 if isinstance(warning, Exception):
223 ex = UnhandledExceptionException(f"Unhandled Exception: {warning}")
224 elif isinstance(warning, CriticalWarning):
225 ex = UnhandledCriticalWarningException(f"Unhandled Critical Warning: {warning}")
227 if ex is not None:
228 ex.add_note(f"Add a 'with'-statement using '{cls.__name__}' somewhere up the call-hierarchy to receive and collect warnings.")
229 raise ex from warning