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

163 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-13 22:36 +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 

42from pyTooling.Decorators import export 

43from pyTooling.MetaClasses import ExtendedType 

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

45from pyTooling.Configuration import Node as Abstract_Node 

46from pyTooling.Configuration import Dictionary as Abstract_Dict 

47from pyTooling.Configuration import Sequence as Abstract_Seq 

48from pyTooling.Configuration import Configuration as Abstract_Configuration 

49 

50 

51@export 

52class Node(Abstract_Node): 

53 """ 

54 Node in a JSON configuration data structure. 

55 """ 

56 

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

58 _cache: Dict[str, ValueT] 

59 _key: KeyT #: Key of this node. 

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

61 

62 def __init__( 

63 self, 

64 root: "Configuration", 

65 parent: NodeT, 

66 key: KeyT, 

67 jsonNode: Union[Dict, List] 

68 ) -> None: 

69 """ 

70 Initializes a JSON node. 

71 

72 :param root: Reference to the root node. 

73 :param parent: Reference to the parent node. 

74 :param key: 

75 :param jsonNode: Reference to the JSON node. 

76 """ 

77 Abstract_Node.__init__(self, root, parent) 

78 

79 self._jsonNode = jsonNode 

80 self._cache = {} 

81 self._key = key 

82 self._length = len(jsonNode) 

83 

84 def __len__(self) -> int: 

85 """ 

86 Returns the number of sub-elements. 

87 

88 :returns: Number of sub-elements. 

89 """ 

90 return self._length 

91 

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

93 """ 

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

95 

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

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

98 """ 

99 return self._GetNodeOrValue(str(key)) 

100 

101 @property 

102 def Key(self) -> KeyT: 

103 """ 

104 Property to access the node's key. 

105 

106 :returns: Key of the node. 

107 """ 

108 return self._key 

109 

110 @Key.setter 

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

112 raise NotImplementedError() 

113 

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

115 """ 

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

117 

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

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

120 """ 

121 path = self._ToPath(query) 

122 return self._GetNodeOrValueByPathExpression(path) 

123 

124 @staticmethod 

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

126 return query.split(":") 

127 

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

129 try: 

130 value = self._cache[key] 

131 except KeyError: 

132 try: 

133 value = self._jsonNode[key] 

134 except (KeyError, TypeError): 

135 try: 

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

137 except KeyError: 

138 try: 

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

140 except KeyError as ex: 

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

142 

143 if isinstance(value, str): 

144 value = self._ResolveVariables(value) 

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

146 value = str(value) 

147 elif isinstance(value, dict): 

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

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

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

151 else: 

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

153 

154 self._cache[key] = value 

155 

156 return value 

157 

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

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

160 return "" 

161 elif "$" not in value: 

162 return value 

163 

164 rawValue = value 

165 result = "" 

166 

167 while (len(rawValue) > 0): 

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

169 beginPos = rawValue.find("$") 

170 if beginPos < 0: 

171 result += rawValue 

172 rawValue = "" 

173 else: 

174 result += rawValue[:beginPos] 

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

176 result += "$" 

177 rawValue = rawValue[1:] 

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

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

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

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

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

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

184 path = rawValue[nextPos+2:endPos] 

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

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

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

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

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

190 else: 

191 path = rawValue[beginPos+2:endPos] 

192 rawValue = rawValue[endPos+1:] 

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

194 

195 return result 

196 

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

198 node = self 

199 for p in path: 

200 if p == "..": 

201 node = node._parent 

202 else: 

203 node = node._GetNodeOrValue(p) 

204 

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

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

207 

208 return node 

209 

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

211 node = self 

212 for p in path: 

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

214 node = node._parent 

215 else: 

216 node = node._GetNodeOrValue(p) 

217 

218 return node 

219 

220 

221@export 

222class Dictionary(Node, Abstract_Dict): 

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

224 

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

226 

227 def __init__( 

228 self, 

229 root: "Configuration", 

230 parent: NodeT, 

231 key: KeyT, 

232 jsonNode: Dict 

233 ) -> None: 

234 """ 

235 Initializes a JSON dictionary. 

236 

237 :param root: Reference to the root node. 

238 :param parent: Reference to the parent node. 

239 :param key: 

240 :param jsonNode: Reference to the JSON node. 

241 """ 

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

243 

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

245 

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

247 """ 

248 Checks if the key is in this dictionary. 

249 

250 :param key: The key to check. 

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

252 """ 

253 return key in self._keys 

254 

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

256 """ 

257 Returns an iterator to iterate dictionary keys. 

258 

259 :returns: Dictionary key iterator. 

260 """ 

261 

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

263 """Iterator to iterate dictionary items.""" 

264 

265 _iter: typing_Iterator 

266 _obj: Dictionary 

267 

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

269 """ 

270 Initializes an iterator for a JSON dictionary node. 

271 

272 :param obj: JSON dictionary to iterate. 

273 """ 

274 self._iter = iter(obj._keys) 

275 self._obj = obj 

276 

277 def __iter__(self) -> Self: 

278 """ 

279 Return itself to fulfil the iterator protocol. 

280 

281 :returns: Itself. 

282 """ 

283 return self # pragma: no cover 

284 

285 def __next__(self) -> ValueT: 

286 """ 

287 Returns the next item in the dictionary. 

288 

289 :returns: Next item. 

290 """ 

291 key = next(self._iter) 

292 return self._obj[key] 

293 

294 return Iterator(self) 

295 

296 

297@export 

298class Sequence(Node, Abstract_Seq): 

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

300 

301 def __init__( 

302 self, 

303 root: "Configuration", 

304 parent: NodeT, 

305 key: KeyT, 

306 jsonNode: List 

307 ) -> None: 

308 """ 

309 Initializes a JSON sequence (list). 

310 

311 :param root: Reference to the root node. 

312 :param parent: Reference to the parent node. 

313 :param key: 

314 :param jsonNode: Reference to the JSON node. 

315 """ 

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

317 

318 self._length = len(jsonNode) 

319 

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

321 """ 

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

323 

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

325 """ 

326 

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

328 """Iterator to iterate sequence items.""" 

329 

330 _i: int #: internal iterator position 

331 _obj: Sequence #: Sequence object to iterate 

332 

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

334 """ 

335 Initializes an iterator for a JSON sequence node. 

336 

337 :param obj: YAML sequence to iterate. 

338 """ 

339 self._i = 0 

340 self._obj = obj 

341 

342 def __iter__(self) -> Self: 

343 """ 

344 Return itself to fulfil the iterator protocol. 

345 

346 :returns: Itself. 

347 """ 

348 return self # pragma: no cover 

349 

350 def __next__(self) -> ValueT: 

351 """ 

352 Returns the next item in the sequence. 

353 

354 :returns: Next item. 

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

356 """ 

357 try: 

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

359 self._i += 1 

360 return result 

361 except IndexError: 

362 raise StopIteration 

363 

364 return Iterator(self) 

365 

366 

367setattr(Node, "DICT_TYPE", Dictionary) 

368setattr(Node, "SEQ_TYPE", Sequence) 

369 

370 

371@export 

372class Configuration(Dictionary, Abstract_Configuration): 

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

374 

375 _jsonConfig: Dict 

376 

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

378 """ 

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

380 

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

382 

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

384 """ 

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

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

387 

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

389 self._jsonConfig = load(file) 

390 

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

392 Abstract_Configuration.__init__(self, configFile) 

393 

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

395 """ 

396 Access a configuration node by key. 

397 

398 :param key: The key to look for. 

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

400 """ 

401 return self._GetNodeOrValue(str(key)) 

402 

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

404 raise NotImplementedError()