Coverage for pyTooling/Warning/__init__.py: 82%
48 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 21:48 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 21: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:: See :ref:`high-level help <WARNING>` for explanations and usage examples.
35"""
36from inspect import currentframe
37from sys import version_info
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
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 except ModuleNotFoundError as ex: # pragma: no cover
51 print("[pyTooling.Common] Could not import directly!")
52 raise ex
55@export
56class WarningCollector:
57 """
58 A context manager to collect warnings within the call hierarchy.
59 """
60 _warnings: List[Exception] #: List of collected warnings.
61 _handler: Nullable[Callable[[Exception], bool]] #: Optional handler function, which is called per collected warning.
63 def __init__(self, warnings: Nullable[List[Exception]] = None, handler: Nullable[Callable[[Exception], bool]] = None) -> None:
64 """
65 Initializes a warning collector.
67 :param warnings: An optional reference to a list of warnings, which can be modified (appended) by this warning
68 collector. If ``None``, an internal list is created and can be referenced by the collector's
69 instance.
70 :param handler: An optional handler function, which processes the current warning and decides if a warning should
71 be reraised as an exception.
72 :raises TypeError: If optional parameter 'warnings' is not of type list.
73 :raises TypeError: If optional parameter 'handler' is not a callable.
74 """
75 if warnings is None:
76 warnings = []
77 elif not isinstance(warnings, list): 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true
78 ex = TypeError(f"Parameter 'warnings' is not list.")
79 if version_info >= (3, 11): # pragma: no cover
80 ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.")
81 raise ex
83 if handler is not None and not isinstance(handler, Callable): 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 ex = TypeError(f"Parameter 'handler' is not callable.")
85 if version_info >= (3, 11): # pragma: no cover
86 ex.add_note(f"Got type '{getFullyQualifiedName(handler)}'.")
87 raise ex
89 self._warnings = warnings
90 self._handler = handler
92 def __enter__(self) -> 'WarningCollector': # -> Self: needs Python 3.11
93 """
94 Enter the warning collector context.
96 :returns: The warning collector instance.
97 """
98 return self
100 def __exit__(
101 self,
102 exc_type: Nullable[Type[BaseException]] = None,
103 exc_val: Nullable[BaseException] = None,
104 exc_tb: Nullable[TracebackType] = None
105 ) -> Nullable[bool]:
106 """
107 Exit the warning collector context.
109 :param exc_type: Exception type
110 :param exc_val: Exception instance
111 :param exc_tb: Exception's traceback.
112 """
114 # outerFrame = currentframe().f_back
115 # print("__exit__:")
116 # for l in outerFrame.f_locals:
117 # print(f" {l}")
118 #
119 # outerFrame.f_locals.pop("ctx")
121 @readonly
122 def Warnings(self) -> List[Exception]:
123 """
124 Read-only property to access the list of collected warnings.
126 :returns: A list of collected warnings.
127 """
128 return self._warnings
130 def AddWarning(self, warning: Exception) -> bool:
131 """
132 Add a warning to the list of warnings managed by this warning collector.
134 :param warning: The warning to add to the collectors internal warning list.
135 :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned
136 ``True``; otherwise ``False``.
137 :raises ValueError: If parameter ``warning`` is None.
138 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`.
139 """
140 if self._warnings is None: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 raise ValueError("Parameter 'warning' is None.")
142 elif self._warnings is None or not isinstance(warning, Exception): 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true
143 ex = TypeError(f"Parameter 'warning' is not of type 'Warning'.")
144 if version_info >= (3, 11): # pragma: no cover
145 ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.")
146 raise ex
148 self._warnings.append(warning)
150 if self._handler is not None:
151 return self._handler(warning)
153 return False
155 @classmethod
156 def Raise(cls, warning: Exception) -> None:
157 """
158 Walk the callstack frame by frame upwards and search for the first warning collector.
160 :param warning: Warning to send upwards in the call stack.
161 :raises Exception: If warning should be converted to an exception.
162 :raises Exception: If the call-stack walk couldn't find a warning collector.
163 """
164 frame = currentframe()
165 while frame := frame.f_back:
166 for localValue in reversed(frame.f_locals.values()):
167 if isinstance(localValue, cls):
168 if localValue.AddWarning(warning):
169 raise Exception(f"Warning: {warning}") from warning
170 return
171 else:
172 raise Exception(f"Unhandled warning: {warning}") from warning