Coverage for pyTooling / Configuration / YAML.py: 90%

165 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-28 12:48 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ __ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ __ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| # 

7# |_| |___/ |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2021-2025 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""" 

32Configuration reader for YAML files. 

33 

34.. hint:: 

35 

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

37""" 

38from pathlib import Path 

39from typing import Dict, List, Union, Iterator as typing_Iterator, Self 

40 

41try: 

42 from ruamel.yaml import YAML, CommentedMap, CommentedSeq 

43except ImportError as ex: # pragma: no cover 

44 raise Exception("Optional dependency 'ruamel.yaml' not installed. Either install pyTooling with extra dependencies 'pyTooling[yaml]' or install 'ruamel.yaml' directly.") from ex 

45 

46try: 

47 from pyTooling.Decorators import export 

48 from pyTooling.MetaClasses import ExtendedType 

49 from pyTooling.Configuration import ConfigurationException, KeyT, NodeT, ValueT 

50 from pyTooling.Configuration import Node as Abstract_Node 

51 from pyTooling.Configuration import Dictionary as Abstract_Dict 

52 from pyTooling.Configuration import Sequence as Abstract_Seq 

53 from pyTooling.Configuration import Configuration as Abstract_Configuration 

54except (ImportError, ModuleNotFoundError): # pragma: no cover 

55 print("[pyTooling.Configuration.YAML] Could not import from 'pyTooling.*'!") 

56 

57 try: 

58 from Decorators import export 

59 from MetaClasses import ExtendedType 

60 from pyTooling.Configuration import ConfigurationException, KeyT, NodeT, ValueT 

61 from pyTooling.Configuration import Node as Abstract_Node 

62 from pyTooling.Configuration import Dictionary as Abstract_Dict 

63 from pyTooling.Configuration import Sequence as Abstract_Seq 

64 from pyTooling.Configuration import Configuration as Abstract_Configuration 

65 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

66 print("[pyTooling.Configuration.YAML] Could not import directly!") 

67 raise ex 

68 

69 

70@export 

71class Node(Abstract_Node): 

72 """ 

73 Node in a YAML configuration data structure. 

74 """ 

75 

76 _yamlNode: Union[CommentedMap, CommentedSeq] #: Reference to the associated YAML node. 

77 _cache: Dict[str, ValueT] 

78 _key: KeyT #: Key of this node. 

79 _length: int #: Number of sub-elements. 

80 

81 def __init__( 

82 self, 

83 root: "Configuration", 

84 parent: NodeT, 

85 key: KeyT, 

86 yamlNode: Union[CommentedMap, CommentedSeq] 

87 ) -> None: 

88 """ 

89 Initializes a YAML node. 

90 

91 :param root: Reference to the root node. 

92 :param parent: Reference to the parent node. 

93 :param key: 

94 :param yamlNode: Reference to the YAML node. 

95 """ 

96 Abstract_Node.__init__(self, root, parent) 

97 

98 self._yamlNode = yamlNode 

99 self._cache = {} 

100 self._key = key 

101 self._length = len(yamlNode) 

102 

103 def __len__(self) -> int: 

104 """ 

105 Returns the number of sub-elements. 

106 

107 :returns: Number of sub-elements. 

108 """ 

109 return self._length 

110 

111 def __getitem__(self, key: KeyT) -> ValueT: 

112 """ 

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

114 

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

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

117 """ 

118 return self._GetNodeOrValue(str(key)) 

119 

120 @property 

121 def Key(self) -> KeyT: 

122 """ 

123 Property to access the node's key. 

124 

125 :returns: Key of the node. 

126 """ 

127 return self._key 

128 

129 @Key.setter 

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

131 raise NotImplementedError() 

132 

133 def QueryPath(self, query: str) -> ValueT: 

134 """ 

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

136 

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

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

