Coverage for tests/test_Config.py: 17%

344 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-02 06:20 -0800

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="") 

121 self.assertEqual(testField.dtype, str) 

122 

123 # verify that forward references work correctly 

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

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="") # type: ignore 

130 

131 # verify that Field raises in conflict with dtype: 

132 with self.assertRaises(ValueError): 

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

134 

135 # verify that Field does not raise if dtype agrees 

136 testField = pexConfig.Field[int](doc="", 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 testValidate(self): 

179 self.simple.validate() 

180 

181 self.inner.validate() 

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

183 self.outer.i.f = 10.0 

184 self.outer.validate() 

185 

186 try: 

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

188 except pexConfig.FieldValidationError: 

189 pass 

190 except Exception: 

191 raise "Validation error Expected" 

192 self.simple.validate() 

193 

194 self.outer.i = InnerConfig 

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

196 self.outer.i = InnerConfig() 

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

198 

199 self.comp.validate() 

200 self.comp.r = None 

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

202 self.comp.r = "BBB" 

203 self.comp.validate() 

204 

205 def testRangeFieldConstructor(self): 

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

207 val = 3 

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

209 self.assertRaises(ValueError, pexConfig.RangeField, "", float, default=val, min=val, max=val - 1e-15) 

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

211 if inclusiveMin and inclusiveMax: 

212 # should not raise 

213 class Cfg1(pexConfig.Config): 

214 r1 = pexConfig.RangeField( 

215 doc="", 

216 dtype=int, 

217 default=val, 

218 min=val, 

219 max=val, 

220 inclusiveMin=inclusiveMin, 

221 inclusiveMax=inclusiveMax, 

222 ) 

223 r2 = pexConfig.RangeField( 

224 doc="", 

225 dtype=float, 

226 default=val, 

227 min=val, 

228 max=val, 

229 inclusiveMin=inclusiveMin, 

230 inclusiveMax=inclusiveMax, 

231 ) 

232 

233 Cfg1() 

234 else: 

235 # raise while constructing the RangeField (hence cannot make 

236 # it part of a Config) 

237 self.assertRaises( 

238 ValueError, 

239 pexConfig.RangeField, 

240 doc="", 

241 dtype=int, 

242 default=val, 

243 min=val, 

244 max=val, 

245 inclusiveMin=inclusiveMin, 

246 inclusiveMax=inclusiveMax, 

247 ) 

248 self.assertRaises( 

249 ValueError, 

250 pexConfig.RangeField, 

251 doc="", 

252 dtype=float, 

253 default=val, 

254 min=val, 

255 max=val, 

256 inclusiveMin=inclusiveMin, 

257 inclusiveMax=inclusiveMax, 

258 ) 

259 

260 def testRangeFieldDefault(self): 

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

262 minVal = 3 

263 maxVal = 4 

264 for val, inclusiveMin, inclusiveMax, shouldRaise in ( 

265 (minVal, False, True, True), 

266 (minVal, True, True, False), 

267 (maxVal, True, False, True), 

268 (maxVal, True, True, False), 

269 ): 

270 

271 class Cfg1(pexConfig.Config): 

272 r = pexConfig.RangeField( 

273 doc="", 

274 dtype=int, 

275 default=val, 

276 min=minVal, 

277 max=maxVal, 

278 inclusiveMin=inclusiveMin, 

279 inclusiveMax=inclusiveMax, 

280 ) 

281 

282 class Cfg2(pexConfig.Config): 

283 r2 = pexConfig.RangeField( 

284 doc="", 

285 dtype=float, 

286 default=val, 

287 min=minVal, 

288 max=maxVal, 

289 inclusiveMin=inclusiveMin, 

290 inclusiveMax=inclusiveMax, 

291 ) 

292 

293 if shouldRaise: 

294 self.assertRaises(pexConfig.FieldValidationError, Cfg1) 

295 self.assertRaises(pexConfig.FieldValidationError, Cfg2) 

296 else: 

297 Cfg1() 

298 Cfg2() 

299 

300 def testSave(self): 

301 self.comp.r = "BBB" 

302 self.comp.p = "AAA" 

303 self.comp.c.f = 5.0 

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

305 

306 roundTrip = Complex() 

307 roundTrip.load("roundtrip.test") 

308 os.remove("roundtrip.test") 

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

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

311 del roundTrip 

312 

313 # test saving to an open file 

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

315 self.comp.saveToStream(outfile) 

316 roundTrip = Complex() 

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

318 roundTrip.loadFromStream(infile) 

319 os.remove("roundtrip.test") 

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

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

322 del roundTrip 

323 

324 # test saving to a string. 

325 saved_string = self.comp.saveToString() 

326 roundTrip = Complex() 

327 roundTrip.loadFromString(saved_string) 

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

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

330 del roundTrip 

331 

332 # Test an override of the default variable name. 

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

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

335 roundTrip = Complex() 

336 with self.assertRaises(NameError): 

337 roundTrip.load("roundtrip.test") 

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

339 os.remove("roundtrip.test") 

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

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

342 

343 def testDuplicateRegistryNames(self): 

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

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

346 

347 def testInheritance(self): 

348 class AAA(pexConfig.Config): 

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

350 

351 class BBB(AAA): 

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

353 

354 class CCC(BBB): 

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

356 

357 # test multi-level inheritance 

358 c = CCC() 

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

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

361 self.assertEqual(c.a, 4) 

362 

363 # test conflicting multiple inheritance 

364 class DDD(pexConfig.Config): 

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

366 

367 class EEE(DDD, AAA): 

368 pass 

369 

370 e = EEE() 

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

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

373 self.assertEqual(e.a, 0.0) 

374 

375 class FFF(AAA, DDD): 

376 pass 

377 

378 f = FFF() 

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

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

381 self.assertEqual(f.a, 4) 

382 

383 # test inheritance from non Config objects 

384 class GGG: 

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

386 

387 class HHH(GGG, AAA): 

388 pass 

389 

390 h = HHH() 

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

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

393 self.assertEqual(h.a, 10.0) 

394 

395 # test partial Field redefinition 

396 

397 class III(AAA): 

398 pass 

399 

400 III.a.default = 5 

401 

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

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

404 

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

406 def testConvertPropertySet(self): 

407 ps = pexConfig.makePropertySet(self.simple) 

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

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

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

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

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

413 

414 ps = pexConfig.makePropertySet(self.comp) 

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

416 

417 def testFreeze(self): 

418 self.comp.freeze() 

419 

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

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

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

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

424 

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

426 self.comp.c.f = 5.0 

427 

428 # Generate a Config through loading 

429 stream = io.StringIO() 

430 stream.write(str(importStatement)) 

431 self.comp.saveToStream(stream) 

432 roundtrip = Complex() 

433 roundtrip.loadFromStream(stream.getvalue()) 

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

435 

436 # Check the save stream 

437 stream = io.StringIO() 

438 roundtrip.saveToStream(stream) 

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

440 streamStr = stream.getvalue() 

441 if shouldBeThere: 

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

443 else: 

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

445 

446 def testImports(self): 

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

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

449 self.checkImportRoundTrip(importing, importing, True) 

450 

451 def testBadImports(self): 

452 dummy = "somethingThatDoesntExist" 

453 importing = ( 

454 """ 

455try: 

456 import %s 

457except ImportError: 

458 pass 

459""" 

460 % dummy 

461 ) 

462 self.checkImportRoundTrip(importing, dummy, False) 

463 

464 def testPickle(self): 

465 self.simple.f = 5 

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

467 self.assertIsInstance(simple, Simple) 

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

469 

470 self.comp.c.f = 5 

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

472 self.assertIsInstance(comp, Complex) 

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

474 

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

476 def testYaml(self): 

477 self.simple.f = 5 

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

479 self.assertIsInstance(simple, Simple) 

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

481 

482 self.comp.c.f = 5 

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

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

485 self.assertIsInstance(comp, Complex) 

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

487 

488 def testCompare(self): 

489 comp2 = Complex() 

490 inner2 = InnerConfig() 

491 simple2 = Simple() 

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

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

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

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

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

497 self.assertEqual(self.simple, simple2) 

498 self.assertEqual(simple2, self.simple) 

499 outList = [] 

500 

501 def outFunc(msg): 

502 outList.append(msg) 

503 

504 simple2.b = True 

505 simple2.ll.append(4) 

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

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

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

509 del outList[:] 

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

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

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

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

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

515 del outList[:] 

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

517 self.simple.ll.append(5) 

518 self.simple.b = True 

519 self.simple.f += 1e8 

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

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

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

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

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

525 del outList[:] 

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

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

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

529 comp2.c.f = 1.0 

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

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

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

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

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

535 

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

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

538 # Before DM-16561, this raised. 

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

540 

541 def testLoadError(self): 

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

543 propagate. 

544 """ 

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

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

547 

548 def testNames(self): 

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

550 

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

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

553 """ 

554 

555 names = self.simple.names() 

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

557 for name in names: 

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

559 

560 def testIteration(self): 

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

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

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

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

565 

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

567 self.assertEqual(k, k1) 

568 if k == "n": 

569 self.assertNotEqual(v, v1) 

570 else: 

571 self.assertEqual(v, v1) 

572 

573 

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

575 unittest.main()