Coverage for tests/test_Config.py: 19%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

315 statements  

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 re 

31import os 

32import pickle 

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("choice test", str, default="Hello", 

57 allowed={"Hello": "First choice", "World": "second choice"}) 

58 r = pexConfig.RangeField("Range test", float, default=3.0, optional=False, 

59 min=3.0, inclusiveMin=True) 

60 ll = pexConfig.ListField("list test", int, default=[1, 2, 3], maxLength=5, 60 ↛ exitline 60 didn't jump to the function exit

61 itemCheck=lambda x: x is not None and x > 0) 

62 d = pexConfig.DictField("dict test", str, str, default={"key": "value"}, 62 ↛ exitline 62 didn't jump to the function exit

63 itemCheck=lambda x: x.startswith('v')) 

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

65 

66 

67GLOBAL_REGISTRY["AAA"] = Simple 

68 

69 

70class InnerConfig(pexConfig.Config): 

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

72 

73 

74GLOBAL_REGISTRY["BBB"] = InnerConfig 

75 

76 

77class OuterConfig(InnerConfig, pexConfig.Config): 

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

79 

80 def __init__(self): 

81 pexConfig.Config.__init__(self) 

82 self.i.f = 5.0 

83 

84 def validate(self): 

85 pexConfig.Config.validate(self) 

86 if self.i.f < 5: 

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

88 

89 

90class Complex(pexConfig.Config): 

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

92 r = pexConfig.ConfigChoiceField("a registry field", typemap=GLOBAL_REGISTRY, 

93 default="AAA", optional=False) 

94 p = pexConfig.ConfigChoiceField("another registry", typemap=GLOBAL_REGISTRY, 

95 default="BBB", optional=True) 

96 

97 

98class Deprecation(pexConfig.Config): 

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

100 

101 

102class ConfigTest(unittest.TestCase): 

103 def setUp(self): 

104 self.simple = Simple() 

105 self.inner = InnerConfig() 

106 self.outer = OuterConfig() 

107 self.comp = Complex() 

108 self.deprecation = Deprecation() 

109 

110 def tearDown(self): 

111 del self.simple 

112 del self.inner 

113 del self.outer 

114 del self.comp 

115 

116 def testInit(self): 

117 self.assertIsNone(self.simple.i) 

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

119 self.assertFalse(self.simple.b) 

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

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

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

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

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

125 

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

127 

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

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

130 

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

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

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

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

135 

136 def testDeprecationWarning(self): 

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

138 """ 

139 with self.assertWarns(FutureWarning) as w: 

140 self.deprecation.old = 5 

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

142 

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

144 

145 def testDeprecationOutput(self): 

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

147 """ 

148 stream = io.StringIO() 

149 self.deprecation.saveToStream(stream) 

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

151 with self.assertWarns(FutureWarning): 

152 self.deprecation.old = 5 

153 stream = io.StringIO() 

154 self.deprecation.saveToStream(stream) 

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

156 

157 def testValidate(self): 

158 self.simple.validate() 

159 

160 self.inner.validate() 

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

162 self.outer.i.f = 10. 

163 self.outer.validate() 

164 

165 try: 

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

167 except pexConfig.FieldValidationError: 

168 pass 

169 except Exception: 

170 raise "Validation error Expected" 

171 self.simple.validate() 

172 

173 self.outer.i = InnerConfig 

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

175 self.outer.i = InnerConfig() 

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

177 

178 self.comp.validate() 

179 self.comp.r = None 

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

181 self.comp.r = "BBB" 

182 self.comp.validate() 

183 

184 def testRangeFieldConstructor(self): 

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

186 """ 

187 val = 3 

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

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

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

191 if inclusiveMin and inclusiveMax: 

192 # should not raise 

193 class Cfg1(pexConfig.Config): 

194 r1 = pexConfig.RangeField(doc="", dtype=int, 

195 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

196 inclusiveMax=inclusiveMax) 

197 r2 = pexConfig.RangeField(doc="", dtype=float, 

198 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

199 inclusiveMax=inclusiveMax) 

200 Cfg1() 

201 else: 

202 # raise while constructing the RangeField (hence cannot make 

203 # it part of a Config) 

204 self.assertRaises(ValueError, pexConfig.RangeField, doc="", dtype=int, 

205 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

206 inclusiveMax=inclusiveMax) 

207 self.assertRaises(ValueError, pexConfig.RangeField, doc="", dtype=float, 

208 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

209 inclusiveMax=inclusiveMax) 

210 

211 def testRangeFieldDefault(self): 

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

213 """ 

214 minVal = 3 

215 maxVal = 4 

216 for val, inclusiveMin, inclusiveMax, shouldRaise in ( 

217 (minVal, False, True, True), 

218 (minVal, True, True, False), 

219 (maxVal, True, False, True), 

220 (maxVal, True, True, False), 

221 ): 

222 class Cfg1(pexConfig.Config): 

223 r = pexConfig.RangeField(doc="", dtype=int, 

224 default=val, min=minVal, max=maxVal, 

225 inclusiveMin=inclusiveMin, inclusiveMax=inclusiveMax) 

226 

227 class Cfg2(pexConfig.Config): 

228 r2 = pexConfig.RangeField(doc="", dtype=float, 

229 default=val, min=minVal, max=maxVal, 

230 inclusiveMin=inclusiveMin, inclusiveMax=inclusiveMax) 

