Coverage for tests / tests_config / test_generic_save_load.py: 100%
196 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-09 16:40 +0100
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-09 16:40 +0100
1# SPDX-FileCopyrightText: Copyright INRIA
2#
3# SPDX-License-Identifier: LGPL-3.0-only
4#
5# Copyright INRIA
6#
7# This file is part of PhysioBlocks, a library mostly developed by the
8# [Ananke project-team](https://team.inria.fr/ananke) at INRIA.
9#
10# Authors:
11# - Colin Drieu
12# - Dominique Chapelle
13# - François Kimmig
14# - Philippe Moireau
15#
16# PhysioBlocks is free software: you can redistribute it and/or modify it under the
17# terms of the GNU Lesser General Public License as published by the Free Software
18# Foundation, version 3 of the License.
19#
20# PhysioBlocks is distributed in the hope that it will be useful, but WITHOUT ANY
21# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
22# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
23#
24# You should have received a copy of the GNU Lesser General Public License along with
25# PhysioBlocks. If not, see <https://www.gnu.org/licenses/>.
27import sys
28from dataclasses import dataclass
29from unittest.mock import patch
31import numpy as np
32import pytest
33import regex as re
35from physioblocks.computing.quantities import Quantity
36from physioblocks.configuration.base import Configuration, ConfigurationError
37from physioblocks.configuration.functions import load, save
39DATA_CLASS_OBJECT_ITEM_ID = "data_class_obj"
40CONFIGURATION_LABEL = "data_class_obj"
41OBJ_KEY = "obj"
42PARAM_A_KEY = "a"
43PARAM_B_KEY = "b"
44WRONG_KEY = "wrong"
45PARAM_A_VALUE = "param_a"
46PARAM_B_VALUE = 0.1
47WRONG_VALUE = "wrong"
48SCALAR_REF = 0.0
49VECTOR_REF = [0.1, 0.2, 0.3]
52@dataclass
53class DataClassObj:
54 a: str
55 b: float
57 def __eq__(self, value):
58 return (
59 isinstance(value, DataClassObj) and value.a == self.a and value.b == self.b
60 )
63@dataclass
64class UnregisteredClassObj:
65 pass
68@pytest.fixture
69def ref_base_object():
70 return DataClassObj(PARAM_A_VALUE, PARAM_B_VALUE)
73@pytest.fixture
74def ref_base_object_config():
75 return Configuration(
76 DATA_CLASS_OBJECT_ITEM_ID,
77 {PARAM_A_KEY: PARAM_A_VALUE, PARAM_B_KEY: PARAM_B_VALUE},
78 )
81@pytest.fixture
82def ref_base_object_config_exception(ref_base_object_config):
83 ref_base_object_config[WRONG_KEY] = WRONG_VALUE
84 return ref_base_object_config
87@pytest.fixture
88def ref_dict(ref_base_object: DataClassObj):
89 return {OBJ_KEY: ref_base_object}
92@pytest.fixture
93def ref_dict_config(ref_base_object_config: Configuration) -> Configuration:
94 return {
95 OBJ_KEY: ref_base_object_config,
96 }
99@pytest.fixture
100def ref_list(ref_base_object: DataClassObj):
101 return [ref_base_object]
104@pytest.fixture
105def ref_list_config(ref_base_object_config: Configuration) -> Configuration:
106 return [ref_base_object_config]
109@pytest.fixture
110def ref_unsorted_configuration(ref_base_object_config):
111 return {
112 "a": "b",
113 "h": {
114 "h.a": "a",
115 "h.b": "b",
116 "h.d": ["h.a", "h.b", "h.c"],
117 "h.e": {"h.e.a": "a", "h.e.b": "h.d"},
118 "h.c": ref_base_object_config,
119 },
120 "b": 0.1,
121 "c": "h.a",
122 "d": "h.b",
123 "e": "h.c",
124 "f": "h.d",
125 "g": "h.e",
126 }
129@pytest.fixture
130def self_referencing_configuration(ref_base_object_config):
131 return {"a": "b", "b": "c", "c": "a"}
134@pytest.fixture
135def deep_self_referencing_configuration(ref_base_object_config):
136 return {"a": "b", "b": "c", "c": {"c.a": ["a", "a1", "a2"]}}
139@pytest.fixture
140def ref_sorted_obj(ref_base_object):
141 return {
142 "b": 0.1,
143 "a": 0.1,
144 "h": {
145 "h.a": 0.1,
146 "h.b": 0.1,
147 "h.c": ref_base_object,
148 "h.e": {"h.e.a": 0.1, "h.e.b": [0.1, 0.1, ref_base_object]},
149 "h.d": [0.1, 0.1, ref_base_object],
150 },
151 "c": 0.1,
152 "d": 0.1,
153 "e": ref_base_object,
154 "f": [0.1, 0.1, ref_base_object],
155 "g": {"h.e.a": 0.1, "h.e.b": [0.1, 0.1, ref_base_object]},
156 }
159@pytest.fixture
160def scalar_qty() -> Quantity:
161 return Quantity(0.0)
164@pytest.fixture
165def vector_qty() -> Quantity:
166 return Quantity(VECTOR_REF)
169@patch(
170 "physioblocks.registers.type_register.__type_register",
171 new={
172 DATA_CLASS_OBJECT_ITEM_ID: DataClassObj,
173 DataClassObj: DATA_CLASS_OBJECT_ITEM_ID,
174 },
175)
176class TestLoad:
177 def test_load_base_types(self):
178 # load float, int, bool values:
179 float_obj = load(1.3)
180 assert float_obj == pytest.approx(1.3)
181 float_obj = load("1.7", configuration_type=float)
182 assert float_obj == pytest.approx(1.7)
184 loaded_int_obj = load(3)
185 assert loaded_int_obj == 3
186 loaded_int_obj = load("1", configuration_type=int)
187 assert loaded_int_obj == 1
188 loaded_int_obj = load(False, configuration_type=int)
189 assert loaded_int_obj == 0
191 loaded_bool_obj = load(True)
192 assert loaded_bool_obj is True
193 loaded_bool_obj = load("False", configuration_type=bool)
194 assert loaded_bool_obj is False
195 loaded_bool_obj = load(1, configuration_type=bool)
196 assert loaded_bool_obj is True
198 def test_load_from_reference(self):
199 references = {
200 "a": "0.1",
201 "b": 0,
202 "c": "3",
203 }
204 assert load(
205 "a", configuration_type=float, configuration_references=references
206 ) == pytest.approx(0.1)
207 assert (
208 load("b", configuration_type=bool, configuration_references=references)
209 is False
210 )
211 assert load(
212 "c", configuration_type=int, configuration_references=references
213 ) == pytest.approx(3)
215 def test_load_base_object(
216 self, ref_base_object_config: Configuration, ref_base_object: DataClassObj
217 ):
218 # use base load
219 base_object = load(ref_base_object_config)
220 assert base_object == ref_base_object
222 def test_load_base_object_list_args(self, ref_base_object: DataClassObj):
223 base_object = load(
224 [PARAM_A_VALUE, PARAM_B_VALUE], configuration_type=DataClassObj
225 )
226 assert base_object == ref_base_object
228 def test_load_base_object_dict_args(
229 self, ref_base_object_config: Configuration, ref_base_object: DataClassObj
230 ):
231 base_object = load(
232 {PARAM_B_KEY: PARAM_B_VALUE, PARAM_A_KEY: PARAM_A_VALUE},
233 configuration_type=DataClassObj,
234 )
235 assert base_object == ref_base_object
237 def test_load_base_object_with_initialized_object(
238 self, ref_base_object_config: Configuration, ref_base_object: DataClassObj
239 ):
240 # use base load with an object to configure
241 configuration_object = DataClassObj("", 0.0)
242 base_object = load(
243 ref_base_object_config, configuration_object=configuration_object
244 )
245 assert base_object == ref_base_object
246 assert configuration_object is base_object
248 def test_load_base_object_arg_dict_with_initialized_object(
249 self, ref_base_object: DataClassObj
250 ):
251 # use base load with dict of arguments and an object to configure
252 configuration_object = DataClassObj("", 0.0)
253 base_object = load(
254 {PARAM_B_KEY: PARAM_B_VALUE, PARAM_A_KEY: PARAM_A_VALUE},
255 configuration_type=DataClassObj,
256 configuration_object=configuration_object,
257 )
258 assert base_object == ref_base_object
259 assert configuration_object is base_object
261 def test_load_base_object_arg_list_with_initialized_object_error(self):
262 # use base load with dict of arguments and an object to configure
263 arg_list = [PARAM_A_VALUE, PARAM_B_VALUE]
264 configuration_object = DataClassObj("", 0.0)
265 err_msg = str.format(
266 "Can not set arguments {0} to existing object {1}. Missing attribute keys.",
267 arg_list,
268 configuration_object,
269 )
270 with pytest.raises(ConfigurationError, match=re.escape(err_msg)):
271 load(
272 arg_list,
273 configuration_type=DataClassObj,
274 configuration_object=configuration_object,
275 )
277 def test_load_base_object_from_reference(
278 self, ref_base_object_config: Configuration, ref_base_object: DataClassObj
279 ):
280 references = {DATA_CLASS_OBJECT_ITEM_ID: ref_base_object}
281 base_object = load(ref_base_object_config, configuration_references=references)
282 assert base_object == ref_base_object
284 def test_load_base_object_exception(
285 self, ref_base_object_config_exception: Configuration
286 ):
287 err_msg = str.format("Error while initialising key {0}", WRONG_KEY)
288 with pytest.raises(ConfigurationError, match=err_msg):
289 load(ref_base_object_config_exception, configuration_key=WRONG_KEY)
291 err_msg = str.format(
292 "Type {0} can not be loaded as a configuration.",
293 UnregisteredClassObj.__name__,
294 )
295 unregistered_config = UnregisteredClassObj()
296 with pytest.raises(TypeError, match=err_msg):
297 load(unregistered_config)
299 def test_load_dict(self, ref_dict: dict, ref_dict_config: Configuration):
300 loaded_dict = load(ref_dict_config)
301 assert loaded_dict == ref_dict
303 configured_dict = {"a": 0.1, "b": 0.2}
304 updated_dict = configured_dict.copy()
305 updated_dict.update(ref_dict)
306 loaded_dict = load(ref_dict_config, configuration_object=configured_dict)
307 assert loaded_dict == updated_dict
308 assert sys.getrefcount(loaded_dict) == sys.getrefcount(configured_dict)
310 def test_load_list(
311 self,
312 ref_list: list,
313 ref_list_config: Configuration,
314 ref_base_object: DataClassObj,
315 ref_base_object_config: Configuration,
316 ):
317 loaded_list = load(ref_list_config)
318 assert loaded_list == ref_list
320 configured_list = [ref_base_object]
321 extended_list_to_load = [
322 ref_base_object_config,
323 ref_base_object_config,
324 0.1,
325 0.2,
326 ]
327 extended_list_ref = [ref_base_object, ref_base_object, 0.1, 0.2]
328 loaded_list = load(extended_list_to_load, configuration_object=configured_list)
329 assert loaded_list == extended_list_ref
330 assert sys.getrefcount(loaded_list) == sys.getrefcount(configured_list)
332 def test_load_with_sort(
333 self, ref_unsorted_configuration: Configuration, ref_sorted_obj: Configuration
334 ):
335 loaded_obj = load(
336 ref_unsorted_configuration,
337 configuration_sort=True,
338 )
339 assert loaded_obj == ref_sorted_obj
341 def test_load_with_sort_self_referencing_config(
342 self, self_referencing_configuration
343 ):
344 err_msg = str.format(
345 "Item {0} is referencing itself: {1}", "a", ["a", "b", "c"]
346 )
347 with pytest.raises(ConfigurationError, match=re.escape(err_msg)):
348 load(
349 self_referencing_configuration,
350 configuration_sort=True,
351 )
353 def test_load_with_sort_deep_self_referencing_config(
354 self, deep_self_referencing_configuration
355 ):
356 err_msg = str.format(
357 "Item {0} is referencing itself: {1}", "a", ["a", "b", "c"]
358 )
359 with pytest.raises(ConfigurationError, match=re.escape(err_msg)):
360 load(
361 deep_self_referencing_configuration,
362 configuration_sort=True,
363 )
366@patch(
367 "physioblocks.registers.type_register.__type_register",
368 new={
369 DATA_CLASS_OBJECT_ITEM_ID: DataClassObj,
370 DataClassObj: DATA_CLASS_OBJECT_ITEM_ID,
371 },
372)
373class TestSave:
374 def test_save_base_object(
375 self, ref_base_object_config: Configuration, ref_base_object: DataClassObj
376 ):
377 config = save(ref_base_object)
378 assert config == ref_base_object_config
380 def mock_save_function(obj, *args, **kwargs):
381 return ref_base_object_config
383 with patch(
384 "physioblocks.registers.save_function_register.__save_functions_register",
385 new={DataClassObj: mock_save_function, mock_save_function: DataClassObj},
386 ):
387 config = save(ref_base_object)
388 assert config == ref_base_object_config
390 def test_save_base_object_with_reference(self, ref_base_object: DataClassObj):
391 config_key = "config_ref"
392 config = save(
393 ref_base_object, configuration_references={config_key: ref_base_object}
394 )
395 assert config == config_key
397 def test_save_base_object_configuration_error(self):
398 obj = UnregisteredClassObj()
399 with pytest.raises(
400 ConfigurationError,
401 match=re.escape(str.format("Can not configure object {0}.", obj)),
402 ):
403 save(obj)
405 def test_save_dict(self, ref_dict: dict, ref_dict_config: Configuration):
406 dict_config = save(ref_dict)
407 assert dict_config == ref_dict_config
409 def test_save_list(self, ref_list: list, ref_list_config: Configuration):
410 list_config = save(ref_list)
411 assert list_config == ref_list_config
413 def test_save_quantities(self, scalar_qty, vector_qty):
414 assert save(scalar_qty) == SCALAR_REF
415 assert save(vector_qty) == VECTOR_REF
417 def test_save_bool(self):
418 assert save(True) == "True"
419 assert save(False) == "False"
421 def test_save_base_types_with_reference(self):
422 references = {"a": 0.1, "b": "str", "c": 3}
423 assert save(0.1, configuration_references=references) == "a"
424 assert save("str", configuration_references=references) == "b"
425 assert save(3, configuration_references=references) == "c"
427 def test_save_ndarray(self):
428 assert save(np.array(VECTOR_REF)) == VECTOR_REF