Hide keyboard shortcuts

Hot-keys 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

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 

35import lsst.pex.config as pexConfig 

36 

37# Some tests depend on daf_base or pex_policy. 

38# Skip them if they are not found. 

39try: 

40 import lsst.daf.base as dafBase 

41except ImportError: 

42 dafBase = None 

43try: 

44 import lsst.pex.policy as pexPolicy 

45except ImportError: 

46 pexPolicy = None 

47 

48GLOBAL_REGISTRY = {} 

49 

50 

51class Simple(pexConfig.Config): 

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

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

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

55 c = pexConfig.ChoiceField("choice test", str, default="Hello", 

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

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

58 min=3.0, inclusiveMin=True) 

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

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

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

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

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

64 

65 

66GLOBAL_REGISTRY["AAA"] = Simple 

67 

68 

69class InnerConfig(pexConfig.Config): 

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

71 

72 

73GLOBAL_REGISTRY["BBB"] = InnerConfig 

74 

75 

76class OuterConfig(InnerConfig, pexConfig.Config): 

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

78 

79 def __init__(self): 

80 pexConfig.Config.__init__(self) 

81 self.i.f = 5.0 

82 

83 def validate(self): 

84 pexConfig.Config.validate(self) 

85 if self.i.f < 5: 

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

87 

88 

89class Complex(pexConfig.Config): 

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

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

92 default="AAA", optional=False) 

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

94 default="BBB", optional=True) 

95 

96 

97class Deprecation(pexConfig.Config): 

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

99 

100 

101class ConfigTest(unittest.TestCase): 

102 def setUp(self): 

103 self.simple = Simple() 

104 self.inner = InnerConfig() 

105 self.outer = OuterConfig() 

106 self.comp = Complex() 

107 self.deprecation = Deprecation() 

108 

109 def tearDown(self): 

110 del self.simple 

111 del self.inner 

112 del self.outer 

113 del self.comp 

114 

115 def testInit(self): 

116 self.assertIsNone(self.simple.i) 

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

118 self.assertFalse(self.simple.b) 

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

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

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

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

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

124 

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

126 

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

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

129 

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

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

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

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

134 

135 def testDeprecationWarning(self): 

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

137 """ 

138 with self.assertWarns(FutureWarning) as w: 

139 self.deprecation.old = 5 

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

141 

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

143 

144 def testDeprecationOutput(self): 

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

146 """ 

147 stream = io.StringIO() 

148 self.deprecation.saveToStream(stream) 

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

150 with self.assertWarns(FutureWarning): 

151 self.deprecation.old = 5 

152 stream = io.StringIO() 

153 self.deprecation.saveToStream(stream) 

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

155 

156 def testValidate(self): 

157 self.simple.validate() 

158 

159 self.inner.validate() 

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

161 self.outer.i.f = 10. 

162 self.outer.validate() 

163 

164 try: 

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

166 except pexConfig.FieldValidationError: 

167 pass 

168 except Exception: 

169 raise "Validation error Expected" 

170 self.simple.validate() 

171 

172 self.outer.i = InnerConfig 

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

174 self.outer.i = InnerConfig() 

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

176 

177 self.comp.validate() 

178 self.comp.r = None 

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

180 self.comp.r = "BBB" 

181 self.comp.validate() 

182 

183 def testRangeFieldConstructor(self): 

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

185 """ 

186 val = 3 

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

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

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

190 if inclusiveMin and inclusiveMax: 

191 # should not raise 

192 class Cfg1(pexConfig.Config): 

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

194 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

195 inclusiveMax=inclusiveMax) 

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

197 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

198 inclusiveMax=inclusiveMax) 

199 Cfg1() 

200 else: 

201 # raise while constructing the RangeField (hence cannot make 

202 # it part of a Config) 

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

204 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

205 inclusiveMax=inclusiveMax) 

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

207 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

208 inclusiveMax=inclusiveMax) 

209 

210 def testRangeFieldDefault(self): 

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

212 """ 

213 minVal = 3 

214 maxVal = 4 

215 for val, inclusiveMin, inclusiveMax, shouldRaise in ( 

216 (minVal, False, True, True), 

217 (minVal, True, True, False), 

218 (maxVal, True, False, True), 

219 (maxVal, True, True, False), 

220 ): 

221 class Cfg1(pexConfig.Config): 

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

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

224 inclusiveMin=inclusiveMin, inclusiveMax=inclusiveMax) 

225 

