Coverage for pyTooling/Configuration/__init__.py: 96%

67 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# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32Abstract configuration reader. 

33 

34.. hint:: 

35 

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

37 

38.. seealso:: 

39 

40 :mod:`pyTooling.Configuration.JSON` 

41 |rarr| A configuration read from a JSON file. 

42 :mod:`pyTooling.Configuration.YAML` 

43 |rarr| A configuration read from a YAML file. 

44 :mod:`pyTooling.GenericPath` 

45 |rarr| The path expressions a configuration is queried with. 

46""" 

47from __future__ import annotations 

48 

49from pathlib import Path 

50from typing import Union, ClassVar, Iterator, Optional as Nullable 

51 

52from pyTooling.Decorators import export, readonly 

53from pyTooling.MetaClasses import ExtendedType, abstractmethod, mixin 

54from pyTooling.Exceptions import ToolingException 

55 

56 

57__all__ = ["KeyT", "NodeT", "ValueT"] 

58 

59 

60KeyT = Union[str, int] #: Type variable for keys. 

61NodeT = Union["Dictionary", "Sequence"] #: Type variable for nodes. 

62ValueT = Union[NodeT, str, int, float] #: Type variable for values. 

63 

64 

65@export 

66class ConfigurationException(ToolingException): 

67 """Base-exception of all exceptions raised by :mod:`pyTooling.Configuration`.""" 

68 

69 

70@export 

71class KeyNotFoundException(ConfigurationException): 

72 """ 

73 The requested key or index doesn't exist in the configuration node. 

74 

75 The key was neither found as a string, nor converted to an integer or float. A note lists the keys or the index 

76 range offered by the node. 

77 """ 

78 

79 

80@export 

81class UnsupportedValueTypeException(ConfigurationException): 

82 """ 

83 The configuration file parser returned a value of a type that isn't supported by :mod:`pyTooling.Configuration`. 

84 

85 Supported are scalars (:class:`str`, :class:`int`, :class:`float`) and the parser's dictionary and sequence types. 

86 """ 

87 

88 

89@export 

90class InterpolationException(ConfigurationException): 

91 """A variable reference (``${...}``) in a configuration value is malformed or can't be resolved.""" 

92 

93 

94@export 

95class PathExpressionException(ConfigurationException): 

96 """A path expression (``a:b:c``) doesn't describe a valid node or value in the configuration.""" 

97 

98 

99@export 

100class Node(metaclass=ExtendedType, slots=True): 

101 """Abstract node in a configuration data structure.""" 

102 

103 DICT_TYPE: ClassVar[type[Dictionary]] #: Type reference used when instantiating new dictionaries 

104 SEQ_TYPE: ClassVar[type[Sequence]] #: Type reference used when instantiating new sequences 

105 _root: Configuration #: Reference to the root node. 

106 _parent: Dictionary #: Reference to a parent node. 

107 

108 def __init__(self, root: Configuration = None, parent: Nullable[NodeT] = None) -> None: 

109 """ 

110 Initializes a node. 

111 

112 :param root: Optional, reference to the root node. 

113 :param parent: Optional, reference to the parent node. 

114 """ 

115 self._root = root 

116 self._parent = parent 

117 

118 @abstractmethod 

119 def __len__(self) -> int: # type: ignore[empty-body] 

120 """ 

121 Returns the number of sub-elements. 

122 

123 :returns: Number of sub-elements. 

124 """ 

125 

126 @abstractmethod 

127 def __getitem__(self, key: KeyT) -> ValueT: # type: ignore[empty-body] 

128 """ 

129 Access an element in the node by index or key. 

130 

131 :param key: Index or key of the element. 

132 :returns: A node (sequence or dictionary) or scalar value (int, float, str). 

133 """ 

134 

135 def __setitem__(self, key: KeyT, value: ValueT) -> None: 

136 """ 

137 Set an element in the node by index or key. 

138 

139 .. attention:: 

140 

141 A configuration is **read-only**: the file format doesn't implements writing. 

142 

143 :param key: Index or key of the element. 

144 :param value: The new value of that element. 

145 :raises NotImplementedError: Always - a configuration is read-only. 

146 """ 

