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