Coverage for tests/test_Config.py: 16%

359 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-06 09:49 +0000

1# This file is part of pex_config. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28import io 

29import itertools 

30import os 

31import pickle 

32import re 

33import tempfile 

34import unittest 

35 

36try: 

37 import yaml 

38except ImportError: 

39 yaml = None 

40 

41import lsst.pex.config as pexConfig 

42 

43# Some tests depend on daf_base. 

44# Skip them if it is not found. 

45try: 

46 import lsst.daf.base as dafBase 

47except ImportError: 

48 dafBase = None 

49 

50GLOBAL_REGISTRY = {} 

51 

52 

53class Simple(pexConfig.Config): 

54 """A simple config used for testing.""" 

55 

56 i = pexConfig.Field("integer test", int, optional=True) 

57 f = pexConfig.Field("float test", float, default=3.0) 

58 b = pexConfig.Field("boolean test", bool, default=False, optional=False) 

59 c = pexConfig.ChoiceField( 

60 "choice test", str, default="Hello", allowed={"Hello": "First choice", "World": "second choice"} 

61 ) 

62 r = pexConfig.RangeField("Range test", float, default=3.0, optional=False, min=3.0, inclusiveMin=True) 

63 ll = pexConfig.ListField( 63 ↛ exitline 63 didn't jump to the function exit

64 "list test", int, default=[1, 2, 3], maxLength=5, itemCheck=lambda x: x is not None and x > 0 

65 ) 

66 d = pexConfig.DictField( 66 ↛ exitline 66 didn't jump to the function exit

67 "dict test", str, str, default={"key": "value"}, itemCheck=lambda x: x.startswith("v") 

68 ) 

69 n = pexConfig.Field("nan test", float, default=float("NAN")) 

70 

71 

72GLOBAL_REGISTRY["AAA"] = Simple 

73 

74 

75class InnerConfig(pexConfig.Config): 

76 """Inner config used for testing.""" 

77 

78 f = pexConfig.Field("Inner.f", float, default=0.0, check=lambda x: x >= 0, optional=False) 78 ↛ exitline 78 didn't run the lambda on line 78

79 

80 

81GLOBAL_REGISTRY["BBB"] = InnerConfig 

82 

83 

84class OuterConfig(InnerConfig, pexConfig.Config): 

85 """Outer config used for testing.""" 

86 

87 i = pexConfig.ConfigField("Outer.i", InnerConfig) 

88 

89 def __init__(self): 

90 pexConfig.Config.__init__(self) 

91 self.i.f = 5.0 

92 

93 def validate(self): 

94 pexConfig.Config.validate(self) 

95 if self.i.f < 5: 

96 raise ValueError("validation failed, outer.i.f must be greater than 5") 

97 

98 

99class Complex(pexConfig.Config): 

100 """A complex config for testing.""" 

101 

102 c = pexConfig.ConfigField("an inner config", InnerConfig) 

103 r = pexConfig.ConfigChoiceField( 

104 "a registry field", typemap=GLOBAL_REGISTRY, default="AAA", optional=False 

105 ) 

106 p = pexConfig.ConfigChoiceField("another registry", typemap=GLOBAL_REGISTRY, default="BBB", optional=True) 

107 

108 

109class Deprecation(pexConfig.Config): 

110 """A test config with a deprecated field.""" 

111 

112 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!") 

113 

114 

115class ConfigTest(unittest.TestCase): 

116 """Tests of basic Config functionality.""" 

117 

118 def setUp(self): 

119 self.simple = Simple() 

120 self.inner = InnerConfig() 

121 self.outer = OuterConfig() 

122 self.comp = Complex() 

123 self.deprecation = Deprecation() 

124 

125 def tearDown(self): 

126 del self.simple 

127 del self.inner 

128 del self.outer 

129 del self.comp 

130 

131 def testFieldTypeAnnotationRuntime(self): 

132 # test parsing type annotation for runtime dtype 

133 testField = pexConfig.Field[str](doc="test") 

134 self.assertEqual(testField.dtype, str) 

135 

136 # verify that forward references work correctly 

137 testField = pexConfig.Field["float"](doc="test") 

138 self.assertEqual(testField.dtype, float) 

139 

140 # verify that Field rejects multiple types 

141 with self.assertRaises(ValueError): 

142 pexConfig.Field[str, int](doc="test") # type: ignore 

143 

144 # verify that Field raises in conflict with dtype: 

145 with self.assertRaises(ValueError): 

146 pexConfig.Field[str](doc="test", dtype=int) 

147 

