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

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/>. 

26 

27import sys 

28from dataclasses import dataclass 

29from unittest.mock import patch 

30 

31import numpy as np 

32import pytest 

33import regex as re 

34 

35from physioblocks.computing.quantities import Quantity 

36from physioblocks.configuration.base import Configuration, ConfigurationError 

37from physioblocks.configuration.functions import load, save 

38 

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] 

50 

51 

52@dataclass 

53class DataClassObj: 

54 a: str 

55 b: float 

56 

57 def __eq__(self, value): 

58 return ( 

59 isinstance(value, DataClassObj) and value.a == self.a and value.b == self.b 

60 ) 

61 

62 

63@dataclass 

64class UnregisteredClassObj: 

65 pass 

66 

67 

68@pytest.fixture 

69def ref_base_object(): 

70 return DataClassObj(PARAM_A_VALUE, PARAM_B_VALUE) 

71 

72 

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 ) 

79 

80 

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 

85 

86 

87@pytest.fixture 

88def ref_dict(ref_base_object: DataClassObj): 

89 return {OBJ_KEY: ref_base_object} 

90 

91 

92@pytest.fixture 

93def ref_dict_config(ref_base_object_config: Configuration) -> Configuration: 

94 return { 

95 OBJ_KEY: ref_base_object_config, 

96 } 

97 

98 

99@pytest.fixture 

100def ref_list(ref_base_object: DataClassObj): 

101 return [ref_base_object] 

102 

103 

104@pytest.fixture 

105def ref_list_config(ref_base_object_config: Configuration) -> Configuration: 

106 return [ref_base_object_config] 

107 

108 

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 } 

127 

128 

129@pytest.fixture 

130def self_referencing_configuration(ref_base_object_config): 

131 return {"a": "b", "b": "c", "c": "a"} 

132 

133 

134@pytest.fixture 

135def deep_self_referencing_configuration(ref_base_object_config): 

136 return {"a": "b", "b": "c", "c": {"c.a": ["a", "a1", "a2"]}} 

137 

138 

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 } 

157 

158 

159@pytest.fixture 

160def scalar_qty() -> Quantity: 

161 return Quantity(0.0) 

162 

163 

164@pytest.fixture 

165def vector_qty() -> Quantity: 

166 return Quantity(VECTOR_REF) 

167 

168 

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) 

183 

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 

190 

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 

197 

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) 

214 

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 

221 

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 

227 

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 

236 

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 

247 

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 

260 

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 ) 

276 

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 

283 

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) 

290 

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) 

298 

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 

302 

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) 

309 

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 

319 

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) 

331 

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 

340 

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 ) 

352 

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 ) 

364 

365 

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 

379 

380 def mock_save_function(obj, *args, **kwargs): 

381 return ref_base_object_config 

382 

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 

389 

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 

396 

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) 

404 

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 

408 

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 

412 

413 def test_save_quantities(self, scalar_qty, vector_qty): 

414 assert save(scalar_qty) == SCALAR_REF 

415 assert save(vector_qty) == VECTOR_REF 

416 

417 def test_save_bool(self): 

418 assert save(True) == "True" 

419 assert save(False) == "False" 

420 

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" 

426 

427 def test_save_ndarray(self): 

428 assert save(np.array(VECTOR_REF)) == VECTOR_REF