231 if shouldRaise: 

232 self.assertRaises(pexConfig.FieldValidationError, Cfg1) 

233 self.assertRaises(pexConfig.FieldValidationError, Cfg2) 

234 else: 

235 Cfg1() 

236 Cfg2() 

237 

238 def testSave(self): 

239 self.comp.r = "BBB" 

240 self.comp.p = "AAA" 

241 self.comp.c.f = 5. 

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

243 

244 roundTrip = Complex() 

245 roundTrip.load("roundtrip.test") 

246 os.remove("roundtrip.test") 

247 

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

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

250 

251 del roundTrip 

252 # test saving to an open file 

253 outfile = open("roundtrip.test", "w") 

254 self.comp.saveToStream(outfile) 

255 outfile.close() 

256 

257 roundTrip = Complex() 

258 roundTrip.load("roundtrip.test") 

259 os.remove("roundtrip.test") 

260 

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

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

263 

264 # test backwards compatibility feature of allowing "root" instead of 

265 # "config" 

266 outfile = open("roundtrip.test", "w") 

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

268 outfile.close() 

269 

270 roundTrip = Complex() 

271 roundTrip.load("roundtrip.test") 

272 os.remove("roundtrip.test") 

273 

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

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

276 

277 def testDuplicateRegistryNames(self): 

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

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

280 

281 def testInheritance(self): 

282 class AAA(pexConfig.Config): 

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

284 

285 class BBB(AAA): 

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

287 

288 class CCC(BBB): 

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

290 

291 # test multi-level inheritance 

292 c = CCC() 

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

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

295 self.assertEqual(c.a, 4) 

296 

297 # test conflicting multiple inheritance 

298 class DDD(pexConfig.Config): 

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

300 

301 class EEE(DDD, AAA): 

302 pass 

303 

304 e = EEE() 

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

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

307 self.assertEqual(e.a, 0.0) 

308 

309 class FFF(AAA, DDD): 

310 pass 

311 f = FFF() 

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

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

314 self.assertEqual(f.a, 4) 

315 

316 # test inheritance from non Config objects 

317 class GGG: 

318 a = pexConfig.Field("AAA.a", float, default=10.) 

319 

320 class HHH(GGG, AAA): 

321 pass 

322 h = HHH() 

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

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

325 self.assertEqual(h.a, 10.0) 

326 

327 # test partial Field redefinition 

328 

329 class III(AAA): 

330 pass 

331 III.a.default = 5 

332 

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

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

335 

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

337 def testConvertPropertySet(self): 

338 ps = pexConfig.makePropertySet(self.simple) 

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

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

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

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

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

344 

345 ps = pexConfig.makePropertySet(self.comp) 

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

347 

348 def testFreeze(self): 

349 self.comp.freeze() 

350 

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

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

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

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

355 

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

357 self.comp.c.f = 5. 

358 

359 # Generate a Config through loading 

360 stream = io.StringIO() 

361 stream.write(str(importStatement)) 

362 self.comp.saveToStream(stream) 

363 roundtrip = Complex() 

364 roundtrip.loadFromStream(stream.getvalue()) 

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

366 

367 # Check the save stream 

368 stream = io.StringIO() 

369 roundtrip.saveToStream(stream) 

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

371 streamStr = stream.getvalue() 

372 if shouldBeThere: 

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

374 else: 

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

376 

377 def testImports(self): 

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

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

380 self.checkImportRoundTrip(importing, importing, True) 

381 

382 def testBadImports(self): 

383 dummy = "somethingThatDoesntExist" 

384 importing = """ 

385try: 

386 import %s 

387except ImportError: 

388 pass 

389""" % dummy 

390 self.checkImportRoundTrip(importing, dummy, False) 

391 

392 def testPickle(self): 

393 self.simple.f = 5 

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

395 self.assertIsInstance(simple, Simple) 

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

397 

398 self.comp.c.f = 5 

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

400 self.assertIsInstance(comp, Complex) 

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

402 

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

404 def testYaml(self): 

405 self.simple.f = 5 

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

407 self.assertIsInstance(simple, Simple) 

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

409 

410 self.comp.c.f = 5 

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

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

413 self.assertIsInstance(comp, Complex) 

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

415 

416 def testCompare(self): 

417 comp2 = Complex() 

418 inner2 = InnerConfig() 

419 simple2 = Simple() 

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

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

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

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

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

425 self.assertEqual(self.simple, simple2) 

426 self.assertEqual(simple2, self.simple) 

427 outList = [] 

428 

429 def outFunc(msg): 

430 outList.append(msg) 

431 simple2.b = True 

432 simple2.ll.append(4) 

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

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

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

436 del outList[:] 

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

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

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

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

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

442 del outList[:] 

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

444 self.simple.ll.append(5) 

445 self.simple.b = True 

446 self.simple.f += 1E8 

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

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

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

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

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

452 del outList[:] 

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

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

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

456 comp2.c.f = 1.0 

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

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

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

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

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

462 

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

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

465 # Before DM-16561, this raised. 

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

467 

468 def testLoadError(self): 

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

470 propagate. 

471 """ 

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

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

474 

475 def testNames(self): 

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

477 

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

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

480 """ 

481 

482 names = self.simple.names() 

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

484 for name in names: 

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

486 

487 

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

489 unittest.main()