139 """ 

140 path = self._ToPath(query) 

141 return self._GetNodeOrValueByPathExpression(path) 

142 

143 @staticmethod 

144 def _ToPath(query: str) -> List[Union[str, int]]: 

145 return query.split(":") 

146 

147 def _GetNodeOrValue(self, key: str) -> ValueT: 

148 try: 

149 value = self._cache[key] 

150 except KeyError: 

151 try: 

152 value = self._yamlNode[key] 

153 except (KeyError, TypeError): 

154 try: 

155 value = self._yamlNode[int(key)] 

156 except KeyError: 

157 try: 

158 value = self._yamlNode[float(key)] 

159 except KeyError as ex: 

160 raise Exception(f"") from ex # XXX: needs error message 

161 

162 if isinstance(value, str): 

163 value = self._ResolveVariables(value) 

164 elif isinstance(value, (int, float)): 

165 value = str(value) 

166 elif isinstance(value, CommentedMap): 

167 value = self.DICT_TYPE(self, self, key, value) 

168 elif isinstance(value, CommentedSeq): 168 ↛ 171line 168 didn't jump to line 171 because the condition on line 168 was always true

169 value = self.SEQ_TYPE(self, self, key, value) 

170 else: 

171 raise Exception(f"") from TypeError(f"Unknown type '{value.__class__.__name__}' returned from ruamel.yaml.") # XXX: error message 

172 

173 self._cache[key] = value 

174 

175 return value 

176 

177 def _ResolveVariables(self, value: str) -> str: 

178 if value == "": 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true

179 return "" 

180 elif "$" not in value: 

181 return value 

182 

183 rawValue = value 

184 result = "" 

185 

186 while (len(rawValue) > 0): 

187# print(f"_ResolveVariables: LOOP rawValue='{rawValue}'") 

188 beginPos = rawValue.find("$") 

189 if beginPos < 0: 

190 result += rawValue 

191 rawValue = "" 

192 else: 

193 result += rawValue[:beginPos] 

194 if rawValue[beginPos + 1] == "$": 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true

195 result += "$" 

196 rawValue = rawValue[1:] 

197 elif rawValue[beginPos + 1] == "{": 197 ↛ 186line 197 didn't jump to line 186 because the condition on line 197 was always true

198 endPos = rawValue.find("}", beginPos) 

199 nextPos = rawValue.rfind("$", beginPos, endPos) 

200 if endPos < 0: 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true

201 raise Exception(f"") # XXX: InterpolationSyntaxError(option, section, f"Bad interpolation variable reference {rest!r}") 

202 if (nextPos > 0) and (nextPos < endPos): # an embedded $-sign 

203 path = rawValue[nextPos+2:endPos] 

204# print(f"_ResolveVariables: path='{path}'") 

205 innervalue = self._GetValueByPathExpression(self._ToPath(path)) 

206# print(f"_ResolveVariables: innervalue='{innervalue}'") 

207 rawValue = rawValue[beginPos:nextPos] + str(innervalue) + rawValue[endPos + 1:] 

208# print(f"_ResolveVariables: new rawValue='{rawValue}'") 

209 else: 

210 path = rawValue[beginPos+2:endPos] 

211 rawValue = rawValue[endPos+1:] 

212 result += str(self._GetValueByPathExpression(self._ToPath(path))) 

213 

214 return result 

215 

216 def _GetValueByPathExpression(self, path: List[KeyT]) -> ValueT: 

217 node = self 

218 for p in path: 

219 if p == "..": 

220 node = node._parent 

221 else: 

222 node = node._GetNodeOrValue(p) 

223 

224 if isinstance(node, Dictionary): 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 raise Exception(f"Error when resolving path expression '{':'.join(path)}' at '{p}'.") from TypeError(f"") # XXX: needs error messages 

226 

227 return node 

228 

229 def _GetNodeOrValueByPathExpression(self, path: List[KeyT]) -> ValueT: 

230 node = self 

231 for p in path: 

232 if p == "..": 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true

233 node = node._parent 

234 else: 

235 node = node._GetNodeOrValue(p) 

236 

237 return node 

238 

239 

240@export 

241class Dictionary(Node, Abstract_Dict): 

242 """A dictionary node in a YAML data file.""" 

243 

244 _keys: List[KeyT] #: List of keys in this dictionary. 

245 

246 def __init__( 

247 self, 

248 root: "Configuration", 

249 parent: NodeT, 

250 key: KeyT, 

251 yamlNode: CommentedMap 

252 ) -> None: 

253 """ 

254 Initializes a YAML dictionary. 

255 

256 :param root: Reference to the root node. 

257 :param parent: Reference to the parent node. 

258 :param key: 

259 :param yamlNode: Reference to the YAML node. 

260 """ 

261 Node.__init__(self, root, parent, key, yamlNode) 

262 

263 self._keys = [str(k) for k in yamlNode.keys()] 

264 

265 def __contains__(self, key: KeyT) -> bool: 

266 """ 

267 Checks if the key is in this dictionary. 

268 

269 :param key: The key to check. 

270 :returns: ``True``, if the key is in the dictionary. 

271 """ 

272 return key in self._keys 

273 

274 def __iter__(self) -> typing_Iterator[ValueT]: 

275 """ 

276 Returns an iterator to iterate dictionary keys. 

277 

278 :returns: Dictionary key iterator. 

