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

164 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-23 22:21 +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""" 

32Configuration reader for JSON files. 

33 

34.. hint:: 

35 

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

37""" 

38from json import load 

39from pathlib import Path 

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

41 

42try: 

43 from pyTooling.Decorators import export 

44 from pyTooling.MetaClasses import ExtendedType 

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

46 from pyTooling.Configuration import Node as Abstract_Node 

47 from pyTooling.Configuration import Dictionary as Abstract_Dict 

48 from pyTooling.Configuration import Sequence as Abstract_Seq 

49 from pyTooling.Configuration import Configuration as Abstract_Configuration 

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

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

52 

53 try: 

54 from Decorators import export 

55 from MetaClasses import ExtendedType 

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

57 from pyTooling.Configuration import Node as Abstract_Node 

58 from pyTooling.Configuration import Dictionary as Abstract_Dict 

59 from pyTooling.Configuration import Sequence as Abstract_Seq 

60 from pyTooling.Configuration import Configuration as Abstract_Configuration 

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

62 print("[pyTooling.Configuration.JSON] Could not import directly!") 

63 raise ex 

64 

65 

66@export 

67class Node(Abstract_Node): 

68 """ 

69 Node in a JSON configuration data structure. 

70 """ 

71 

72 _jsonNode: Union[Dict, List] #: Reference to the associated JSON node. 

73 _cache: Dict[str, ValueT] 

74 _key: KeyT #: Key of this node. 

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

76 

77 def __init__( 

78 self, 

79 root: "Configuration", 

80 parent: NodeT, 

81 key: KeyT, 

82 jsonNode: Union[Dict, List] 

83 ) -> None: 

84 """ 

85 Initializes a JSON node. 

86 

87 :param root: Reference to the root node. 

88 :param parent: Reference to the parent node. 

89 :param key: 

90 :param jsonNode: Reference to the JSON node. 

91 """ 

92 Abstract_Node.__init__(self, root, parent) 

93 

94 self._jsonNode = jsonNode 

95 self._cache = {} 

96 self._key = key 

97 self._length = len(jsonNode) 

98 

99 def __len__(self) -> int: 

100 """ 

101 Returns the number of sub-elements. 

102 

103 :returns: Number of sub-elements. 

104 """ 

105 return self._length 

106 

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

108 """ 

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

110 

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

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

113 """ 

114 return self._GetNodeOrValue(str(key)) 

115 

116 @property 

117 def Key(self) -> KeyT: 

118 """ 

119 Property to access the node's key. 

120 

121 :returns: Key of the node. 

122 """ 

123 return self._key 

124 

125 @Key.setter 

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

127 raise NotImplementedError() 

128 

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

130 """ 

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

132 

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

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

135 """ 

136 path = self._ToPath(query) 

137 return self._GetNodeOrValueByPathExpression(path) 

138 

139 @staticmethod 

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

141 return query.split(":") 

142 

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

144 try: 

145 value = self._cache[key] 

146 except KeyError: 

147 try: 

148 value = self._jsonNode[key] 

149 except (KeyError, TypeError): 

150 try: 

151 value = self._jsonNode[int(key)] 

152 except KeyError: 

153 try: 

154 value = self._jsonNode[float(key)] 

155 except KeyError as ex: 

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

157 

158 if isinstance(value, str): 

159 value = self._ResolveVariables(value) 

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

161 value = str(value) 

162 elif isinstance(value, dict): 

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

164 elif isinstance(value, list): 164 ↛ 167line 164 didn't jump to line 167 because the condition on line 164 was always true

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

166 else: 

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

168 

169 self._cache[key] = value 

170 

171 return value 

172 

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

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

175 return "" 

176 elif "$" not in value: 

177 return value 

178 

179 rawValue = value 

180 result = "" 

181 

182 while (len(rawValue) > 0): 

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

184 beginPos = rawValue.find("$") 

185 if beginPos < 0: 

186 result += rawValue 

187 rawValue = "" 

188 else: 

189 result += rawValue[:beginPos] 

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

191 result += "$" 

192 rawValue = rawValue[1:] 

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

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

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

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

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

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

199 path = rawValue[nextPos+2:endPos] 

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

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

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

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

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

205 else: 

206 path = rawValue[beginPos+2:endPos] 

207 rawValue = rawValue[endPos+1:] 

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

209 

210 return result 

211 

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

213 node = self 

214 for p in path: 

215 if p == "..": 

216 node = node._parent 

217 else: 

218 node = node._GetNodeOrValue(p) 

219 

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

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

222 

223 return node 

224 

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

226 node = self 

