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
« 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.
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
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
51@export
52class Node(Abstract_Node):
53 """
54 Node in a JSON configuration data structure.
55 """
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.
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.
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)
79 self._jsonNode = jsonNode
80 self._cache = {}
81 self._key = key
82 self._length = len(jsonNode)
84 def __len__(self) -> int:
85 """
86 Returns the number of sub-elements.
88 :returns: Number of sub-elements.
89 """
90 return self._length
92 def __getitem__(self, key: KeyT) -> ValueT:
93 """
94 Access an element in the node by index or key.
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))
101 @property
102 def Key(self) -> KeyT:
103 """
104 Property to access the node's key.
106 :returns: Key of the node.
107 """
108 return self._key
110 @Key.setter
111 def Key(self, value: KeyT) -> None:
112 raise NotImplementedError()
114 def QueryPath(self, query: str) -> ValueT:
115 """
116 Return a node or value based on a path description to that node or value.
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)
124 @staticmethod
125 def _ToPath(query: str) -> List[Union[str, int]]:
126 return query.split(":")
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
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
154 self._cache[key] = value
156 return value
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
164 rawValue = value
165 result = ""
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)))
195 return result
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)
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
208 return node
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)
218 return node
221@export
222class Dictionary(Node, Abstract_Dict):
223 """A dictionary node in a JSON data file."""
225 _keys: List[KeyT] #: List of keys in this dictionary.
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.
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)
244 self._keys = [str(k) for k in jsonNode.keys()]
246 def __contains__(self, key: KeyT) -> bool:
247 """
248 Checks if the key is in this dictionary.
250 :param key: The key to check.
251 :returns: ``True``, if the key is in the dictionary.
252 """
253 return key in self._keys
255 def __iter__(self) -> typing_Iterator[ValueT]:
256 """
257 Returns an iterator to iterate dictionary keys.
259 :returns: Dictionary key iterator.
260 """
262 class Iterator(metaclass=ExtendedType, slots=True):
263 """Iterator to iterate dictionary items."""
265 _iter: typing_Iterator
266 _obj: Dictionary
268 def __init__(self, obj: Dictionary) -> None:
269 """
270 Initializes an iterator for a JSON dictionary node.
272 :param obj: JSON dictionary to iterate.
273 """
274 self._iter = iter(obj._keys)
275 self._obj = obj
277 def __iter__(self) -> Self:
278 """
279 Return itself to fulfil the iterator protocol.
281 :returns: Itself.
282 """
283 return self # pragma: no cover
285 def __next__(self) -> ValueT:
286 """
287 Returns the next item in the dictionary.
289 :returns: Next item.
290 """
291 key = next(self._iter)
292 return self._obj[key]
294 return Iterator(self)
297@export
298class Sequence(Node, Abstract_Seq):
299 """A sequence node (ordered list) in a JSON data file."""
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).
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)
318 self._length = len(jsonNode)
320 def __iter__(self) -> typing_Iterator[ValueT]:
321 """
322 Returns an iterator to iterate items in the sequence of sub-nodes.
324 :returns: Iterator to iterate items in a sequence.
325 """
327 class Iterator(metaclass=ExtendedType, slots=True):
328 """Iterator to iterate sequence items."""
330 _i: int #: internal iterator position
331 _obj: Sequence #: Sequence object to iterate
333 def __init__(self, obj: Sequence) -> None:
334 """
335 Initializes an iterator for a JSON sequence node.
337 :param obj: YAML sequence to iterate.
338 """
339 self._i = 0
340 self._obj = obj
342 def __iter__(self) -> Self:
343 """
344 Return itself to fulfil the iterator protocol.
346 :returns: Itself.
347 """
348 return self # pragma: no cover
350 def __next__(self) -> ValueT:
351 """
352 Returns the next item in the sequence.
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
364 return Iterator(self)
367setattr(Node, "DICT_TYPE", Dictionary)
368setattr(Node, "SEQ_TYPE", Sequence)
371@export
372class Configuration(Dictionary, Abstract_Configuration):
373 """A configuration read from a JSON file."""
375 _jsonConfig: Dict
377 def __init__(self, configFile: Path) -> None:
378 """
379 Initializes a configuration instance that reads a JSON file as input.
381 All sequence items or dictionaries key-value-pairs in the JSON file are accessible via Python's dictionary syntax.
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)
388 with configFile.open("r", encoding="utf-8") as file:
389 self._jsonConfig = load(file)
391 Dictionary.__init__(self, self, self, None, self._jsonConfig)
392 Abstract_Configuration.__init__(self, configFile)
394 def __getitem__(self, key: str) -> ValueT:
395 """
396 Access a configuration node by key.
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))
403 def __setitem__(self, key: str, value: ValueT) -> None:
404 raise NotImplementedError()