Coverage for pyTooling/Warning/__init__.py: 84%
65 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 18:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 18:22 +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 if version_info >= (3, 11): # pragma: no cover
91 ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.")
92 raise ex
94 if handler is not None and not isinstance(handler, Callable): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 ex = TypeError(f"Parameter 'handler' is not callable.")
96 if version_info >= (3, 11): # pragma: no cover
97 ex.add_note(f"Got type '{getFullyQualifiedName(handler)}'.")
98 raise ex
100 self._parent = None
101 self._warnings = warnings
102 self._handler = handler
104 def __enter__(self) -> 'WarningCollector': # -> Self: needs Python 3.11
105 """
106 Enter the warning collector context.
108 :returns: The warning collector instance.
109 """
110 global _threadLocalData
112 try:
113 self.Parent = _threadLocalData.warningCollector
114 except AttributeError:
115 pass
117 _threadLocalData.warningCollector = self
119 return self
121 def __exit__(
122 self,
123 exc_type: Nullable[Type[BaseException]] = None,
124 exc_val: Nullable[BaseException] = None,
125 exc_tb: Nullable[TracebackType] = None
126 ) -> Nullable[bool]:
127 """
128 Exit the warning collector context.
130 :param exc_type: Exception type
131 :param exc_val: Exception instance
132 :param exc_tb: Exception's traceback.
133 """
134 global _threadLocalData
136 _threadLocalData.warningCollector = self._parent
138 @property
139 def Parent(self) -> Nullable["WarningCollector"]:
140 return self._parent
142 @Parent.setter
143 def Parent(self, value: "WarningCollector") -> None:
144 self._parent = value
146 @readonly
147 def Warnings(self) -> List[Exception]:
148 """
149 Read-only property to access the list of collected warnings.
151 :returns: A list of collected warnings.
152 """
153 return self._warnings
155 def AddWarning(self, warning: Exception) -> bool:
156 """
157 Add a warning to the list of warnings managed by this warning collector.
159 :param warning: The warning to add to the collectors internal warning list.
160 :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned
161 ``True``; otherwise ``False``.
162 :raises ValueError: If parameter ``warning`` is None.
163 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`.
164 """
165 if self._warnings is None: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 raise ValueError("Parameter 'warning' is None.")
167 elif self._warnings is None or not isinstance(warning, Exception): 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true
168 ex = TypeError(f"Parameter 'warning' is not of type 'Warning'.")
169 if version_info >= (3, 11): # pragma: no cover
170 ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.")
171 raise ex
173 self._warnings.append(warning)
175 if self._handler is not None:
176 return self._handler(warning)
178 return False
180 @classmethod
181 def Raise(cls, warning: Exception) -> None:
182 """
183 Walk the callstack frame by frame upwards and search for the first warning collector.
185 :param warning: Warning to send upwards in the call stack.
186 :raises Exception: If warning should be converted to an exception.
187 :raises Exception: If the call-stack walk couldn't find a warning collector.
188 """
189 global _threadLocalData
190 try:
191 warningCollector = _threadLocalData.warningCollector
192 if warningCollector.AddWarning(warning):
193 raise Exception(f"Warning: {warning}") from warning
194 except AttributeError:
195 raise UnhandledWarningException(f"Unhandled warning: {warning}") from warning