148 # verify that Field does not raise if dtype agrees 

149 testField = pexConfig.Field[int](doc="test", dtype=int) 

150 self.assertEqual(testField.dtype, int) 

151 

152 def testInit(self): 

153 self.assertIsNone(self.simple.i) 

154 self.assertEqual(self.simple.f, 3.0) 

155 self.assertFalse(self.simple.b) 

156 self.assertEqual(self.simple.c, "Hello") 

157 self.assertEqual(list(self.simple.ll), [1, 2, 3]) 

158 self.assertEqual(self.simple.d["key"], "value") 

159 self.assertEqual(self.inner.f, 0.0) 

160 self.assertEqual(self.deprecation.old, 10) 

161 

162 self.assertEqual(self.deprecation._fields["old"].doc, "Something. Deprecated: not used!") 

163 

164 self.assertEqual(self.outer.i.f, 5.0) 

165 self.assertEqual(self.outer.f, 0.0) 

166 

167 self.assertEqual(self.comp.c.f, 0.0) 

168 self.assertEqual(self.comp.r.name, "AAA") 

169 self.assertEqual(self.comp.r.active.f, 3.0) 

170 self.assertEqual(self.comp.r["BBB"].f, 0.0) 

171 

172 def testDeprecationWarning(self): 

173 """Test that a deprecated field emits a warning when it is set.""" 

174 with self.assertWarns(FutureWarning) as w: 

175 self.deprecation.old = 5 

176 self.assertEqual(self.deprecation.old, 5) 

177 

178 self.assertIn(self.deprecation._fields["old"].deprecated, str(w.warnings[-1].message)) 

179 

180 def testDeprecationOutput(self): 

181 """Test that a deprecated field is not written out unless it is set.""" 

182 stream = io.StringIO() 

183 self.deprecation.saveToStream(stream) 

184 self.assertNotIn("config.old", stream.getvalue()) 

185 with self.assertWarns(FutureWarning): 

186 self.deprecation.old = 5 

187 stream = io.StringIO() 

188 self.deprecation.saveToStream(stream) 

189 self.assertIn("config.old=5\n", stream.getvalue()) 

190 

191 def testDocstring(self): 

192 """Test that the docstring is not allowed to be empty.""" 

193 with self.assertRaises(ValueError): 

194 pexConfig.Field("", int, default=1) 

195 

196 with self.assertRaises(ValueError): 

197 pexConfig.RangeField("", int, default=3, min=3, max=4) 

198 

199 with self.assertRaises(ValueError): 

200 pexConfig.DictField("", str, str, default={"key": "value"}) 

201 

202 with self.assertRaises(ValueError): 

203 pexConfig.ListField("", int, default=[1, 2, 3]) 

204 

205 with self.assertRaises(ValueError): 

206 pexConfig.ConfigField("", InnerConfig) 

207 

208 with self.assertRaises(ValueError): 

209 pexConfig.ConfigChoiceField("", typemap=GLOBAL_REGISTRY, default="AAA") 

210 

211 def testValidate(self): 

212 self.simple.validate() 

213 

214 self.inner.validate() 

215 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5) 

216 self.outer.i.f = 10.0 

217 self.outer.validate() 

218 

219 try: 

220 self.simple.d["failKey"] = "failValue" 

221 except pexConfig.FieldValidationError: 

222 pass 

223 except Exception: 

224 raise "Validation error Expected" 

225 self.simple.validate() 

226 

227 self.outer.i = InnerConfig 

228 self.assertRaises(ValueError, self.outer.validate) 

229 self.outer.i = InnerConfig() 

230 self.assertRaises(ValueError, self.outer.validate) 

231 

232 self.comp.validate() 

233 self.comp.r = None 

234 self.assertRaises(ValueError, self.comp.validate) 

235 self.comp.r = "BBB" 

236 self.comp.validate() 

237 

238 def testRangeFieldConstructor(self): 

239 """Test RangeField constructor's checking of min, max.""" 

240 val = 3 

241 self.assertRaises(ValueError, pexConfig.RangeField, "test", int, default=val, min=val, max=val - 1) 

242 self.assertRaises( 

243 ValueError, pexConfig.RangeField, "test", float, default=val, min=val, max=val - 1e-15 

244 ) 

245 for inclusiveMin, inclusiveMax in itertools.product((False, True), (False, True)): 

246 if inclusiveMin and inclusiveMax: 

247 # should not raise 

248 class Cfg1(pexConfig.Config): 

