Coverage for pyTooling/Exceptions/__init__.py: 78%
82 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +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 <EXCEPTION>` 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: 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__(message)
130 @readonly
131 def Message(self) -> str:
132 """
133 Read-only property to return the exception's message (first element of :attr:`~BaseException.args`).
135 :returns: The exception's message.
136 """
137 return self.args[0]
139 @readonly
140 def HasNotes(self) -> bool:
141 """
142 Read-only property to return if the warning has attached notes.
144 :returns: True, if the warning has attached notes.
145 """
146 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0
148 @readonly
149 def Notes(self) -> tuple[str, ...]:
150 """
151 Read-only property to return warning's attached notes.
153 :returns: Attached notes.
154 """
155 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()
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 ToolingException(Exception):
200 """The exception is raised by pyTooling internal features."""
202 @readonly
203 def HasNotes(self) -> bool:
204 """
205 Read-only property to return if the warning has attached notes.
207 :returns: True, if the warning has attached notes.
208 """
209 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0
211 @readonly
212 def Notes(self) -> tuple[str, ...]:
213 """
214 Read-only property to return warning's attached notes.
216 :returns: Attached notes.
217 """
218 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple()
221@export
222class MissingDependencyError(ToolingException, ImportError):
223 """
224 The exception is raised when an optional dependency of pyTooling is not installed.
226 Some modules need a package pyTooling doesn't install by default. Importing such a module without its dependency
227 raises this exception instead of the bare :exc:`ImportError`, so the message names the extra that installs it.
229 The exception is a :exc:`ToolingException` and an :exc:`ImportError`: a caller guarding an optional import catches
230 :exc:`ImportError`, a caller handling pyTooling's own errors catches :exc:`ToolingException`. It carries the missing
231 package and the extra as :attr:`Dependency` and :attr:`Extra`; :attr:`ImportError.name` is the missing package too.
233 .. admonition:: ``example.py``
235 .. code-block:: python
237 try:
238 from ruamel.yaml import YAML
239 except ImportError as ex:
240 raise MissingDependencyError(dependency="ruamel.yaml", extra="yaml") from ex
241 """
243 EXIT_CODE: ClassVar[int] = 242 #: Exit code an application should use when an optional dependency is missing.
245 _dependency: str #: Field storing the name of the package that is not installed.
246 _extra: Nullable[str] #: Field storing the name of the pyTooling extra installing that package.
248 def __init__(self, message: Nullable[str] = None, /, *, dependency: str, extra: Nullable[str] = None) -> None:
249 """
250 Initialize a new missing-dependency error and attach the installation hint as a note.
252 :param message: Optional, the exception message. |br|
253 When omitted, it is derived from ``dependency`` - every raising site says the same thing, so
254 there is nothing for the caller to add.
255 :param dependency: Name of the package that is not installed.
256 :param extra: Optional, name of the pyTooling extra installing that package. |br|
257 When given, the note offers ``pyTooling[<extra>]`` next to the package itself.
258 """
259 super().__init__(
260 message if message is not None else f"Optional dependency '{dependency}' not installed.",
261 name=dependency
262 )
264 self._dependency = dependency
265 self._extra = extra
267 if extra is None:
268 self.add_note(f"Install '{dependency}'.")
269 else:
270 self.add_note(f"Either install pyTooling with extra 'pyTooling[{extra}]' or install '{dependency}' directly.")
272 @readonly
273 def Dependency(self) -> str:
274 """
275 Read-only property to access the name of the package that is not installed (:attr:`_dependency`).
277 :returns: Name of the missing package.
278 """
279 return self._dependency
281 @readonly
282 def Extra(self) -> Nullable[str]:
283 """
284 Read-only property to access the name of the pyTooling extra installing the package (:attr:`_extra`).
286 :returns: Name of the extra, or ``None`` if the package has no extra of its own.
287 """
288 return self._extra
290 @readonly
291 def InstallCommands(self) -> tuple[str, ...]:
292 """
293 Read-only property to return the command lines installing the missing package.
295 The extra comes first, because it installs the package *and* records why it is needed. Both commands are
296 plain text and need no terminal support: an application that cannot even import :mod:`pyTooling.TerminalUI` -
297 because *colorama* is the missing package - can print them itself, and
298 :meth:`~pyTooling.TerminalUI.TerminalBaseApplication.PrintMissingDependencyError` formats them when it can.
300 :returns: One command line per installation option, most specific first.
301 """
302 if self._extra is None:
303 return (f"pip install {self._dependency}", )
305 return (
306 f"pip install pyTooling[{self._extra}]",
307 f"pip install {self._dependency}"
308 )
311# FIXME: Why not derived from ExceptionBase?
312@export
313class DuplicateElementError(Exception):
314 """
315 The exception is raised when an element is added to a data structure, which already contains an element of the same
316 kind and key.
318 It's a common base: a package's own exception derives from it and from the package's base-exception, e.g.
319 ``class DuplicateVertexError(GraphError, DuplicateElementError)``. It has no initializer of its own, so the
320 package's base-exception decides how the message is stored.
321 """
324@export
325class AlreadyInHierarchyError(Exception):
326 """
327 The exception is raised when an element is added to a hierarchy, but it's already part of one.
329 It's a common base: a package's own exception derives from it and from the package's base-exception, e.g.
330 ``class AlreadyInTreeError(TreeError, AlreadyInHierarchyError)``. It has no initializer of its own, so the
331 package's base-exception decides how the message is stored.
332 """
335@export
336class NotInSameHierarchyError(Exception):
337 """
338 The exception is raised when an element is used together with an element of another data structure.
340 The element itself is valid, only its ownership is wrong. It's a common base: a package's own exception derives from
341 it and from the package's base-exception, e.g. ``class NotInSameTreeError(TreeError, NotInSameHierarchyError)``. It
342 has no initializer of its own, so the package's base-exception decides how the message is stored.
343 """
346@export
347class NotAChildError(Exception):
348 """
349 The exception is raised when an element is not a direct child of the element it's removed from or looked up in.
351 It's about the direct parent/child relation, not about the whole hierarchy: a grandchild is in the same hierarchy,
352 but it's a child of another element. It's a common base: a package's own exception derives from it and from the
353 package's base-exception, e.g. ``class NotAChildError(TreeError, NotAChildError)`` in :mod:`pyTooling.Tree`. It has
354 no initializer of its own, so the package's base-exception decides how the message is stored.
355 """
358@export
359class InUseError(Exception):
360 """
361 The exception is raised when an element is removed from a data structure, but other elements still use it.
363 It's a common base: a package's own exception derives from it and from the package's base-exception, e.g.
364 ``class LibraryInUseError(VHDLModelException, InUseError)`` in pyVHDLModel. It has no initializer of its own, so the
365 package's base-exception decides how the message is stored.
366 """