227 for p in path: 

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

229 node = node._parent 

230 else: 

231 node = node._GetNodeOrValue(p) 

232 

233 return node 

234 

235 

236@export 

237class Dictionary(Node, Abstract_Dict): 

238 """A dictionary node in a JSON data file.""" 

239 

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

241 

242 def __init__( 

243 self, 

244 root: "Configuration", 

245 parent: NodeT, 

246 key: KeyT, 

247 jsonNode: Dict 

248 ) -> None: 

249 """ 

250 Initializes a JSON dictionary. 

251 

252 :param root: Reference to the root node. 

253 :param parent: Reference to the parent node. 

254 :param key: 

255 :param jsonNode: Reference to the JSON node. 

256 """ 

257 Node.__init__(self, root, parent, key, jsonNode) 

258 

259 self._keys = [str(k) for k in jsonNode.keys()] 

260 

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

262 """ 

263 Checks if the key is in this dictionary. 

264 

265 :param key: The key to check. 

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

267 """ 

268 return key in self._keys 

269 

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

271 """ 

272 Returns an iterator to iterate dictionary keys. 

273 

274 :returns: Dictionary key iterator. 

275 """ 

276 

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

278 """Iterator to iterate dictionary items.""" 

279 

280 _iter: typing_Iterator 

281 _obj: Dictionary 

282 

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

284 """ 

285 Initializes an iterator for a JSON dictionary node. 

286 

287 :param obj: JSON dictionary to iterate. 

288 """ 

289 self._iter = iter(obj._keys) 

290 self._obj = obj 

291 

292 def __iter__(self) -> Self: 

293 """ 

294 Return itself to fulfil the iterator protocol. 

295 

296 :returns: Itself. 

297 """ 

298 return self # pragma: no cover 

299 

300 def __next__(self) -> ValueT: 

301 """ 

302 Returns the next item in the dictionary. 

303 

304 :returns: Next item. 

305 """ 

306 key = next(self._iter) 

307 return self._obj[key] 

308 

309 return Iterator(self) 

310 

311 

312@export 

313class Sequence(Node, Abstract_Seq): 

314 """A sequence node (ordered list) in a JSON data file.""" 

315 

316 def __init__( 

317 self, 

318 root: "Configuration", 

319 parent: NodeT, 

320 key: KeyT, 

321 jsonNode: List 

322 ) -> None: 

323 """ 

324 Initializes a JSON sequence (list). 

325 

326 :param root: Reference to the root node. 

327 :param parent: Reference to the parent node. 

328 :param key: 

329 :param jsonNode: Reference to the JSON node. 

330 """ 

331 Node.__init__(self, root, parent, key, jsonNode) 

332 

333 self._length = len(jsonNode) 

334 

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

336 """ 

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

338 

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

340 """ 

341 

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

343 """Iterator to iterate sequence items.""" 

344 

345 _i: int #: internal iterator position 

346 _obj: Sequence #: Sequence object to iterate 

347 

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

349 """ 

350 Initializes an iterator for a JSON sequence node. 

351 

352 :param obj: YAML sequence to iterate. 

353 """ 

354 self._i = 0 

355 self._obj = obj 

356 

357 def __iter__(self) -> Self: 

358 """ 

359 Return itself to fulfil the iterator protocol. 

360 

361 :returns: Itself. 

362 """ 

363 return self # pragma: no cover 

364 

365 def __next__(self) -> ValueT: 

366 """ 

367 Returns the next item in the sequence. 

368 

369 :returns: Next item. 

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

371 """ 

372 try: 

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

374 self._i += 1 

375 return result 

376 except IndexError: 

377 raise StopIteration 

378 

379 return Iterator(self) 

380 

381 

382setattr(Node, "DICT_TYPE", Dictionary) 

383setattr(Node, "SEQ_TYPE", Sequence) 

384 

385 

386@export 

387class Configuration(Dictionary, Abstract_Configuration): 

388 """A configuration read from a JSON file.""" 

389 

390 _jsonConfig: Dict 

391 

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

393 """ 

394 Initializes a configuration instance that reads a JSON file as input. 

395 

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

397 

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

399 """ 

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

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

402 

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

404 self._jsonConfig = load(file) 

405 

406 Dictionary.__init__(self, self, self, None, self._jsonConfig) 

407 Abstract_Configuration.__init__(self, configFile) 

408 

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

410 """ 

411 Access a configuration node by key. 

412 

413 :param key: The key to look for. 

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

415 """ 

416 return self._GetNodeOrValue(str(key)) 

417 

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

419 raise NotImplementedError()