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
« 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.
34.. hint::
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
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.*'!")
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
66@export
67class Node(Abstract_Node):
68 """
69 Node in a JSON configuration data structure.
70 """
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.
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.
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)
94 self._jsonNode = jsonNode
95 self._cache = {}
96 self._key = key
97 self._length = len(jsonNode)
99 def __len__(self) -> int:
100 """
101 Returns the number of sub-elements.
103 :returns: Number of sub-elements.
104 """
105 return self._length
107 def __getitem__(self, key: KeyT) -> ValueT:
108 """
109 Access an element in the node by index or key.
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))
116 @property
117 def Key(self) -> KeyT:
118 """
119 Property to access the node's key.
121 :returns: Key of the node.
122 """
123 return self._key
125 @Key.setter
126 def Key(self, value: KeyT) -> None:
127 raise NotImplementedError()
129 def QueryPath(self, query: str) -> ValueT:
130 """
131 Return a node or value based on a path description to that node or value.
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)
139 @staticmethod
140 def _ToPath(query: str) -> List[Union[str, int]]:
141 return query.split(":")
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
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
169 self._cache[key] = value
171 return value
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
179 rawValue = value
180 result = ""
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)))
210 return result
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)
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
223 return node
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)
233 return node
236@export
237class Dictionary(Node, Abstract_Dict):
238 """A dictionary node in a JSON data file."""
240 _keys: List[KeyT] #: List of keys in this dictionary.
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.
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)
259 self._keys = [str(k) for k in jsonNode.keys()]
261 def __contains__(self, key: KeyT) -> bool:
262 """
263 Checks if the key is in this dictionary.
265 :param key: The key to check.
266 :returns: ``True``, if the key is in the dictionary.
267 """
268 return key in self._keys
270 def __iter__(self) -> typing_Iterator[ValueT]:
271 """
272 Returns an iterator to iterate dictionary keys.
274 :returns: Dictionary key iterator.
275 """
277 class Iterator(metaclass=ExtendedType, slots=True):
278 """Iterator to iterate dictionary items."""
280 _iter: typing_Iterator
281 _obj: Dictionary
283 def __init__(self, obj: Dictionary) -> None:
284 """
285 Initializes an iterator for a JSON dictionary node.
287 :param obj: JSON dictionary to iterate.
288 """
289 self._iter = iter(obj._keys)
290 self._obj = obj
292 def __iter__(self) -> Self:
293 """
294 Return itself to fulfil the iterator protocol.
296 :returns: Itself.
297 """
298 return self # pragma: no cover
300 def __next__(self) -> ValueT:
301 """
302 Returns the next item in the dictionary.
304 :returns: Next item.
305 """
306 key = next(self._iter)
307 return self._obj[key]
309 return Iterator(self)
312@export
313class Sequence(Node, Abstract_Seq):
314 """A sequence node (ordered list) in a JSON data file."""
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).
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)
333 self._length = len(jsonNode)
335 def __iter__(self) -> typing_Iterator[ValueT]:
336 """
337 Returns an iterator to iterate items in the sequence of sub-nodes.
339 :returns: Iterator to iterate items in a sequence.
340 """
342 class Iterator(metaclass=ExtendedType, slots=True):
343 """Iterator to iterate sequence items."""
345 _i: int #: internal iterator position
346 _obj: Sequence #: Sequence object to iterate
348 def __init__(self, obj: Sequence) -> None:
349 """
350 Initializes an iterator for a JSON sequence node.
352 :param obj: YAML sequence to iterate.
353 """
354 self._i = 0
355 self._obj = obj
357 def __iter__(self) -> Self:
358 """
359 Return itself to fulfil the iterator protocol.
361 :returns: Itself.
362 """
363 return self # pragma: no cover
365 def __next__(self) -> ValueT:
366 """
367 Returns the next item in the sequence.
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
379 return Iterator(self)
382setattr(Node, "DICT_TYPE", Dictionary)
383setattr(Node, "SEQ_TYPE", Sequence)
386@export
387class Configuration(Dictionary, Abstract_Configuration):
388 """A configuration read from a JSON file."""
390 _jsonConfig: Dict
392 def __init__(self, configFile: Path) -> None:
393 """
394 Initializes a configuration instance that reads a JSON file as input.
396 All sequence items or dictionaries key-value-pairs in the JSON file are accessible via Python's dictionary syntax.
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)
403 with configFile.open("r", encoding="utf-8") as file:
404 self._jsonConfig = load(file)
406 Dictionary.__init__(self, self, self, None, self._jsonConfig)
407 Abstract_Configuration.__init__(self, configFile)
409 def __getitem__(self, key: str) -> ValueT:
410 """
411 Access a configuration node by key.
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))
418 def __setitem__(self, key: str, value: ValueT) -> None:
419 raise NotImplementedError()