249 r1 = pexConfig.RangeField( 

250 doc="test", 

251 dtype=int, 

252 default=val, 

253 min=val, 

254 max=val, 

255 inclusiveMin=inclusiveMin, 

256 inclusiveMax=inclusiveMax, 

257 ) 

258 r2 = pexConfig.RangeField( 

259 doc="test", 

260 dtype=float, 

261 default=val, 

262 min=val, 

263 max=val, 

264 inclusiveMin=inclusiveMin, 

265 inclusiveMax=inclusiveMax, 

266 ) 

267 

268 Cfg1() 

269 else: 

270 # raise while constructing the RangeField (hence cannot make 

271 # it part of a Config) 

272 self.assertRaises( 

273 ValueError, 

274 pexConfig.RangeField, 

275 doc="test", 

276 dtype=int, 

277 default=val, 

278 min=val, 

279 max=val, 

280 inclusiveMin=inclusiveMin, 

281 inclusiveMax=inclusiveMax, 

282 ) 

283 self.assertRaises( 

284 ValueError, 

285 pexConfig.RangeField, 

286 doc="test", 

287 dtype=float, 

288 default=val, 

289 min=val, 

290 max=val, 

291 inclusiveMin=inclusiveMin, 

292 inclusiveMax=inclusiveMax, 

293 ) 

294 

295 def testRangeFieldDefault(self): 

296 """Test RangeField's checking of the default value.""" 

297 minVal = 3 

298 maxVal = 4 

299 for val, inclusiveMin, inclusiveMax, shouldRaise in ( 

300 (minVal, False, True, True), 

301 (minVal, True, True, False), 

302 (maxVal, True, False, True), 

303 (maxVal, True, True, False), 

304 ): 

305 

306 class Cfg1(pexConfig.Config): 

307 r = pexConfig.RangeField( 

308 doc="test", 

309 dtype=int, 

310 default=val, 

311 min=minVal, 

312 max=maxVal, 

313 inclusiveMin=inclusiveMin, 

314 inclusiveMax=inclusiveMax, 

315 ) 

316 

317 class Cfg2(pexConfig.Config): 

318 r2 = pexConfig.RangeField( 

319 doc="test", 

320 dtype=float, 

321 default=val, 

322 min=minVal, 

323 max=maxVal, 

324 inclusiveMin=inclusiveMin, 

325 inclusiveMax=inclusiveMax, 

326 ) 

327 

328 if shouldRaise: 

329 self.assertRaises(pexConfig.FieldValidationError, Cfg1) 

330 self.assertRaises(pexConfig.FieldValidationError, Cfg2) 

331 else: 

332 Cfg1() 

333 Cfg2() 

334 

335 def testSave(self): 

336 self.comp.r = "BBB" 

337 self.comp.p = "AAA" 

338 self.comp.c.f = 5.0 

339 with tempfile.TemporaryDirectory(prefix="config-save-test", ignore_cleanup_errors=True) as tmpdir: 

340 roundtrip_path = os.path.join(tmpdir, "roundtrip.test") 

341 self.comp.save(roundtrip_path) 

342 

343 roundTrip = Complex() 

344 roundTrip.load(roundtrip_path) 

345 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

346 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

347 del roundTrip 

348 

349 # test saving to an open file 

350 roundtrip_path = os.path.join(tmpdir, "roundtrip_open.test") 

351 with open(roundtrip_path, "w") as outfile: 

352 self.comp.saveToStream(outfile) 

353 roundTrip = Complex() 

354 with open(roundtrip_path) as infile: 

355 roundTrip.loadFromStream(infile) 

356 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

357 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

358 del roundTrip 

359 

360 # Test an override of the default variable name. 

361 roundtrip_path = os.path.join(tmpdir, "roundtrip_def.test") 

362 with open(roundtrip_path, "w") as outfile: 

363 self.comp.saveToStream(outfile, root="root") 

364 roundTrip = Complex() 

365 with self.assertRaises(NameError): 

366 roundTrip.load(roundtrip_path) 

367 roundTrip.load(roundtrip_path, root="root") 

368 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

369 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

370 

371 # test saving to a string. 

372 saved_string = self.comp.saveToString() 

373 roundTrip = Complex() 

374 roundTrip.loadFromString(saved_string) 

375 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

376 self.assertEqual(self.comp.r.name, roundTrip.r.name) 

377 del roundTrip 

378 

379 def testDuplicateRegistryNames(self): 

380 self.comp.r["AAA"].f = 5.0 

381 self.assertEqual(self.comp.p["AAA"].f, 3.0) 

382 

383 def testInheritance(self): 

384 class AAA(pexConfig.Config): 

