Coverage for pyTooling/Exceptions/__init__.py: 76%
76 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-23 19:20 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-23 19:20 +0000
1# ==================================================================================================================== #
2# #
3# _____ _ _ _____ _ _ #
4# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | ____|_ _____ ___ _ __ | |_(_) ___ _ __ ___ #
5# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | _| \ \/ / __/ _ \ '_ \| __| |/ _ \| '_ \/ __| #
6# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___ > < (_| __/ |_) | |_| | (_) | | | \__ \ #
7# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____/_/\_\___\___| .__/ \__|_|\___/|_| |_|___/ #
8# |_| |___/ |___/ |_| #
9# ==================================================================================================================== #
10# Authors: #
11# Patrick Lehmann #
12# #
13# License: #
14# ==================================================================================================================== #
15# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany #
16# #
17# Licensed under the Apache License, Version 2.0 (the "License"); #
18# you may not use this file except in compliance with the License. #
19# You may obtain a copy of the License at #
20# #
21# http://www.apache.org/licenses/LICENSE-2.0 #
22# #
23# Unless required by applicable law or agreed to in writing, software #
24# distributed under the License is distributed on an "AS IS" BASIS, #
25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
26# See the License for the specific language governing permissions and #
27# limitations under the License. #
28# #
29# SPDX-License-Identifier: Apache-2.0 #
30# ==================================================================================================================== #
31#
32"""
33A common set of missing exceptions in Python.
35.. hint::
37 See :ref:`high-level help <EXECPTION>` for explanations and usage examples.
39.. seealso::
41 :mod:`pyTooling.Warning`
42 |rarr| Warnings, which are collected instead of raised.
43 :mod:`pyTooling.MetaClasses`
44 |rarr| The exceptions raised for a class that violates the meta-class' rules.
45"""
46from typing import ClassVar, Iterable, Any, Optional as Nullable
47from pyTooling.Decorators import export, readonly
50@export
51def addNoteWithItemList(
52 ex: BaseException,
53 message: str,
54 items: Iterable[Any],
55 *,
56 indent: str = " ",
57 separator: str = ", ",
58 maxWidth: int = 100
59) -> None:
60 """
61 Add a message as a note to the exception. The iterables items are added as a coma separated list. If the list gets too
62 long, remaining items will be continued in addition notes.
64 :param ex: Exception to attach the note to.
65 :param message: Optional, the message of the note.
66 :param items: An iterable of items to add to the note.
67 :param indent: Optional, the indentation of the additional notes.
68 :param separator: Optional, separator between items.
69 :param maxWidth: Optional, the maximum width of the attached notes.
70 """
71 note = message
72 sep = ""
74 for item in items:
75 if len(note) + len(newItem := f"{sep}{item}") <= maxWidth:
76 note += newItem
77 sep = separator
78 else:
79 ex.add_note(note)
81 note = f"{indent}{item}"
82 sep = separator
84 ex.add_note(note)
87@export
88class OverloadResolutionError(Exception):
89 """
90 The exception is raised, when no matching overloaded method was found.
92 .. attention::
94 Method overloading is not implemented yet - the ``overloadable`` decorator and the dispatching machinery are
95 commented out in :mod:`pyTooling.MetaClasses`. Nothing raises this exception today; it is declared so the feature
96 has its exception when it arrives.
97 """
99 @readonly
100 def HasNotes(self) -> bool:
101 """
102 Read-only property to return if the warning has attached notes.
104 :returns: True, if the warning has attached notes.
105 """
106 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0
108 @readonly
109 def Notes(self) -> tuple[str, ...]:
110 """
111 Read-only property to return warning's attached notes.
113 :returns: Attached notes.
114 """
115 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()
118@export
119class ExceptionBase(Exception):
120 """Base exception derived from :exc:`Exception <python:Exception>` for all custom exceptions."""
122 def __init__(self, message: str = "") -> None:
123 """
124 ExceptionBase initializer.
126 :param message: Optional, the exception message.
127 """
128 super().__init__()
129 self.message = message
131 @readonly
132 def HasNotes(self) -> bool:
133 """
134 Read-only property to return if the warning has attached notes.
136 :returns: True, if the warning has attached notes.
137 """
138 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0
140 @readonly
141 def Notes(self) -> tuple[str, ...]:
142 """
143 Read-only property to return warning's attached notes.
145 :returns: Attached notes.
146 """
147 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()
149 def __str__(self) -> str:
150 """
151 Returns the exception's message text.
153 :returns: The exception's message text.
154 """
155 return self.message
157 # @DocumentMemberAttribute(False)
158 # @MethodAlias(pyExceptions.with_traceback)
159 # def with_traceback(self): pass
162@export
163class EnvironmentVariableError(ExceptionBase):
164 """The exception is raised when an expected environment variable is missing."""
167@export
168class ConfigurationError(ExceptionBase):
169 """
170 The exception is raised when a configuration is invalid.
172 It is the base-exception for configuration problems in pyTooling **and in packages building on it**, so an
173 application reading configuration files from several sources can catch them all with one clause instead of one
174 per package. :mod:`pyTooling.Configuration` derives its own exceptions from it.
176 Attach the details as notes rather than folding them into the message - the value that was rejected, the file it
177 came from, the values that would have been accepted:
179 .. code-block:: Python
181 ex = ConfigurationError(f"Unknown log level '{value}'.")
182 ex.add_note(f"Configuration file: {path}")
183 ex.add_note(f"Allowed values: {', '.join(levels)}")
184 raise ex
185 """
188@export
189class PlatformNotSupportedError(ExceptionBase):
190 """The exception is raise if the platform is not supported."""
193@export
194class NotConfiguredError(ExceptionBase):
195 """The exception is raise if the requested setting is not configured."""
198@export
199class MissingDependencyError(ImportError):
200 """
201 The exception is raised when an optional dependency of pyTooling is not installed.
203 Some modules need a package pyTooling doesn't install by default. Importing such a module without its dependency
204 raises this exception instead of the bare :exc:`ImportError`, so the message names the extra that installs it.
206 The exception derives from :exc:`ImportError`, because that is what a caller guarding an optional import expects to
207 catch, and it carries the missing package and the extra as :attr:`Dependency` and :attr:`Extra`.
209 .. admonition:: ``example.py``
211 .. code-block:: python
213 try:
214 from ruamel.yaml import YAML
215 except ImportError as ex:
216 raise MissingDependencyError(dependency="ruamel.yaml", extra="yaml") from ex
217 """
219 EXIT_CODE: ClassVar[int] = 242 #: Exit code an application should use when an optional dependency is missing.
221 _dependency: str #: Field storing the name of the package that is not installed.
222 _extra: Nullable[str] #: Field storing the name of the pyTooling extra installing that package.
224 def __init__(self, message: Nullable[str] = None, /, *, dependency: str, extra: Nullable[str] = None) -> None:
225 """
226 Initialize a new missing-dependency error and attach the installation hint as a note.
228 :param message: Optional, the exception message. |br|
229 When omitted, it is derived from ``dependency`` - every raising site says the same thing, so
230 there is nothing for the caller to add.
231 :param dependency: Name of the package that is not installed.
232 :param extra: Optional, name of the pyTooling extra installing that package. |br|
233 When given, the note offers ``pyTooling[<extra>]`` next to the package itself.
234 """
235 super().__init__(message if message is not None else f"Optional dependency '{dependency}' not installed.")
237 self._dependency = dependency
238 self._extra = extra
240 if extra is None:
241 self.add_note(f"Install '{dependency}'.")
242 else:
243 self.add_note(f"Either install pyTooling with extra 'pyTooling[{extra}]' or install '{dependency}' directly.")
245 @readonly
246 def Dependency(self) -> str:
247 """
248 Read-only property to access the name of the package that is not installed (:attr:`_dependency`).
250 :returns: Name of the missing package.
251 """
252 return self._dependency
254 @readonly
255 def Extra(self) -> Nullable[str]:
256 """
257 Read-only property to access the name of the pyTooling extra installing the package (:attr:`_extra`).
259 :returns: Name of the extra, or ``None`` if the package has no extra of its own.
260 """
261 return self._extra
263 @readonly
264 def InstallCommands(self) -> tuple[str, ...]:
265 """
266 Read-only property to return the command lines installing the missing package.
268 The extra comes first, because it installs the package *and* records why it is needed. Both commands are
269 plain text and need no terminal support: an application that cannot even import :mod:`pyTooling.TerminalUI` -
270 because *colorama* is the missing package - can print them itself, and
271 :meth:`~pyTooling.TerminalUI.TerminalBaseApplication.PrintMissingDependencyException` formats them when it can.
273 :returns: One command line per installation option, most specific first.
274 """
275 if self._extra is None:
276 return (f"pip install {self._dependency}", )
278 return (
279 f"pip install pyTooling[{self._extra}]",
280 f"pip install {self._dependency}"
281 )
284# FIXME: Why not derived from ExceptionBase?
285@export
286class ToolingException(Exception):
287 """The exception is raised by pyTooling internal features."""
289 @readonly
290 def HasNotes(self) -> bool:
291 """
292 Read-only property to return if the warning has attached notes.
294 :returns: True, if the warning has attached notes.
295 """
296 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0
298 @readonly
299 def Notes(self) -> tuple[str, ...]:
300 """
301 Read-only property to return warning's attached notes.
303 :returns: Attached notes.
304 """
305 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()
308# ==================================================================================================================== #
309# Deprecated names, kept for backwards compatibility. Removed in v10.0.0.
310# ==================================================================================================================== #
311EnvironmentException = EnvironmentVariableError
312MissingDependencyException = MissingDependencyError
313NotConfiguredException = NotConfiguredError
314PlatformNotSupportedException = PlatformNotSupportedError