Coverage for pyTooling/Warning/__init__.py: 80%
68 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 22:23 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 22:23 +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 sys import version_info
37from threading import local
38from types import TracebackType
39from typing import List, Callable, Optional as Nullable, Type
41try:
42 from pyTooling.Decorators import export, readonly
43 from pyTooling.Common import getFullyQualifiedName
44 from pyTooling.Exceptions import ExceptionBase
45except ModuleNotFoundError: # pragma: no cover
46 print("[pyTooling.Common] Could not import from 'pyTooling.*'!")
48 try:
49 from Decorators import export, readonly
50 from Common import getFullyQualifiedName
51 from Exceptions import ExceptionBase
52 except ModuleNotFoundError as ex: # pragma: no cover
53 print("[pyTooling.Common] Could not import directly!")
54 raise ex
57_threadLocalData = local()
60@export
61class UnhandledWarningException(ExceptionBase):
62 pass
65@export
66class WarningCollector:
67 """
68 A context manager to collect warnings within the call hierarchy.
69 """
70 _parent: Nullable["WarningCollector"] #: Parent WarningCollector
71 _warnings: List[Exception] #: List of collected warnings.
72 _handler: Nullable[Callable[[Exception], bool]] #: Optional handler function, which is called per collected warning.
74 def __init__(self, warnings: Nullable[List[Exception]] = None, handler: Nullable[Callable[[Exception], bool]] = None) -> None:
75 """
76 Initializes a warning collector.
78 :param warnings: An optional reference to a list of warnings, which can be modified (appended) by this warning
79 collector. If ``None``, an internal list is created and can be referenced by the collector's
80 instance.
81 :param handler: An optional handler function, which processes the current warning and decides if a warning should
82 be reraised as an exception.
83 :raises TypeError: If optional parameter 'warnings' is not of type list.
84 :raises TypeError: If optional parameter 'handler' is not a callable.
85 """
86 if warnings is None:
87 warnings = []
88 elif not isinstance(warnings, list): 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 ex = TypeError(f"Parameter 'warnings' is not list.")
90 ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.")
91 raise ex
93 if handler is not None and not isinstance(handler, Callable): 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true
94 ex = TypeError(f"Parameter 'handler' is not callable.")
95 ex.add_note(f"Got type '{getFullyQualifiedName(handler)}'.")
96 raise ex
98 self._parent = None
99 self._warnings = warnings
100 self._handler = handler
102 def __enter__(self) -> 'WarningCollector': # -> Self: needs Python 3.11
103 """
104 Enter the warning collector context.
106 :returns: The warning collector instance.
107 """
108 global _threadLocalData
110 try:
111 self.Parent = _threadLocalData.warningCollector
112 except AttributeError:
113 pass
115 _threadLocalData.warningCollector = self
117 return self
119 def __exit__(
120 self,
121 exc_type: Nullable[Type[BaseException]] = None,
122 exc_val: Nullable[BaseException] = None,
123 exc_tb: Nullable[TracebackType] = None
124 ) -> Nullable[bool]:
125 """
126 Exit the warning collector context.
128 :param exc_type: Exception type
129 :param exc_val: Exception instance
130 :param exc_tb: Exception's traceback.
131 """
132 global _threadLocalData
134 _threadLocalData.warningCollector = self._parent
136 @property
137 def Parent(self) -> Nullable["WarningCollector"]:
138 return self._parent
140 @Parent.setter
141 def Parent(self, value: "WarningCollector") -> None:
142 self._parent = value
144 @readonly
145 def Warnings(self) -> List[Exception]:
146 """
147 Read-only property to access the list of collected warnings.
149 :returns: A list of collected warnings.
150 """
151 return self._warnings
153 def AddWarning(self, warning: Exception) -> bool:
154 """
155 Add a warning to the list of warnings managed by this warning collector.
157 :param warning: The warning to add to the collectors internal warning list.
158 :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned
159 ``True``; otherwise ``False``.
160 :raises ValueError: If parameter ``warning`` is None.
161 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`.
162 """
163 if self._warnings is None: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 raise ValueError("Parameter 'warning' is None.")
165 elif self._warnings is None or not isinstance(warning, Exception): 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 ex = TypeError(f"Parameter 'warning' is not of type 'Warning'.")
167 ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.")
168 raise ex
170 self._warnings.append(warning)
172 if self._handler is not None:
173 return self._handler(warning)
175 return False
177 @classmethod
178 def Raise(cls, warning: Exception) -> None:
179 """
180 Walk the callstack frame by frame upwards and search for the first warning collector.
182 :param warning: Warning to send upwards in the call stack.
183 :raises Exception: If warning should be converted to an exception.
184 :raises Exception: If the call-stack walk couldn't find a warning collector.
185 """
186 global _threadLocalData
187 try:
188 warningCollector = _threadLocalData.warningCollector
189 if warningCollector.AddWarning(warning):
190 raise Exception(f"Warning: {warning}") from warning
191 except AttributeError:
192 raise UnhandledWarningException(f"Unhandled warning: {warning}") from warning