385 a = pexConfig.Field("AAA.a", int, default=4) 

386 

387 class BBB(AAA): 

388 b = pexConfig.Field("BBB.b", int, default=3) 

389 

390 class CCC(BBB): 

391 c = pexConfig.Field("CCC.c", int, default=2) 

392 

393 # test multi-level inheritance 

394 c = CCC() 

395 self.assertIn("a", c.toDict()) 

396 self.assertEqual(c._fields["a"].dtype, int) 

397 self.assertEqual(c.a, 4) 

398 

399 # test conflicting multiple inheritance 

400 class DDD(pexConfig.Config): 

401 a = pexConfig.Field("DDD.a", float, default=0.0) 

402 

403 class EEE(DDD, AAA): 

404 pass 

405 

406 e = EEE() 

407 self.assertEqual(e._fields["a"].dtype, float) 

408 self.assertIn("a", e.toDict()) 

409 self.assertEqual(e.a, 0.0) 

410 

411 class FFF(AAA, DDD): 

412 pass 

413 

414 f = FFF() 

415 self.assertEqual(f._fields["a"].dtype, int) 

416 self.assertIn("a", f.toDict()) 

417 self.assertEqual(f.a, 4) 

418 

419 # test inheritance from non Config objects 

420 class GGG: 

421 a = pexConfig.Field("AAA.a", float, default=10.0) 

422 

423 class HHH(GGG, AAA): 

424 pass 

425 

426 h = HHH() 

427 self.assertEqual(h._fields["a"].dtype, float) 

428 self.assertIn("a", h.toDict()) 

429 self.assertEqual(h.a, 10.0) 

430 

431 # test partial Field redefinition 

432 

433 class III(AAA): 

434 pass 

435 

436 III.a.default = 5 

437 

438 self.assertEqual(III.a.default, 5) 

439 self.assertEqual(AAA.a.default, 4) 

440 

441 @unittest.skipIf(dafBase is None, "lsst.daf.base is required") 

442 def testConvertPropertySet(self): 

443 ps = pexConfig.makePropertySet(self.simple) 

444 self.assertFalse(ps.exists("i")) 

445 self.assertEqual(ps.getScalar("f"), self.simple.f) 

446 self.assertEqual(ps.getScalar("b"), self.simple.b) 

447 self.assertEqual(ps.getScalar("c"), self.simple.c) 

448 self.assertEqual(list(ps.getArray("ll")), list(self.simple.ll)) 

449 

450 ps = pexConfig.makePropertySet(self.comp) 

451 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f) 

452 

453 def testFreeze(self): 

454 self.comp.freeze() 

455 

456 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.c, "f", 10.0) 

457 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "r", "AAA") 

458 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "p", "AAA") 

459 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.p["AAA"], "f", 5.0) 

460 

461 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere): 

462 self.comp.c.f = 5.0 

463 

464 # Generate a Config through loading 

465 stream = io.StringIO() 

466 stream.write(str(importStatement)) 

467 self.comp.saveToStream(stream) 

468 roundtrip = Complex() 

469 roundtrip.loadFromStream(stream.getvalue()) 

470 self.assertEqual(self.comp.c.f, roundtrip.c.f) 

471 

472 # Check the save stream 

473 stream = io.StringIO() 

474 roundtrip.saveToStream(stream) 

475 self.assertEqual(self.comp.c.f, roundtrip.c.f) 

476 streamStr = stream.getvalue() 

477 if shouldBeThere: 

478 self.assertTrue(re.search(searchString, streamStr)) 

479 else: 

480 self.assertFalse(re.search(searchString, streamStr)) 

481 

482 def testImports(self): 

483 # A module not used by anything else, but which exists 

484 importing = "import lsst.pex.config._doNotImportMe\n" 

485 self.checkImportRoundTrip(importing, importing, True) 

486 

487 def testBadImports(self): 

488 dummy = "somethingThatDoesntExist" 

489 importing = ( 

490 """ 

491try: 

492 import %s 

493except ImportError: 

494 pass 

495""" 

496 % dummy 

497 ) 

498 self.checkImportRoundTrip(importing, dummy, False) 

499 

500 def testPickle(self): 

501 self.simple.f = 5 

502 simple = pickle.loads(pickle.dumps(self.simple)) 

503 self.assertIsInstance(simple, Simple) 

504 self.assertEqual(self.simple.f, simple.f) 

505 

506 self.comp.c.f = 5 

507 comp = pickle.loads(pickle.dumps(self.comp)) 

508 self.assertIsInstance(comp, Complex) 

