Coverage for tests/test_Config.py: 16%

357 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-28 10:15 +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 unittest 

34 

35try: 

36 import yaml 

37except ImportError: 

38 yaml = None 

39 

40import lsst.pex.config as pexConfig 

41 

42# Some tests depend on daf_base. 

43# Skip them if it is not found. 

44try: 

45 import lsst.daf.base as dafBase 

46except ImportError: 

47 dafBase = None 

48 

49GLOBAL_REGISTRY = {} 

50 

51 

52class Simple(pexConfig.Config): 

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

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

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

56 c = pexConfig.ChoiceField( 

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

58 ) 

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

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

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

62 ) 

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

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

65 ) 

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

67 

68 

69GLOBAL_REGISTRY["AAA"] = Simple 

70 

71 

72class InnerConfig(pexConfig.Config): 

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

74 

75 

76GLOBAL_REGISTRY["BBB"] = InnerConfig 

77 

78 

79class OuterConfig(InnerConfig, pexConfig.Config): 

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

81 

82 def __init__(self): 

83 pexConfig.Config.__init__(self) 

84 self.i.f = 5.0 

85 

86 def validate(self): 

87 pexConfig.Config.validate(self) 

88 if self.i.f < 5: 

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

90 

91 

92class Complex(pexConfig.Config): 

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

94 r = pexConfig.ConfigChoiceField( 

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

96 ) 

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

98 

99 

100class Deprecation(pexConfig.Config): 

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

102 

103 

104class ConfigTest(unittest.TestCase): 

105 def setUp(self): 

106 self.simple = Simple() 

107 self.inner = InnerConfig() 

108 self.outer = OuterConfig() 

109 self.comp = Complex() 

110 self.deprecation = Deprecation() 

111 

112 def tearDown(self): 

113 del self.simple 

114 del self.inner 

115 del self.outer 

116 del self.comp 

117 

118 def testFieldTypeAnnotationRuntime(self): 

119 # test parsing type annotation for runtime dtype 

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

121 self.assertEqual(testField.dtype, str) 

122 

123 # verify that forward references work correctly 

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

125 self.assertEqual(testField.dtype, float) 

126 

127 # verify that Field rejects multiple types 

128 with self.assertRaises(ValueError): 

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

130 

131 # verify that Field raises in conflict with dtype: 

132 with self.assertRaises(ValueError): 

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

134 

135 # verify that Field does not raise if dtype agrees 

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

137 self.assertEqual(testField.dtype, int) 

138 

139 def testInit(self): 

140 self.assertIsNone(self.simple.i) 

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

142 self.assertFalse(self.simple.b) 

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

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

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

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

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

148 

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

150 

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

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

153 

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

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

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

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

158 

159 def testDeprecationWarning(self): 

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

161 with self.assertWarns(FutureWarning) as w: 

162 self.deprecation.old = 5 

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

164 

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

166 

167 def testDeprecationOutput(self): 

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

169 stream = io.StringIO() 

170 self.deprecation.saveToStream(stream) 

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

172 with self.assertWarns(FutureWarning): 

173 self.deprecation.old = 5 

174 stream = io.StringIO() 

175 self.deprecation.saveToStream(stream) 

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

177 

178 def testDocstring(self): 

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

180 with self.assertRaises(ValueError): 

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

182 

183 with self.assertRaises(ValueError): 

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

185 

186 with self.assertRaises(ValueError): 

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

188 

189 with self.assertRaises(ValueError): 

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

191 

192 with self.assertRaises(ValueError): 

193 pexConfig.ConfigField("", InnerConfig) 

194 

195 with self.assertRaises(ValueError): 

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

197 

198 def testValidate(self): 

199 self.simple.validate() 

200 

201 self.inner.validate() 

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

203 self.outer.i.f = 10.0 

204 self.outer.validate() 

205 

206 try: 

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

208 except pexConfig.FieldValidationError: 

209 pass 

210 except Exception: 

211 raise "Validation error Expected" 

212 self.simple.validate() 

213 

214 self.outer.i = InnerConfig 

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

216 self.outer.i = InnerConfig() 

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

218 

219 self.comp.validate() 

220 self.comp.r = None 

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

222 self.comp.r = "BBB" 

223 self.comp.validate() 

224 

225 def testRangeFieldConstructor(self): 

