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

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 EnvironmentException(ExceptionBase): 

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

165 

166 

167# ConfigurationException 

168# 

169 

170@export 

171class PlatformNotSupportedException(ExceptionBase): 

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

173 

174 

175@export 

176class NotConfiguredException(ExceptionBase): 

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

178 

179 

180@export 

181class MissingDependencyException(ImportError): 

182 """ 

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

184 

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. 

187 

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

190 

191 .. admonition:: ``example.py`` 

192 

193 .. code-block:: python 

194 

195 try: 

196 from ruamel.yaml import YAML 

197 except ImportError as ex: 

198 raise MissingDependencyException(dependency="ruamel.yaml", extra="yaml") from ex 

199 """ 

200 

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

202 

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. 

205 

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. 

209 

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

218 

219 self._dependency = dependency 

220 self._extra = extra 

221 

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

226 

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

231 

232 :returns: Name of the missing package. 

233 """ 

234 return self._dependency 

235 

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

240 

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

242 """ 

243 return self._extra 

244 

245 @readonly 

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

247 """ 

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

249 

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. 

254 

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

259 

260 return ( 

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

262 f"pip install {self._dependency}" 

263 ) 

264 

265 

266# FIXME: Why not derived from ExceptionBase? 

267@export 

268class ToolingException(Exception): 

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

270 

271 @readonly 

272 def HasNotes(self) -> bool: 

273 """ 

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

275 

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 

279 

280 @readonly 

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

282 """ 

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

284 

285 :returns: Attached notes. 

286 """ 

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