509 self.assertEqual(self.comp.c.f, comp.c.f) 

510 

511 @unittest.skipIf(yaml is None, "Test requires pyyaml") 

512 def testYaml(self): 

513 self.simple.f = 5 

514 simple = yaml.safe_load(yaml.dump(self.simple)) 

515 self.assertIsInstance(simple, Simple) 

516 self.assertEqual(self.simple.f, simple.f) 

517 

518 self.comp.c.f = 5 

519 # Use a different loader to check that it also works 

520 comp = yaml.load(yaml.dump(self.comp), Loader=yaml.FullLoader) 

521 self.assertIsInstance(comp, Complex) 

522 self.assertEqual(self.comp.c.f, comp.c.f) 

523 

524 def testCompare(self): 

525 comp2 = Complex() 

526 inner2 = InnerConfig() 

527 simple2 = Simple() 

528 self.assertTrue(self.comp.compare(comp2)) 

529 self.assertTrue(comp2.compare(self.comp)) 

530 self.assertTrue(self.comp.c.compare(inner2)) 

531 self.assertTrue(self.simple.compare(simple2)) 

532 self.assertTrue(simple2.compare(self.simple)) 

533 self.assertEqual(self.simple, simple2) 

534 self.assertEqual(simple2, self.simple) 

535 outList = [] 

536 

537 def outFunc(msg): 

538 outList.append(msg) 

539 

540 simple2.b = True 

541 simple2.ll.append(4) 

542 simple2.d["foo"] = "var" 

543 self.assertFalse(self.simple.compare(simple2, shortcut=True, output=outFunc)) 

544 self.assertEqual(len(outList), 1) 

545 del outList[:] 

546 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc)) 

547 output = "\n".join(outList) 

548 self.assertIn("Inequality in b", output) 

549 self.assertIn("Inequality in size for ll", output) 

550 self.assertIn("Inequality in keys for d", output) 

551 del outList[:] 

552 self.simple.d["foo"] = "vast" 

553 self.simple.ll.append(5) 

554 self.simple.b = True 

555 self.simple.f += 1e8 

556 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc)) 

557 output = "\n".join(outList) 

558 self.assertIn("Inequality in f", output) 

559 self.assertIn("Inequality in ll[3]", output) 

560 self.assertIn("Inequality in d['foo']", output) 

561 del outList[:] 

562 comp2.r["BBB"].f = 1.0 # changing the non-selected item shouldn't break equality 

563 self.assertTrue(self.comp.compare(comp2)) 

564 comp2.r["AAA"].i = 56 # changing the selected item should break equality 

565 comp2.c.f = 1.0 

566 self.assertFalse(self.comp.compare(comp2, shortcut=False, output=outFunc)) 

567 output = "\n".join(outList) 

568 self.assertIn("Inequality in c.f", output) 

569 self.assertIn("Inequality in r['AAA']", output) 

570 self.assertNotIn("Inequality in r['BBB']", output) 

571 

572 # Before DM-16561, this incorrectly returned `True`. 

573 self.assertFalse(self.inner.compare(self.outer)) 

574 # Before DM-16561, this raised. 

575 self.assertFalse(self.outer.compare(self.inner)) 

576 

577 def testLoadError(self): 

578 """Check that loading allows errors in the file being loaded to 

579 propagate. 

580 """ 

581 self.assertRaises(SyntaxError, self.simple.loadFromStream, "bork bork bork") 

582 self.assertRaises(NameError, self.simple.loadFromStream, "config.f = bork") 

583 

584 def testNames(self): 

585 """Check that the names() method returns valid keys. 

586 

587 Also check that we have the right number of keys, and as they are 

588 all known to be valid we know that we got them all. 

589 """ 

590 names = self.simple.names() 

591 self.assertEqual(len(names), 8) 

592 for name in names: 

593 self.assertTrue(hasattr(self.simple, name)) 

594 

595 def testIteration(self): 

596 self.assertIn("ll", self.simple) 

597 self.assertIn("ll", self.simple.keys()) 

598 self.assertIn("Hello", self.simple.values()) 

599 self.assertEqual(len(self.simple.values()), 8) 

600 

601 for k, v, (k1, v1) in zip(self.simple.keys(), self.simple.values(), self.simple.items()): 

602 self.assertEqual(k, k1) 

603 if k == "n": 

604 self.assertNotEqual(v, v1) 

605 else: 

606 self.assertEqual(v, v1) 

607 

608 

609if __name__ == "__main__": 609 ↛ 610line 609 didn't jump to line 610, because the condition on line 609 was never true

610 unittest.main()