147 raise NotImplementedError("Currently, the configuration is read-only. Writing isn't implemented.") 

148 

149 @abstractmethod 

150 def __iter__(self) -> Iterator[ValueT]: # type: ignore[empty-body] 

151 """ 

152 Returns an iterator to iterate a node. 

153 

154 :returns: Node iterator. 

155 """ 

156 

157 @property 

158 def Key(self) -> KeyT: 

159 """ 

160 Property to access the node's key. 

161 

162 :returns: Key of the node. 

163 """ 

164 raise NotImplementedError() 

165 

166 @Key.setter 

167 def Key(self, value: KeyT) -> None: 

168 raise NotImplementedError() 

169 

170 @abstractmethod 

171 def QueryPath(self, query: str) -> ValueT: # type: ignore[empty-body] 

172 """ 

173 Return a node or value based on a path description to that node or value. 

174 

175 :param query: String describing the path to the node or value. 

176 :returns: A node (sequence or dictionary) or scalar value (int, float, str). 

177 """ 

178 

179 

180@export 

181@mixin 

182class Dictionary(Node): 

183 """Abstract dictionary node in a configuration.""" 

184 

185 def __init__(self, root: Configuration = None, parent: Nullable[NodeT] = None) -> None: 

186 """ 

187 Initializes a dictionary. 

188 

189 :param root: Optional, reference to the root node. 

190 :param parent: Optional, reference to the parent node. 

191 """ 

192 Node.__init__(self, root, parent) 

193 

194 def __contains__(self, key: KeyT) -> bool: # type: ignore[empty-body] 

195 """ 

196 Check if a key exists in this dictionary node. 

197 

198 :param key: The key to check for. 

199 :returns: ``True``, if the key exists in this node. 

200 """ 

201 raise NotImplementedError() 

202 

203 

204@export 

205@mixin 

206class Sequence(Node): 

207 """Abstract sequence node in a configuration.""" 

208 

209 def __init__(self, root: Configuration = None, parent: Nullable[NodeT] = None) -> None: 

210 """ 

211 Initializes a sequence. 

212 

213 :param root: Optional, reference to the root node. 

214 :param parent: Optional, reference to the parent node. 

215 """ 

216 Node.__init__(self, root, parent) 

217 

218 def __getitem__(self, index: int) -> ValueT: # type: ignore[empty-body] 

219 """ 

220 Read an element of this sequence node by index. 

221 

222 :param index: Index of the element to read. 

223 :returns: A node (sequence or dictionary) or scalar value (int, float, str). 

224 """ 

225 raise NotImplementedError() 

226 

227 def __setitem__(self, index: int, value: ValueT) -> None: 

228 """ 

229 Write an element of this sequence node by index. 

230 

231 .. attention:: 

232 

233 A configuration is **read-only** - see :meth:`Node.__setitem__`. 

234 

235 :param index: Index of the element to write. 

236 :param value: The new value of that element. 

237 :raises NotImplementedError: Always - a configuration is read-only. 

238 """ 

239 raise NotImplementedError("Currently, the configuration is read-only. Writing isn't implemented.") 

240 

241 

242setattr(Node, "DICT_TYPE", Dictionary) 

243setattr(Node, "SEQ_TYPE", Sequence) 

244 

245 

246@export 

247@mixin 

248class Configuration(Node): 

249 """Abstract root node in a configuration.""" 

250 

251 _configFile: Path #: Path to the configuration file. 

252 

253 def __init__(self, configFile: Path, root: Configuration = None, parent: Nullable[NodeT] = None) -> None: 

254 """ 

255 Initializes a configuration. 

256 

257 :param configFile: Configuration file. 

258 :param root: Optional, reference to the root node. 

259 :param parent: Optional, reference to the parent node. 

260 """ 

261 Node.__init__(self, root, parent) 

262 self._configFile = configFile 

263 

264 @readonly 

265 def ConfigFile(self) -> Path: 

266 """ 

267 Read-only property to access the configuration file's path. 

268 

269 :returns: Path to the configuration file. 

270 """ 

271 return self._configFile