226 """Test RangeField constructor's checking of min, max""" 

227 val = 3 

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

229 self.assertRaises( 

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

231 ) 

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

233 if inclusiveMin and inclusiveMax: 

234 # should not raise 

235 class Cfg1(pexConfig.Config): 

236 r1 = pexConfig.RangeField( 

237 doc="test", 

238 dtype=int, 

239 default=val, 

240 min=val, 

241 max=val, 

242 inclusiveMin=inclusiveMin, 

243 inclusiveMax=inclusiveMax, 

244 ) 

245 r2 = pexConfig.RangeField( 

246 doc="test", 

247 dtype=float, 

248 default=val, 

249 min=val, 

250 max=val, 

251 inclusiveMin=inclusiveMin, 

252 inclusiveMax=inclusiveMax, 

253 ) 

254 

255 Cfg1() 

256 else: 

257 # raise while constructing the RangeField (hence cannot make 

258 # it part of a Config) 

259 self.assertRaises( 

260 ValueError, 

261 pexConfig.RangeField, 

262 doc="test", 

263 dtype=int, 

264 default=val, 

265 min=val, 

266 max=val, 

267 inclusiveMin=inclusiveMin, 

268 inclusiveMax=inclusiveMax, 

269 ) 

270 self.assertRaises( 

271 ValueError, 

272 pexConfig.RangeField, 

273 doc="test", 

274 dtype=float, 

275 default=val, 

276 min=val, 

277 max=val, 

278 inclusiveMin=inclusiveMin, 

279 inclusiveMax=inclusiveMax, 

280 ) 

281 

282 def testRangeFieldDefault(self): 

283 """Test RangeField's checking of the default value""" 

284 minVal = 3 

285 maxVal = 4 

286 for val, inclusiveMin, inclusiveMax, shouldRaise in ( 

287 (minVal, False, True, True), 

288 (minVal, True, True, False), 

289 (maxVal, True, False, True), 

290 (maxVal, True, True, False), 

291 ): 

292 

293 class Cfg1(pexConfig.Config): 

294 r = pexConfig.RangeField( 

295 doc="test", 

296 dtype=int, 

297 default=val, 

298 min=minVal, 

299 max=maxVal, 

300 inclusiveMin=inclusiveMin, 

301 inclusiveMax=inclusiveMax, 

302 ) 

303 

304 class Cfg2(pexConfig.Config): 

305 r2 = pexConfig.RangeField( 

306 doc="test", 

307 dtype=float, 

308 default=val, 

309 min=minVal, 

310 max=maxVal, 

311 inclusiveMin=inclusiveMin, 

312 inclusiveMax=inclusiveMax, 

313 ) 

314 

315 if shouldRaise: 

316 self.assertRaises(pexConfig.FieldValidationError, Cfg1) 

317 self.assertRaises(pexConfig.FieldValidationError, Cfg2) 

318 else: 

319 Cfg1() 

320 Cfg2() 

321 

322 def testSave(self): 

323 self.comp.r = "BBB" 

324 self.comp.p = "AAA" 

325 self.comp.c.f = 5.0 

326 self.comp.save("roundtrip.test") 

327 

328 roundTrip = Complex() 

329 roundTrip.load("roundtrip.test") 

330 os.remove("roundtrip.test") 

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

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

333 del roundTrip 

334 

335 # test saving to an open file 

336 with open("roundtrip.test", "w") as outfile: 

337 self.comp.saveToStream(outfile) 

338 roundTrip = Complex() 

339 with open("roundtrip.test", "r") as infile: 

340 roundTrip.loadFromStream(infile) 

341 os.remove("roundtrip.test") 

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

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

344 del roundTrip 

345 

346 # test saving to a string. 

347 saved_string = self.comp.saveToString() 

348 roundTrip = Complex() 

349 roundTrip.loadFromString(saved_string) 

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

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

352 del roundTrip 

353 

354 # Test an override of the default variable name. 

355 with open("roundtrip.test", "w") as outfile: 

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

357 roundTrip = Complex() 

358 with self.assertRaises(NameError): 

359 roundTrip.load("roundtrip.test") 

360 roundTrip.load("roundtrip.test", root="root") 

361 os.remove("roundtrip.test") 

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

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

364 

365 def testDuplicateRegistryNames(self): 

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

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