279 """ 

280 

281 class Iterator(metaclass=ExtendedType, slots=True): 

282 """Iterator to iterate dictionary items.""" 

283 

284 _iter: typing_Iterator[ValueT] 

285 _obj: Dictionary 

286 

287 def __init__(self, obj: Dictionary) -> None: 

288 """ 

289 Initializes an iterator for a YAML dictionary node. 

290 

291 :param obj: YAML dictionary to iterate. 

292 """ 

293 self._iter = iter(obj._keys) 

294 self._obj = obj 

295 

296 def __iter__(self) -> Self: 

297 """ 

298 Return itself to fulfil the iterator protocol. 

299 

300 :returns: Itself. 

301 """ 

302 return self # pragma: no cover 

303 

304 def __next__(self) -> ValueT: 

305 """ 

306 Returns the next item in the dictionary. 

307 

308 :returns: Next item. 

309 """ 

310 key = next(self._iter) 

311 return self._obj[key] 

312 

313 return Iterator(self) 

314 

315 

316@export 

317class Sequence(Node, Abstract_Seq): 

318 """A sequence node (ordered list) in a YAML data file.""" 

319 

320 def __init__( 

321 self, 

322 root: "Configuration", 

323 parent: NodeT, 

324 key: KeyT, 

325 yamlNode: CommentedSeq 

326 ) -> None: 

327 """ 

328 Initializes a YAML sequence (list). 

329 

330 :param root: Reference to the root node. 

331 :param parent: Reference to the parent node. 

332 :param key: 

333 :param yamlNode: Reference to the YAML node. 

334 """ 

335 Node.__init__(self, root, parent, key, yamlNode) 

336 

337 self._length = len(yamlNode) 

338 

339 def __iter__(self) -> typing_Iterator[ValueT]: 

340 """ 

341 Returns an iterator to iterate items in the sequence of sub-nodes. 

342 

343 :returns: Iterator to iterate items in a sequence. 

344 """ 

345 class Iterator(metaclass=ExtendedType, slots=True): 

346 """Iterator to iterate sequence items.""" 

347 

348 _i: int #: internal iterator position 

349 _obj: Sequence #: Sequence object to iterate 

350 

351 def __init__(self, obj: Sequence) -> None: 

352 """ 

353 Initializes an iterator for a YAML sequence node. 

354 

355 :param obj: YAML sequence to iterate. 

356 """ 

357 self._i = 0 

358 self._obj = obj 

359 

360 def __iter__(self) -> Self: 

361 """ 

362 Return itself to fulfil the iterator protocol. 

363 

364 :returns: Itself. 

365 """ 

366 return self # pragma: no cover 

367 

368 def __next__(self) -> ValueT: 

369 """ 

370 Returns the next item in the sequence. 

371 

372 :returns: Next item. 

373 :raises StopIteration: If end of sequence is reached. 

374 """ 

375 try: 

376 result = self._obj[str(self._i)] 

377 self._i += 1 

378 return result 

379 except IndexError: 

380 raise StopIteration 

381 

382 return Iterator(self) 

383 

384 

385setattr(Node, "DICT_TYPE", Dictionary) 

386setattr(Node, "SEQ_TYPE", Sequence) 

387 

388 

389@export 

390class Configuration(Dictionary, Abstract_Configuration): 

391 """A configuration read from a YAML file.""" 

392 

393 _yamlConfig: YAML 

394 

395 def __init__(self, configFile: Path) -> None: 

396 """ 

397 Initializes a configuration instance that reads a YAML file as input. 

398 

399 All sequence items or dictionaries key-value-pairs in the YAML file are accessible via Python's dictionary syntax. 

400 

401 :param configFile: Configuration file to read and parse. 

402 """ 

403 if not configFile.exists(): 403 ↛ 404line 403 didn't jump to line 404 because the condition on line 403 was never true

404 raise ConfigurationException(f"JSON configuration file '{configFile}' not found.") from FileNotFoundError(configFile) 

405 

406 with configFile.open("r", encoding="utf-8") as file: 

407 self._yamlConfig = YAML().load(file) 

408 

409 Dictionary.__init__(self, self, self, None, self._yamlConfig) 

410 Abstract_Configuration.__init__(self, configFile) 

411 

412 def __getitem__(self, key: str) -> ValueT: 

413 """ 

414 Access a configuration node by key. 

415 

416 :param key: The key to look for. 

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

418 """ 

419 return self._GetNodeOrValue(str(key)) 

420 

421 def __setitem__(self, key: str, value: ValueT) -> None: 

422 raise NotImplementedError()