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
« 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.
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
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.*'!")
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
64@export
65class Node(Abstract_Node):
66 _jsonNode: Union[Dict, List]
67 _cache: Dict[str, ValueT]
68 _key: KeyT
69 _length: int
71 def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: Union[Dict, List]) -> None:
72 Abstract_Node.__init__(self, root, parent)
74 self._jsonNode = jsonNode
75 self._cache = {}
76 self._key = key
77 self._length = len(jsonNode)
79 def __len__(self) -> int:
80 """
81 Returns the number of sub-elements.
83 :returns: Number of sub-elements.
84 """
85 return self._length
87 def __getitem__(self, key: KeyT) -> ValueT:
88 return self._GetNodeOrValue(str(key))
90 @property
91 def Key(self) -> KeyT:
92 return self._key
94 @Key.setter
95 def Key(self, value: KeyT) -> None:
96 raise NotImplementedError()
98 def QueryPath(self, query: str) -> ValueT:
99 path = self._ToPath(query)
100 return self._GetNodeOrValueByPathExpression(path)
102 @staticmethod
103 def _ToPath(query: str) -> List[Union[str, int]]:
104 return query.split(":")
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
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
132 self._cache[key] = value
134 return value
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
142 rawValue = value
143 result = ""
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)))
173 return result
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)
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
186 return node
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)
196 return node
199@export
200class Dictionary(Node, Abstract_Dict):
201 """A dictionary node in a JSON data file."""
203 _keys: List[KeyT]
205 def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: Dict) -> None:
206 Node.__init__(self, root, parent, key, jsonNode)
208 self._keys = [str(k) for k in jsonNode.keys()]
210 def __contains__(self, key: KeyT) -> bool:
211 return key in self._keys
213 def __iter__(self) -> typing_Iterator[ValueT]:
214 class Iterator(metaclass=ExtendedType, slots=True):
215 _iter: typing_Iterator
216 _obj: Dictionary
218 def __init__(self, obj: Dictionary) -> None:
219 self._iter = iter(obj._keys)
220 self._obj = obj
222 def __iter__(self) -> "Iterator":
223 """
224 Return itself to fulfil the iterator protocol.
226 :returns: Itself.
227 """
228 return self # pragma: no cover
230 def __next__(self) -> ValueT:
231 """
232 Returns the next item in the dictionary.
234 :returns: Next item.
235 """
236 key = next(self._iter)
237 return self._obj[key]
239 return Iterator(self)
242@export
243class Sequence(Node, Abstract_Seq):
244 """A sequence node (ordered list) in a JSON data file."""
246 def __init__(self, root: "Configuration", parent: NodeT, key: KeyT, jsonNode: List) -> None:
247 Node.__init__(self, root, parent, key, jsonNode)
249 self._length = len(jsonNode)
251 def __getitem__(self, key: KeyT) -> ValueT:
252 return self._GetNodeOrValue(str(key))
254 def __iter__(self) -> typing_Iterator[ValueT]:
255 """
256 Returns an iterator to iterate items in the sequence of sub-nodes.
258 :returns: Iterator to iterate items in a sequence.
259 """
260 class Iterator(metaclass=ExtendedType, slots=True):
261 """Iterator to iterate sequence items."""
263 _i: int #: internal iterator position
264 _obj: Sequence #: Sequence object to iterate
266 def __init__(self, obj: Sequence) -> None:
267 self._i = 0
268 self._obj = obj
270 def __iter__(self) -> "Iterator":
271 """
272 Return itself to fulfil the iterator protocol.
274 :returns: Itself.
275 """
276 return self # pragma: no cover
278 def __next__(self) -> ValueT:
279 """
280 Returns the next item in the sequence.
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
292 return Iterator(self)
295setattr(Node, "DICT_TYPE", Dictionary)
296setattr(Node, "SEQ_TYPE", Sequence)
299@export
300class Configuration(Dictionary, Abstract_Configuration):
301 """A configuration read from a JSON file."""
303 _jsonConfig: Dict
305 def __init__(self, configFile: Path) -> None:
306 """
307 Initializes a configuration instance that reads a JSON file as input.
309 All sequence items or dictionaries key-value-pairs in the JSON file are accessible via Python's dictionary syntax.
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)
316 with configFile.open("r", encoding="utf-8") as file:
317 self._jsonConfig = load(file)
319 Dictionary.__init__(self, self, self, None, self._jsonConfig)
320 Abstract_Configuration.__init__(self, configFile)
322 def __getitem__(self, key: str) -> ValueT:
323 """
324 Access a configuration node by key.
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))
331 def __setitem__(self, key: str, value: ValueT) -> None:
332 raise NotImplementedError()