368 

369 def testInheritance(self): 

370 class AAA(pexConfig.Config): 

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

372 

373 class BBB(AAA): 

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

375 

376 class CCC(BBB): 

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

378 

379 # test multi-level inheritance 

380 c = CCC() 

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

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

383 self.assertEqual(c.a, 4) 

384 

385 # test conflicting multiple inheritance 

386 class DDD(pexConfig.Config): 

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

388 

389 class EEE(DDD, AAA): 

390 pass 

391 

392 e = EEE() 

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

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

395 self.assertEqual(e.a, 0.0) 

396 

397 class FFF(AAA, DDD): 

398 pass 

399 

400 f = FFF() 

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

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

403 self.assertEqual(f.a, 4) 

404 

405 # test inheritance from non Config objects 

406 class GGG: 

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

408 

409 class HHH(GGG, AAA): 

410 pass 

411 

412 h = HHH() 

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

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

415 self.assertEqual(h.a, 10.0) 

416 

417 # test partial Field redefinition 

418 

419 class III(AAA): 

420 pass 

421 

422 III.a.default = 5 

423 

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

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

426 

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

428 def testConvertPropertySet(self): 

429 ps = pexConfig.makePropertySet(self.simple) 

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

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

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

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

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

435 

436 ps = pexConfig.makePropertySet(self.comp) 

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

438 

439 def testFreeze(self): 

440 self.comp.freeze() 

441 

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

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

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

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

446 

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

448 self.comp.c.f = 5.0 

449 

450 # Generate a Config through loading 

451 stream = io.StringIO() 

452 stream.write(str(importStatement)) 

453 self.comp.saveToStream(stream) 

454 roundtrip = Complex() 

455 roundtrip.loadFromStream(stream.getvalue()) 

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

457 

458 # Check the save stream 

459 stream = io.StringIO() 

460 roundtrip.saveToStream(stream) 

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

462 streamStr = stream.getvalue() 

463 if shouldBeThere: 

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

465 else: 

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

467 

468 def testImports(self): 

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

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

471 self.checkImportRoundTrip(importing, importing, True) 

472 

473 def testBadImports(self): 

474 dummy = "somethingThatDoesntExist" 

475 importing = ( 

476 """ 

477try: 

478 import %s 

479except ImportError: 

480 pass 

481""" 

482 % dummy 

483 ) 

484 self.checkImportRoundTrip(importing, dummy, False) 

485 

486 def testPickle(self): 

487 self.simple.f = 5 

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

489 self.assertIsInstance(simple, Simple) 

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

491 

492 self.comp.c.f = 5 

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

494 self.assertIsInstance(comp, Complex) 

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

496 

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

498 def testYaml(self): 

499 self.simple.f = 5 

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

501 self.assertIsInstance(simple, Simple) 

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

503 

504 self.comp.c.f = 5 

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

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

507 self.assertIsInstance(comp, Complex) 

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

509 

510 def testCompare(self): 

511 comp2 = Complex() 

512 inner2 = InnerConfig() 

513 simple2 = Simple() 

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

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

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

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

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

519 self.assertEqual(self.simple, simple2) 

520 self.assertEqual(simple2, self.simple) 

521 outList = [] 

522 

523 def outFunc(msg): 

524 outList.append(msg) 

525 

526 simple2.b = True 

527 simple2.ll.append(4) 

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

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

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

531 del outList[:] 

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

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

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

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

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

537 del outList[:] 

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

539 self.simple.ll.append(5) 

540 self.simple.b = True 

541 self.simple.f += 1e8 

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

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

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

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

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

547 del outList[:] 

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

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

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

551 comp2.c.f = 1.0 

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

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

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

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

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

557 

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

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

560 # Before DM-16561, this raised. 

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

562 

563 def testLoadError(self): 

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

565 propagate. 

566 """ 

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

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

569 

570 def testNames(self): 

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

572 

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

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

575 """ 

576 

577 names = self.simple.names() 

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

579 for name in names: 

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

581 

582 def testIteration(self): 

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

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

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

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

587 

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

589 self.assertEqual(k, k1) 

590 if k == "n": 

591 self.assertNotEqual(v, v1) 

592 else: 

593 self.assertEqual(v, v1) 

594 

595 

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

597 unittest.main()