226 class Cfg2(pexConfig.Config): 

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

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

229 inclusiveMin=inclusiveMin, inclusiveMax=inclusiveMax) 

230 if shouldRaise: 

231 self.assertRaises(pexConfig.FieldValidationError, Cfg1) 

232 self.assertRaises(pexConfig.FieldValidationError, Cfg2) 

233 else: 

234 Cfg1() 

235 Cfg2() 

236 

237 def testSave(self): 

238 self.comp.r = "BBB" 

239 self.comp.p = "AAA" 

240 self.comp.c.f = 5. 

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

242 

243 roundTrip = Complex() 

244 roundTrip.load("roundtrip.test") 

245 os.remove("roundtrip.test") 

246 

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

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

249 

250 del roundTrip 

251 # test saving to an open file 

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

253 self.comp.saveToStream(outfile) 

254 outfile.close() 

255 

256 roundTrip = Complex() 

257 roundTrip.load("roundtrip.test") 

258 os.remove("roundtrip.test") 

259 

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

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

262 

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

264 # "config" 

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

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

267 outfile.close() 

268 

269 roundTrip = Complex() 

270 roundTrip.load("roundtrip.test") 

271 os.remove("roundtrip.test") 

272 

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

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

275 

276 def testDuplicateRegistryNames(self): 

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

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

279 

280 def testInheritance(self): 

281 class AAA(pexConfig.Config): 

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

283 

284 class BBB(AAA): 

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

286 

287 class CCC(BBB): 

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

289 

290 # test multi-level inheritance 

291 c = CCC() 

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

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

294 self.assertEqual(c.a, 4) 

295 

296 # test conflicting multiple inheritance 

297 class DDD(pexConfig.Config): 

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

299 

300 class EEE(DDD, AAA): 

301 pass 

302 

303 e = EEE() 

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

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

306 self.assertEqual(e.a, 0.0) 

307 

308 class FFF(AAA, DDD): 

309 pass 

310 f = FFF() 

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

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

313 self.assertEqual(f.a, 4) 

314 

315 # test inheritance from non Config objects 

316 class GGG: 

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

318 

319 class HHH(GGG, AAA): 

320 pass 

321 h = HHH() 

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

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

324 self.assertEqual(h.a, 10.0) 

325 

326 # test partial Field redefinition 

327 

328 class III(AAA): 

329 pass 

330 III.a.default = 5 

331 

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

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

334 

335 @unittest.skipIf(pexPolicy is None, "lsst.pex.policy is required") 

336 def testConvertPolicy(self): 

337 with self.assertWarns(FutureWarning): 

338 pol = pexConfig.makePolicy(self.simple) 

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

340 self.assertEqual(pol.get("f"), self.simple.f) 

341 self.assertEqual(pol.get("b"), self.simple.b) 

342 self.assertEqual(pol.get("c"), self.simple.c) 

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

344 

345 with self.assertWarns(FutureWarning): 

346 pol = pexConfig.makePolicy(self.comp) 

347 self.assertEqual(pol.get("c.f"), self.comp.c.f) 

348 

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

350 def testConvertPropertySet(self): 

351 ps = pexConfig.makePropertySet(self.simple) 

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

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

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

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

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

357 

358 ps = pexConfig.makePropertySet(self.comp) 

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

360 

361 def testFreeze(self): 

362 self.comp.freeze() 

363 

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

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

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

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

368 

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

370 self.comp.c.f = 5. 

371 

372 # Generate a Config through loading 

373 stream = io.StringIO() 

374 stream.write(str(importStatement)) 

375 self.comp.saveToStream(stream) 

376 roundtrip = Complex() 

377 roundtrip.loadFromStream(stream.getvalue()) 

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

379 

380 # Check the save stream 

381 stream = io.StringIO() 

382 roundtrip.saveToStream(stream) 

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

384 streamStr = stream.getvalue() 

385 if shouldBeThere: 

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

387 else: 

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

389 

390 def testImports(self): 

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

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

393 self.checkImportRoundTrip(importing, importing, True) 

394 

395 def testBadImports(self): 

396 dummy = "somethingThatDoesntExist" 

397 importing = """ 

398try: 

399 import %s 

400except ImportError: 

401 pass 

402""" % dummy 

403 self.checkImportRoundTrip(importing, dummy, False) 

404 

405 def testPickle(self): 

406 self.simple.f = 5 

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

408 self.assertIsInstance(simple, Simple) 

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

410 

411 self.comp.c.f = 5 

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

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()