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

166 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-25 22:22 +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 JSON files. 

33 

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

35""" 

36from json import load 

37from pathlib import Path 

38from typing import Dict, List, Union, Iterator as typing_Iterator 

39 

40try: 

41 from pyTooling.Decorators import export 

42 from pyTooling.MetaClasses import ExtendedType 

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

44 from pyTooling.Configuration import Node as Abstract_Node 

45 from pyTooling.Configuration import Dictionary as Abstract_Dict 

46 from pyTooling.Configuration import Sequence as Abstract_Seq 

47 from pyTooling.Configuration import Configuration as Abstract_Configuration 

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

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

50 

51 try: 

52 from Decorators import export 

53 from MetaClasses import ExtendedType 

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

55 from pyTooling.Configuration import Node as Abstract_Node 

56 from pyTooling.Configuration import Dictionary as Abstract_Dict 

57 from pyTooling.Configuration import Sequence as Abstract_Seq 

58 from pyTooling.Configuration import Configuration as Abstract_Configuration 

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

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

61 raise ex 

62 

63 

64@export 

65class Node(Abstract_Node): 

66 _jsonNode: Union[Dict, List] 

67 _cache: Dict[str, ValueT] 

68 _key: KeyT 

69 _length: int 

70 

71 def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: Union[Dict, List]) -> None: 

72 Abstract_Node.__init__(self, root, parent) 

73 

74 self._jsonNode = jsonNode 

75 self._cache = {} 

76 self._key = key 

77 self._length = len(jsonNode) 

78 

79 def __len__(self) -> int: 

80 """ 

81 Returns the number of sub-elements. 

82 

83 :returns: Number of sub-elements. 

84 """ 

85 return self._length 

86 

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

88 return self._GetNodeOrValue(str(key)) 

89 

90 @property 

91 def Key(self) -> KeyT: 

92 return self._key 

93 

94 @Key.setter 

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

96 raise NotImplementedError() 

97 

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

99 path = self._ToPath(query) 

100 return self._GetNodeOrValueByPathExpression(path) 

101 

102 @staticmethod 

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

104 return query.split(":") 

105 

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

107 try: 

108 value = self._cache[key] 

109 except KeyError: 

110 try: 

111 value = self._jsonNode[key] 

112 except (KeyError, TypeError): 

113 try: 

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

115 except KeyError: 

116 try: 

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

118 except KeyError as ex: 

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

120 

121 if isinstance(value, str): 

122 value = self._ResolveVariables(value) 

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

124 value = str(value) 

125 elif isinstance(value, dict): 

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

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

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

129 else: 

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

131 

132 self._cache[key] = value 

133 

134 return value 

135 

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

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

138 return "" 

139 elif "$" not in value: 

140 return value 

141 

142 rawValue = value 

143 result = "" 

144 

145 while (len(rawValue) > 0): 

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

147 beginPos = rawValue.find("$") 

148 if beginPos < 0: 

149 result += rawValue 

150 rawValue = "" 

151 else: 

152 result += rawValue[:beginPos] 

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

154 result += "$" 

155 rawValue = rawValue[1:] 

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

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

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

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

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

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

162 path = rawValue[nextPos+2:endPos] 

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

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

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

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

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

168 else: 

169 path = rawValue[beginPos+2:endPos] 

170 rawValue = rawValue[endPos+1:] 

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

172 

173 return result 

174 

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

176 node = self 

177 for p in path: 

178 if p == "..": 

179 node = node._parent 

180 else: 

181 node = node._GetNodeOrValue(p) 

182 

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

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

185 

186 return node 

187 

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

189 node = self 

190 for p in path: 

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

192 node = node._parent 

193 else: 

194 node = node._GetNodeOrValue(p) 

195 

196 return node 

197 

198 

199@export 

200class Dictionary(Node, Abstract_Dict): 

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

202 

203 _keys: List[KeyT] 

204 

205 def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: Dict) -> None: 

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

207 

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

209 

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

211 return key in self._keys 

212 

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

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

215 _iter: typing_Iterator 

216 _obj: Dictionary 

217 

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

219 self._iter = iter(obj._keys) 

220 self._obj = obj 

221 

222 def __iter__(self) -> "Iterator": 

223 """ 

224 Return itself to fulfil the iterator protocol. 

225 

226 :returns: Itself. 

227 """ 

228 return self # pragma: no cover 

229 

230 def __next__(self) -> ValueT: 

231 """ 

232 Returns the next item in the dictionary. 

233 

234 :returns: Next item. 

235 """ 

236 key = next(self._iter) 

237 return self._obj[key] 

238 

239 return Iterator(self) 

240 

241 

242@export 

243class Sequence(Node, Abstract_Seq): 

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

245 

246 def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: List) -> None: 

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

248 

249 self._length = len(jsonNode) 

250 

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

252 return self._GetNodeOrValue(str(key)) 

253 

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

255 """ 

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

257 

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

259 """ 

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

261 """Iterator to iterate sequence items.""" 

262 

263 _i: int #: internal iterator position 

264 _obj: Sequence #: Sequence object to iterate 

265 

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

267 self._i = 0 

268 self._obj = obj 

269 

270 def __iter__(self) -> "Iterator": 

271 """ 

272 Return itself to fulfil the iterator protocol. 

273 

274 :returns: Itself. 

275 """ 

276 return self # pragma: no cover 

277 

278 def __next__(self) -> ValueT: 

279 """ 

280 Returns the next item in the sequence. 

281 

282 :returns: Next item. 

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

284 """ 

285 try: 

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

287 self._i += 1 

288 return result 

289 except IndexError: 

290 raise StopIteration 

291 

292 return Iterator(self) 

293 

294 

295setattr(Node, "DICT_TYPE", Dictionary) 

296setattr(Node, "SEQ_TYPE", Sequence) 

297 

298 

299@export 

300class Configuration(Dictionary, Abstract_Configuration): 

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

302 

303 _jsonConfig: Dict 

304 

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

306 """ 

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

308 

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

310 

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

312 """ 

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

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

315 

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

317 self._jsonConfig = load(file) 

318 

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

320 Abstract_Configuration.__init__(self, configFile) 

321 

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

323 """ 

324 Access a configuration node by key. 

325 

326 :param key: The key to look for. 

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

328 """ 

329 return self._GetNodeOrValue(str(key)) 

330 

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

332 raise NotImplementedError()