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

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 <EXECPTION>` 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: 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 = "" 

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__() 

129 self.message = message 

130 

131 @readonly 

132 def HasNotes(self) -> bool: 

133 """ 

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

135 

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 

139 

140 @readonly 

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

142 """ 

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

144 

145 :returns: Attached notes. 

146 """ 

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

148 

149 def __str__(self) -> str: 

150 """ 

151 Returns the exception's message text. 

152 

153 :returns: The exception's message text. 

154 """ 

155 return self.message 

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 MissingDependencyError(ImportError): 

200 """ 

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

202 

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. 

205 

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

208 

209 .. admonition:: ``example.py`` 

210 

211 .. code-block:: python 

212 

213 try: 

214 from ruamel.yaml import YAML 

215 except ImportError as ex: 

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

217 """ 

218 

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

220 

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. 

223 

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. 

227 

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

236 

237 self._dependency = dependency 

238 self._extra = extra 

239 

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

244 

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

249 

250 :returns: Name of the missing package. 

251 """ 

252 return self._dependency 

253 

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

258 

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

260 """ 

261 return self._extra 

262 

263 @readonly 

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

265 """ 

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

267 

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. 

272 

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

277 

278 return ( 

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

280 f"pip install {self._dependency}" 

281 ) 

282 

283 

284# FIXME: Why not derived from ExceptionBase? 

285@export 

286class ToolingException(Exception): 

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

288 

289 @readonly 

290 def HasNotes(self) -> bool: 

291 """ 

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

293 

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 

297 

298 @readonly 

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

300 """ 

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

302 

303 :returns: Attached notes. 

304 """ 

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

306 

307 

308# ==================================================================================================================== # 

309# Deprecated names, kept for backwards compatibility. Removed in v10.0.0. 

310# ==================================================================================================================== # 

311EnvironmentException = EnvironmentVariableError 

312MissingDependencyException = MissingDependencyError 

313NotConfiguredException = NotConfiguredError 

314PlatformNotSupportedException = PlatformNotSupportedError