FourCIPP 1.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fourcipp/__init__.py +34 -0
- fourcipp/config/4C_metadata.yaml +63447 -0
- fourcipp/config/4C_schema.json +402481 -0
- fourcipp/config/config.yaml +12 -0
- fourcipp/fourc_input.py +697 -0
- fourcipp/legacy_io/__init__.py +162 -0
- fourcipp/legacy_io/element.py +135 -0
- fourcipp/legacy_io/inline_dat.py +224 -0
- fourcipp/legacy_io/node.py +114 -0
- fourcipp/legacy_io/node_topology.py +194 -0
- fourcipp/legacy_io/particle.py +71 -0
- fourcipp/utils/__init__.py +22 -0
- fourcipp/utils/cli.py +168 -0
- fourcipp/utils/configuration.py +222 -0
- fourcipp/utils/converter.py +138 -0
- fourcipp/utils/dict_utils.py +407 -0
- fourcipp/utils/metadata.py +1017 -0
- fourcipp/utils/not_set.py +77 -0
- fourcipp/utils/type_hinting.py +44 -0
- fourcipp/utils/validation.py +155 -0
- fourcipp/utils/yaml_io.py +172 -0
- fourcipp/version.py +34 -0
- fourcipp-1.50.0.dist-info/METADATA +225 -0
- fourcipp-1.50.0.dist-info/RECORD +28 -0
- fourcipp-1.50.0.dist-info/WHEEL +5 -0
- fourcipp-1.50.0.dist-info/entry_points.txt +2 -0
- fourcipp-1.50.0.dist-info/licenses/LICENSE +21 -0
- fourcipp-1.50.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 FourCIPP Authors
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
"""Converter to convert custom types to native Python types."""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Converter:
|
|
33
|
+
"""Converter class to convert custom types to native Python types."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._custom_converters: dict = {}
|
|
37
|
+
|
|
38
|
+
def register_type(
|
|
39
|
+
self,
|
|
40
|
+
custom_type: type,
|
|
41
|
+
converter_function: Callable[[Converter, Any], Any],
|
|
42
|
+
) -> Converter:
|
|
43
|
+
"""Register a custom type and its converter function.
|
|
44
|
+
|
|
45
|
+
The first argument of the converter_function is a converter object.
|
|
46
|
+
This allows you to pass self down to your converter function to
|
|
47
|
+
recursively call it and use custom types you already registered.
|
|
48
|
+
The second argument is the object to be converted.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
custom_type: Custom class to register
|
|
52
|
+
converter_function: Converter function
|
|
53
|
+
"""
|
|
54
|
+
self._custom_converters[custom_type] = converter_function
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def register_types(self, types_dict: dict) -> Converter:
|
|
58
|
+
"""Register multiple custom types and their converter functions.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
types_dict: Dictionary with custom types as keys and
|
|
62
|
+
converter functions as values
|
|
63
|
+
"""
|
|
64
|
+
self._custom_converters.update(types_dict)
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def register_numpy_types(self) -> Converter:
|
|
68
|
+
"""Register NumPy types and their converter functions."""
|
|
69
|
+
|
|
70
|
+
def convert_ndarray(converter: Converter, obj: np.ndarray) -> list[Any]:
|
|
71
|
+
"""Convert a NumPy ndarray to a list.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
converter: Converter object
|
|
75
|
+
obj: NumPy ndarray to convert
|
|
76
|
+
"""
|
|
77
|
+
return converter(obj.tolist())
|
|
78
|
+
|
|
79
|
+
def convert_generic(
|
|
80
|
+
converter: Converter, obj: np.generic
|
|
81
|
+
) -> int | float | bool | str:
|
|
82
|
+
"""Convert a NumPy generic type to a native Python type.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
converter: Converter object
|
|
86
|
+
obj: NumPy generic type to convert
|
|
87
|
+
"""
|
|
88
|
+
return obj.item()
|
|
89
|
+
|
|
90
|
+
self.register_type(np.generic, convert_generic)
|
|
91
|
+
self.register_type(np.ndarray, convert_ndarray)
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def __call__(self, obj: Any) -> Any:
|
|
95
|
+
"""Convert the object to a native Python type.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
obj: Object to convert
|
|
99
|
+
"""
|
|
100
|
+
# If no custom converters are present, no need to do a conversion
|
|
101
|
+
if not self._custom_converters:
|
|
102
|
+
return obj
|
|
103
|
+
|
|
104
|
+
# Look if object is present in the custom converters
|
|
105
|
+
for custom_type in self._custom_converters:
|
|
106
|
+
if isinstance(obj, custom_type):
|
|
107
|
+
# First match will be used.
|
|
108
|
+
# Keep in mind for inheritance you have think about child classes!
|
|
109
|
+
# Make sure if you have parent and child classes registered separately, to first register the child classes!
|
|
110
|
+
return self._custom_converters[custom_type](self, obj)
|
|
111
|
+
|
|
112
|
+
# Lets convert
|
|
113
|
+
match obj:
|
|
114
|
+
# Convert the nested types
|
|
115
|
+
case list():
|
|
116
|
+
return [self(entry) for entry in obj]
|
|
117
|
+
case set():
|
|
118
|
+
return (self(entry) for entry in obj)
|
|
119
|
+
case dict():
|
|
120
|
+
return {k: self(v) for k, v in obj.items()}
|
|
121
|
+
|
|
122
|
+
# Nothing to do here, since these are the accepted types
|
|
123
|
+
case int() | float() | bool() | str() | None:
|
|
124
|
+
return obj
|
|
125
|
+
|
|
126
|
+
# Type was not registered and is not one of the standards one
|
|
127
|
+
case _:
|
|
128
|
+
raise TypeError(
|
|
129
|
+
f"Object {obj} of type {type(obj)} can not be converted"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def __str__(self) -> str:
|
|
133
|
+
"""String representation of the Converter class."""
|
|
134
|
+
string = "Converter with custom object (objects will be converted from top to bottom):"
|
|
135
|
+
for k, v in self._custom_converters.items():
|
|
136
|
+
string += f"\n\t{k}\t: {v}"
|
|
137
|
+
|
|
138
|
+
return string
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 FourCIPP Authors
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
"""Dict utils."""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
from loguru import logger
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compare_nested_dicts_or_lists(
|
|
32
|
+
obj: Any,
|
|
33
|
+
reference_obj: Any,
|
|
34
|
+
allow_int_vs_float_comparison: bool = False,
|
|
35
|
+
rtol: float = 1.0e-5,
|
|
36
|
+
atol: float = 1.0e-8,
|
|
37
|
+
equal_nan: bool = False,
|
|
38
|
+
custom_compare: Callable | None = None,
|
|
39
|
+
) -> bool:
|
|
40
|
+
"""Recursively compare two nested dictionaries or lists.
|
|
41
|
+
|
|
42
|
+
In case objects are not within the provided tolerance an `AssertionError` is raised.
|
|
43
|
+
|
|
44
|
+
To compare custom python objects, a `custom_compare` callable can be provided which:
|
|
45
|
+
- Returns nothing/`None` if the objects where not compared within `custom_compare`
|
|
46
|
+
- Returns `True` if the objects are seen as equal
|
|
47
|
+
- Raises AssertionError if the objects are not equal
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
obj: Object for comparison
|
|
51
|
+
reference_obj: Reference object
|
|
52
|
+
allow_int_vs_float_comparison: Allow a tolerance based comparison between int and
|
|
53
|
+
float
|
|
54
|
+
rtol: The relative tolerance parameter for numpy.isclose
|
|
55
|
+
atol: The absolute tolerance parameter for numpy.isclose
|
|
56
|
+
equal_nan: Whether to compare NaN's as equal for numpy.isclose
|
|
57
|
+
custom_compare: Callable to compare objects within this nested framework
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if the dictionaries are equal
|
|
61
|
+
"""
|
|
62
|
+
# Compare non-standard python objects
|
|
63
|
+
if custom_compare is not None:
|
|
64
|
+
# Check if result is not None
|
|
65
|
+
if result := custom_compare(obj, reference_obj) is not None:
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
# Ensures the types are the same
|
|
69
|
+
if not type(obj) is type(reference_obj):
|
|
70
|
+
if (
|
|
71
|
+
not allow_int_vs_float_comparison # Except floats can be ints
|
|
72
|
+
or not isinstance(obj, (float, int))
|
|
73
|
+
or not isinstance(reference_obj, (float, int))
|
|
74
|
+
):
|
|
75
|
+
raise AssertionError(
|
|
76
|
+
f"Object is of type {type(obj)}, but the reference is of type {type(reference_obj)}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Objects are numerics
|
|
80
|
+
if isinstance(obj, (float, int)):
|
|
81
|
+
if not np.isclose(obj, reference_obj, rtol, atol, equal_nan):
|
|
82
|
+
raise AssertionError(
|
|
83
|
+
f"The numerics are not close:\n\nobj = {obj}\n\nreference_obj = {reference_obj}"
|
|
84
|
+
)
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# Object are dicts
|
|
88
|
+
if isinstance(obj, dict):
|
|
89
|
+
# ^ is the symmetric difference operator, i.e. union of the sets without the intersection
|
|
90
|
+
if missing_keys := set(obj.keys()) ^ set(reference_obj.keys()):
|
|
91
|
+
raise AssertionError(
|
|
92
|
+
f"The following keys could not be found in both dicts {missing_keys}:"
|
|
93
|
+
f"\nobj: {obj}\n\nreference_obj:{reference_obj}"
|
|
94
|
+
)
|
|
95
|
+
for key in obj:
|
|
96
|
+
compare_nested_dicts_or_lists(
|
|
97
|
+
obj[key],
|
|
98
|
+
reference_obj[key],
|
|
99
|
+
allow_int_vs_float_comparison,
|
|
100
|
+
rtol,
|
|
101
|
+
atol,
|
|
102
|
+
equal_nan,
|
|
103
|
+
custom_compare,
|
|
104
|
+
)
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
# Objects are lists
|
|
108
|
+
if isinstance(obj, list):
|
|
109
|
+
if len(obj) != len(reference_obj):
|
|
110
|
+
raise AssertionError(
|
|
111
|
+
f"The list lengths differ (got {len(obj)} and {len(reference_obj)}).\nobj "
|
|
112
|
+
f"{obj}\n\nreference_obj:{reference_obj}"
|
|
113
|
+
)
|
|
114
|
+
for obj_item, reference_obj_item in zip(obj, reference_obj):
|
|
115
|
+
compare_nested_dicts_or_lists(
|
|
116
|
+
obj_item,
|
|
117
|
+
reference_obj_item,
|
|
118
|
+
allow_int_vs_float_comparison,
|
|
119
|
+
rtol,
|
|
120
|
+
atol,
|
|
121
|
+
equal_nan,
|
|
122
|
+
custom_compare,
|
|
123
|
+
)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
# Otherwise compare the objects directly
|
|
127
|
+
if obj != reference_obj:
|
|
128
|
+
raise AssertionError(
|
|
129
|
+
f"The objects are not equal:\n\nobj = {obj}\n\nreference_obj = {reference_obj}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_dict(
|
|
136
|
+
nested_dict: dict | list, keys: Sequence, optional: bool = True
|
|
137
|
+
) -> Iterator[dict]:
|
|
138
|
+
"""Return dict entry within a nested dict by keys.
|
|
139
|
+
|
|
140
|
+
In case a list is encountered, this function yields over every entry.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
nested_dict: dict to iterate. Due to recursiveness, this can also be a list
|
|
144
|
+
keys: List of keys to access
|
|
145
|
+
optional: If the entry is part of a collection that does no exist as it is optional
|
|
146
|
+
|
|
147
|
+
Yields:
|
|
148
|
+
Desired data
|
|
149
|
+
"""
|
|
150
|
+
# Start with the original data
|
|
151
|
+
sub_data = nested_dict
|
|
152
|
+
sub_keys = list(keys)
|
|
153
|
+
|
|
154
|
+
# Get from dict
|
|
155
|
+
if isinstance(nested_dict, dict):
|
|
156
|
+
# Loop over all keys
|
|
157
|
+
for key in keys:
|
|
158
|
+
# Jump into the dict
|
|
159
|
+
if isinstance(sub_data, dict):
|
|
160
|
+
if key in sub_data:
|
|
161
|
+
# Jump into the entry key
|
|
162
|
+
sub_data = sub_data[key]
|
|
163
|
+
sub_keys.pop(0)
|
|
164
|
+
else:
|
|
165
|
+
# Unknown key
|
|
166
|
+
if optional:
|
|
167
|
+
logger.debug(f"Entry {keys} not found and is set as optional")
|
|
168
|
+
return
|
|
169
|
+
else:
|
|
170
|
+
raise KeyError(
|
|
171
|
+
f"Key '{key}' not found in dictionary {sub_data}."
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
# Jump into the sub_data with the remaining keys
|
|
175
|
+
yield from _get_dict(sub_data, sub_keys)
|
|
176
|
+
|
|
177
|
+
# Exit function afterwards
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Check the last entry type
|
|
181
|
+
# dict: nothing to do
|
|
182
|
+
if isinstance(sub_data, dict):
|
|
183
|
+
yield sub_data
|
|
184
|
+
# List: jump in an do it all over
|
|
185
|
+
elif isinstance(sub_data, list):
|
|
186
|
+
# Last key is a list of objects
|
|
187
|
+
if not sub_keys:
|
|
188
|
+
for item in sub_data:
|
|
189
|
+
# Only dicts are allowed
|
|
190
|
+
if isinstance(item, dict):
|
|
191
|
+
yield item
|
|
192
|
+
else:
|
|
193
|
+
raise TypeError(f"Expected type dict, got {type(item)}")
|
|
194
|
+
# More nested keys
|
|
195
|
+
else:
|
|
196
|
+
for item in sub_data:
|
|
197
|
+
yield from _get_dict(item, sub_keys)
|
|
198
|
+
# Unsupported type
|
|
199
|
+
else:
|
|
200
|
+
raise TypeError(
|
|
201
|
+
f"The current data {sub_data} for type {type(sub_data)} for keys {keys} is neither a dict nor a list."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Exit function afterwards
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _split_off_last_key(
|
|
209
|
+
nested_dict: dict,
|
|
210
|
+
keys: Sequence,
|
|
211
|
+
optional: bool = True,
|
|
212
|
+
yield_dict_if_missing: bool = False,
|
|
213
|
+
) -> Iterator[Any]:
|
|
214
|
+
"""Utility to return the last key and its parent entry.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
nested_dict: dict to iterate. Due to recursiveness, this can also be a list
|
|
218
|
+
keys: List of keys to access
|
|
219
|
+
optional: If the entry is part of a collection that does no exist as it is optional
|
|
220
|
+
yield_dict_if_missing: Return parent entry even if the entry is not provided
|
|
221
|
+
|
|
222
|
+
Yields:
|
|
223
|
+
Parent entry
|
|
224
|
+
"""
|
|
225
|
+
last_key = keys[-1]
|
|
226
|
+
|
|
227
|
+
for entry in _get_dict(nested_dict, keys[:-1], optional):
|
|
228
|
+
# Key has to be provided
|
|
229
|
+
if last_key not in entry:
|
|
230
|
+
if optional:
|
|
231
|
+
logger.debug(f"Entry {keys} not found and was set to optional.")
|
|
232
|
+
# Still return the dict
|
|
233
|
+
if yield_dict_if_missing:
|
|
234
|
+
yield entry, last_key
|
|
235
|
+
# Ignore
|
|
236
|
+
else:
|
|
237
|
+
continue
|
|
238
|
+
else:
|
|
239
|
+
raise KeyError(f"Entry {last_key} not in {entry}")
|
|
240
|
+
|
|
241
|
+
yield entry, last_key
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_entry(
|
|
245
|
+
nested_dict: dict,
|
|
246
|
+
keys: Sequence,
|
|
247
|
+
optional: bool = True,
|
|
248
|
+
) -> Iterator[Any]:
|
|
249
|
+
"""Get entry by a list of keys.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
nested_dict: Nested data dict
|
|
253
|
+
keys: List of keys to get the entry
|
|
254
|
+
optional: If the entry is part of a collection that does no exist as it is optional
|
|
255
|
+
|
|
256
|
+
Yields:
|
|
257
|
+
Entry
|
|
258
|
+
"""
|
|
259
|
+
for entry, last_key in _split_off_last_key(nested_dict, keys, optional):
|
|
260
|
+
yield entry[last_key]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def remove(
|
|
264
|
+
nested_dict: dict,
|
|
265
|
+
keys: Sequence,
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Remove entry.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
nested_dict: Nested data dict
|
|
271
|
+
keys: List of keys to the entry
|
|
272
|
+
optional: If the entry is part of a collection that does no exist as it is optional
|
|
273
|
+
"""
|
|
274
|
+
for entry, last_key in _split_off_last_key(nested_dict, keys):
|
|
275
|
+
entry.pop(last_key)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def replace_value(
|
|
279
|
+
nested_dict: dict,
|
|
280
|
+
keys: Sequence,
|
|
281
|
+
new_value: Any,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Replace value.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
nested_dict: Nested data dict
|
|
287
|
+
keys: List of keys to the entry
|
|
288
|
+
new_value: New value to set
|
|
289
|
+
"""
|
|
290
|
+
for entry, last_key in _split_off_last_key(nested_dict, keys):
|
|
291
|
+
logger.debug(f"Replacing {last_key}: from {entry[last_key]} to {new_value}")
|
|
292
|
+
entry[last_key] = new_value
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def make_default_explicit(
|
|
296
|
+
nested_dict: dict,
|
|
297
|
+
keys: Sequence,
|
|
298
|
+
default_value: Any,
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Make default explicit, i.e. set the value in the input.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
nested_dict: Nested data dict
|
|
304
|
+
keys: List of keys to the entry
|
|
305
|
+
default_value: Default value to set
|
|
306
|
+
"""
|
|
307
|
+
for entry, last_key in _split_off_last_key(
|
|
308
|
+
nested_dict, keys, yield_dict_if_missing=True
|
|
309
|
+
):
|
|
310
|
+
if last_key not in entry:
|
|
311
|
+
entry[last_key] = default_value
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def make_default_implicit(
|
|
315
|
+
nested_dict: dict,
|
|
316
|
+
keys: Sequence,
|
|
317
|
+
default_value: Any,
|
|
318
|
+
) -> None:
|
|
319
|
+
"""Make default implicit, i.e., removed it if set with the default value in
|
|
320
|
+
the input.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
nested_dict: Nested data dict
|
|
324
|
+
keys: List of keys to the entry
|
|
325
|
+
default_value: Default value to set
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
for entry, last_key in _split_off_last_key(nested_dict, keys):
|
|
329
|
+
if entry[last_key] == default_value:
|
|
330
|
+
entry.pop(last_key)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def change_default(
|
|
334
|
+
nested_dict: dict,
|
|
335
|
+
keys: Sequence,
|
|
336
|
+
old_default: Any,
|
|
337
|
+
new_default: Any,
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Change default value.
|
|
340
|
+
|
|
341
|
+
If default value is not provided the old default is set. Entries where the value equals the new default value is removed.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
nested_dict: Nested data dict
|
|
345
|
+
keys: List of keys to the entry
|
|
346
|
+
old_default: Old default value to set
|
|
347
|
+
new_default: New default value
|
|
348
|
+
"""
|
|
349
|
+
for entry, last_key in _split_off_last_key(
|
|
350
|
+
nested_dict, keys, yield_dict_if_missing=True
|
|
351
|
+
):
|
|
352
|
+
# Optional entry is missing
|
|
353
|
+
if last_key not in entry:
|
|
354
|
+
entry[last_key] = old_default
|
|
355
|
+
# If entry is set with the new default, remove it
|
|
356
|
+
else:
|
|
357
|
+
if entry[last_key] == new_default:
|
|
358
|
+
entry.pop(last_key)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def rename_parameter(
|
|
362
|
+
nested_dict: dict,
|
|
363
|
+
keys: Sequence,
|
|
364
|
+
new_name: str,
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Rename parameter.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
nested_dict: Nested data dict
|
|
370
|
+
keys: List of keys to the entry
|
|
371
|
+
new_name: New name of the parameter
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
for entry, last_key in _split_off_last_key(nested_dict, keys):
|
|
375
|
+
entry[new_name] = entry.pop(last_key)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def sort_by_key_order(data: dict, key_order: list[str]) -> dict:
|
|
379
|
+
"""Sort a dictionary by a specific key order.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
data: Dictionary to sort.
|
|
383
|
+
key_order: List of keys in the desired order.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Sorted dictionary.
|
|
387
|
+
"""
|
|
388
|
+
if set(key_order) != set(data.keys()):
|
|
389
|
+
raise ValueError("'key_order' must include all keys in the dictionary!")
|
|
390
|
+
|
|
391
|
+
return {key: data[key] for key in key_order if key in data}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def sort_alphabetically(
|
|
395
|
+
data: dict,
|
|
396
|
+
) -> dict:
|
|
397
|
+
"""Sort a dictionary alphabetically.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
data: Dictionary to sort.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Sorted dictionary.
|
|
404
|
+
"""
|
|
405
|
+
return sort_by_key_order(
|
|
406
|
+
data, sorted(data.keys(), key=lambda s: (s.lower(), 0 if s.islower() else 1))
|
|
407
|
+
)
|