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

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. 

34 

35.. hint:: 

36 

37 See :ref:`high-level help <EXCEPTION>` for explanations and usage examples. 

38 

39.. seealso:: 

40 

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 

48 

49 

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. 

63 

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 = "" 

73 

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) 

80 

81 note = f"{indent}{item}" 

82 sep = separator 

83 

84 ex.add_note(note) 

85 

86 

87@export 

88class OverloadResolutionError(Exception): 

89 """ 

90 The exception is raised, when no matching overloaded method was found. 

91 

92 .. attention:: 

93 

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 """ 

98 

99 @readonly 

100 def HasNotes(self) -> bool: 

101 """ 

102 Read-only property to return if the warning has attached notes. 

103 

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 

107 

108 @readonly 

109 def Notes(self) -> tuple[str, ...]: 

110 """ 

111 Read-only property to return warning's attached notes. 

112 

113 :returns: Attached notes. 

114 """ 

115 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple() 

116 

117 

118@export 

119class ExceptionBase(Exception): 

120 """Base exception derived from :exc:`Exception <python:Exception>` for all custom exceptions.""" 

121 

122 def __init__(self, message: str = "") -> None: 

123 """ 

124 ExceptionBase initializer. 

125 

126 :param message: Optional, the exception message. 

127 """ 

128 super().__init__(message) 

129 

130 @readonly 

131 def Message(self) -> str: 

132 """ 

133 Read-only property to return the exception's message (first element of :attr:`~BaseException.args`). 

134 

135 :returns: The exception's message. 

136 """ 

137 return self.args[0] 

138 

139 @readonly 

140 def HasNotes(self) -> bool: 

141 """ 

142 Read-only property to return if the warning has attached notes. 

143 

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 

147 

148 @readonly 

149 def Notes(self) -> tuple[str, ...]: 

150 """ 

151 Read-only property to return warning's attached notes. 

152 

153 :returns: Attached notes. 

154 """ 

155 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple() 

156 

157 # @DocumentMemberAttribute(False) 

158 # @MethodAlias(pyExceptions.with_traceback) 

159 # def with_traceback(self): pass 

160 

161 

162@export 

163class EnvironmentVariableError(ExceptionBase): 

164 """The exception is raised when an expected environment variable is missing.""" 

165 

166 

167@export 

168class ConfigurationError(ExceptionBase): 

169 """ 

170 The exception is raised when a configuration is invalid. 

171 

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. 

175 

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: 

178 

179 .. code-block:: Python 

180 

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 """ 

186 

187 

188@export 

189class PlatformNotSupportedError(ExceptionBase): 

190 """The exception is raise if the platform is not supported.""" 

191 

192 

193@export 

194class NotConfiguredError(ExceptionBase): 

195 """The exception is raise if the requested setting is not configured.""" 

196 

197 

198@export 

199class ToolingException(Exception): 

200 """The exception is raised by pyTooling internal features.""" 

201 

202 @readonly 

203 def HasNotes(self) -> bool: 

204 """ 

205 Read-only property to return if the warning has attached notes. 

206 

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 

210 

211 @readonly 

212 def Notes(self) -> tuple[str, ...]: 

213 """ 

214 Read-only property to return warning's attached notes. 

215 

216 :returns: Attached notes. 

217 """ 

218 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple() 

219 

220 

221@export 

222class MissingDependencyError(ToolingException, ImportError): 

223 """ 

224 The exception is raised when an optional dependency of pyTooling is not installed. 

225 

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. 

228 

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. 

232 

233 .. admonition:: ``example.py`` 

234 

235 .. code-block:: python 

236 

237 try: 

238 from ruamel.yaml import YAML 

239 except ImportError as ex: 

240 raise MissingDependencyError(dependency="ruamel.yaml", extra="yaml") from ex 

241 """ 

242 

243 EXIT_CODE: ClassVar[int] = 242 #: Exit code an application should use when an optional dependency is missing. 

244 

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. 

247 

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. 

251 

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 ) 

263 

264 self._dependency = dependency 

265 self._extra = extra 

266 

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.") 

271 

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`). 

276 

277 :returns: Name of the missing package. 

278 """ 

279 return self._dependency 

280 

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`). 

285 

286 :returns: Name of the extra, or ``None`` if the package has no extra of its own. 

287 """ 

288 return self._extra 

289 

290 @readonly 

291 def InstallCommands(self) -> tuple[str, ...]: 

292 """ 

293 Read-only property to return the command lines installing the missing package. 

294 

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. 

299 

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}", ) 

304 

305 return ( 

306 f"pip install pyTooling[{self._extra}]", 

307 f"pip install {self._dependency}" 

308 ) 

309 

310 

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. 

317 

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 """ 

322 

323 

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. 

328 

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 """ 

333 

334 

335@export 

336class NotInSameHierarchyError(Exception): 

337 """ 

338 The exception is raised when an element is used together with an element of another data structure. 

339 

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 """ 

344 

345 

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. 

350 

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 """ 

356 

357 

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. 

362 

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 """