Coverage for tests/test_Config.py: 20%

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

321 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 self.assertEqual(self.comp.c.f, roundTrip.c.f) 

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

249 del roundTrip 

250 

251 # test saving to an open file 

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

253 self.comp.saveToStream(outfile) 

254 roundTrip = Complex() 

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

256 roundTrip.loadFromStream(infile) 

257 os.remove("roundtrip.test") 

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

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

260 del roundTrip 

261 

262 # test saving to a string. 

263 saved_string = self.comp.saveToString() 

264 roundTrip = Complex() 

265 roundTrip.loadFromString(saved_string) 

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

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

268 del roundTrip 

269 

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

271 # "config" 

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

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

274 roundTrip = Complex() 

275 roundTrip.load("roundtrip.test") 

276 os.remove("roundtrip.test") 

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

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

279 

280 def testDuplicateRegistryNames(self): 

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

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

283 

284 def testInheritance(self): 

285 class AAA(pexConfig.Config): 

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

287 

288 class BBB(AAA): 

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

290 

291 class CCC(BBB): 

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

293 

294 # test multi-level inheritance 

295 c = CCC() 

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

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

298 self.assertEqual(c.a, 4) 

299 

300 # test conflicting multiple inheritance 

301 class DDD(pexConfig.Config): 

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

303 

304 class EEE(DDD, AAA): 

305 pass 

306 

307 e = EEE() 

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

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

310 self.assertEqual(e.a, 0.0) 

311 

312 class FFF(AAA, DDD): 

313 pass 

314 f = FFF() 

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

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

317 self.assertEqual(f.a, 4) 

318 

319 # test inheritance from non Config objects 

320 class GGG: 

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

322 

323 class HHH(GGG, AAA): 

324 pass 

325 h = HHH() 

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

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

328 self.assertEqual(h.a, 10.0) 

329 

330 # test partial Field redefinition 

331 

332 class III(AAA): 

333 pass 

334 III.a.default = 5 

335 

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

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

338 

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

340 def testConvertPropertySet(self): 

341 ps = pexConfig.makePropertySet(self.simple) 

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

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

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

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

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

347 

348 ps = pexConfig.makePropertySet(self.comp) 

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

350 

351 def testFreeze(self): 

352 self.comp.freeze() 

353 

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

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

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

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

358 

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

360 self.comp.c.f = 5. 

361 

362 # Generate a Config through loading 

363 stream = io.StringIO() 

364 stream.write(str(importStatement)) 

365 self.comp.saveToStream(stream) 

366 roundtrip = Complex() 

367 roundtrip.loadFromStream(stream.getvalue()) 

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

369 

370 # Check the save stream 

371 stream = io.StringIO() 

372 roundtrip.saveToStream(stream) 

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

374 streamStr = stream.getvalue() 

375 if shouldBeThere: 

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

377 else: 

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

379 

380 def testImports(self): 

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

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

383 self.checkImportRoundTrip(importing, importing, True) 

384 

385 def testBadImports(self): 

386 dummy = "somethingThatDoesntExist" 

387 importing = """ 

388try: 

389 import %s 

390except ImportError: 

391 pass 

392""" % dummy 

393 self.checkImportRoundTrip(importing, dummy, False) 

394 

395 def testPickle(self): 

396 self.simple.f = 5 

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

398 self.assertIsInstance(simple, Simple) 

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

400 

401 self.comp.c.f = 5 

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

403 self.assertIsInstance(comp, Complex) 

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

405 

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

407 def testYaml(self): 

408 self.simple.f = 5 

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

410 self.assertIsInstance(simple, Simple) 

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

412 

413 self.comp.c.f = 5 

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

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

416 self.assertIsInstance(comp, Complex) 

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

418 

419 def testCompare(self): 

420 comp2 = Complex() 

421 inner2 = InnerConfig() 

422 simple2 = Simple() 

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

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

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

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

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

428 self.assertEqual(self.simple, simple2) 

429 self.assertEqual(simple2, self.simple) 

430 outList = [] 

431 

432 def outFunc(msg): 

433 outList.append(msg) 

434 simple2.b = True 

435 simple2.ll.append(4) 

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

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

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

439 del outList[:] 

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

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

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

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

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

445 del outList[:] 

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

447 self.simple.ll.append(5) 

448 self.simple.b = True 

449 self.simple.f += 1E8 

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

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

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

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

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

455 del outList[:] 

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

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

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

459 comp2.c.f = 1.0 

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

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

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

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

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

465 

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

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

468 # Before DM-16561, this raised. 

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

470 

471 def testLoadError(self): 

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

473 propagate. 

474 """ 

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

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

477 

478 def testNames(self): 

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

480 

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

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

483 """ 

484 

485 names = self.simple.names() 

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

487 for name in names: 

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

489 

490 

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

492 unittest.main()