Coverage for pyTooling/Exceptions/__init__.py: 74%
70 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +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 EnvironmentException(ExceptionBase):
164 """The exception is raised when an expected environment variable is missing."""
167# ConfigurationException
168#
170@export
171class PlatformNotSupportedException(ExceptionBase):
172 """The exception is raise if the platform is not supported."""
175@export
176class NotConfiguredException(ExceptionBase):
177 """The exception is raise if the requested setting is not configured."""
180@export
181class MissingDependencyException(ImportError):
182 """
183 The exception is raised when an optional dependency of pyTooling is not installed.
185 Some modules need a package pyTooling doesn't install by default. Importing such a module without its dependency
186 raises this exception instead of the bare :exc:`ImportError`, so the message names the extra that installs it.
188 The exception derives from :exc:`ImportError`, because that is what a caller guarding an optional import expects to
189 catch, and it carries the missing package and the extra as :attr:`Dependency` and :attr:`Extra`.
191 .. admonition:: ``example.py``
193 .. code-block:: python
195 try:
196 from ruamel.yaml import YAML
197 except ImportError as ex:
198 raise MissingDependencyException(dependency="ruamel.yaml", extra="yaml") from ex
199 """
201 EXIT_CODE: ClassVar[int] = 242 #: Exit code an application should use when an optional dependency is missing.
203 _dependency: str #: Field storing the name of the package that is not installed.
204 _extra: Nullable[str] #: Field storing the name of the pyTooling extra installing that package.
206 def __init__(self, message: Nullable[str] = None, /, *, dependency: str, extra: Nullable[str] = None) -> None:
207 """
208 Initialize a new missing-dependency error and attach the installation hint as a note.
210 :param message: Optional, the exception message. |br|
211 When omitted, it is derived from ``dependency`` - every raising site says the same thing, so
212 there is nothing for the caller to add.
213 :param dependency: Name of the package that is not installed.
214 :param extra: Optional, name of the pyTooling extra installing that package. |br|
215 When given, the note offers ``pyTooling[<extra>]`` next to the package itself.
216 """
217 super().__init__(message if message is not None else f"Optional dependency '{dependency}' not installed.")
219 self._dependency = dependency
220 self._extra = extra
222 if extra is None:
223 self.add_note(f"Install '{dependency}'.")
224 else:
225 self.add_note(f"Either install pyTooling with extra 'pyTooling[{extra}]' or install '{dependency}' directly.")
227 @readonly
228 def Dependency(self) -> str:
229 """
230 Read-only property to access the name of the package that is not installed (:attr:`_dependency`).
232 :returns: Name of the missing package.
233 """
234 return self._dependency
236 @readonly
237 def Extra(self) -> Nullable[str]:
238 """
239 Read-only property to access the name of the pyTooling extra installing the package (:attr:`_extra`).
241 :returns: Name of the extra, or ``None`` if the package has no extra of its own.
242 """
243 return self._extra
245 @readonly
246 def InstallCommands(self) -> tuple[str, ...]:
247 """
248 Read-only property to return the command lines installing the missing package.
250 The extra comes first, because it installs the package *and* records why it is needed. Both commands are
251 plain text and need no terminal support: an application that cannot even import :mod:`pyTooling.TerminalUI` -
252 because *colorama* is the missing package - can print them itself, and
253 :meth:`~pyTooling.TerminalUI.TerminalBaseApplication.PrintMissingDependencyException` formats them when it can.
255 :returns: One command line per installation option, most specific first.
256 """
257 if self._extra is None:
258 return (f"pip install {self._dependency}", )
260 return (
261 f"pip install pyTooling[{self._extra}]",
262 f"pip install {self._dependency}"
263 )
266# FIXME: Why not derived from ExceptionBase?
267@export
268class ToolingException(Exception):
269 """The exception is raised by pyTooling internal features."""
271 @readonly
272 def HasNotes(self) -> bool:
273 """
274 Read-only property to return if the warning has attached notes.
276 :returns: True, if the warning has attached notes.
277 """
278 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0
280 @readonly
281 def Notes(self) -> tuple[str, ...]:
282 """
283 Read-only property to return warning's attached notes.
285 :returns: Attached notes.
286 """
287 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()