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. 

38# Skip them if it is not found. 

39try: 

40 import lsst.daf.base as dafBase 

41except ImportError: 

42 dafBase = None 

43 

44GLOBAL_REGISTRY = {} 

45 

46 

47class Simple(pexConfig.Config): 

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

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

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

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

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

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

54 min=3.0, inclusiveMin=True) 

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

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

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

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

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

60 

61 

62GLOBAL_REGISTRY["AAA"] = Simple 

63 

64 

65class InnerConfig(pexConfig.Config): 

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

67 

68 

69GLOBAL_REGISTRY["BBB"] = InnerConfig 

70 

71 

72class OuterConfig(InnerConfig, pexConfig.Config): 

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

74 

75 def __init__(self): 

76 pexConfig.Config.__init__(self) 

77 self.i.f = 5.0 

78 

79 def validate(self): 

80 pexConfig.Config.validate(self) 

81 if self.i.f < 5: 

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

83 

84 

85class Complex(pexConfig.Config): 

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

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

88 default="AAA", optional=False) 

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

90 default="BBB", optional=True) 

91 

92 

93class Deprecation(pexConfig.Config): 

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

95 

96 

97class ConfigTest(unittest.TestCase): 

98 def setUp(self): 

99 self.simple = Simple() 

100 self.inner = InnerConfig() 

101 self.outer = OuterConfig() 

102 self.comp = Complex() 

103 self.deprecation = Deprecation() 

104 

105 def tearDown(self): 

106 del self.simple 

107 del self.inner 

108 del self.outer 

109 del self.comp 

110 

111 def testInit(self): 

112 self.assertIsNone(self.simple.i) 

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

114 self.assertFalse(self.simple.b) 

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

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

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

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

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

120 

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

122 

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

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

125 

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

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

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

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

130 

131 def testDeprecationWarning(self): 

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

133 """ 

134 with self.assertWarns(FutureWarning) as w: 

135 self.deprecation.old = 5 

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

137 

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

139 

140 def testDeprecationOutput(self): 

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

142 """ 

143 stream = io.StringIO() 

144 self.deprecation.saveToStream(stream) 

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

146 with self.assertWarns(FutureWarning): 

147 self.deprecation.old = 5 

148 stream = io.StringIO() 

149 self.deprecation.saveToStream(stream) 

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

151 

152 def testValidate(self): 

153 self.simple.validate() 

154 

155 self.inner.validate() 

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

157 self.outer.i.f = 10. 

158 self.outer.validate() 

159 

160 try: 

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

162 except pexConfig.FieldValidationError: 

163 pass 

164 except Exception: 

165 raise "Validation error Expected" 

166 self.simple.validate() 

167 

168 self.outer.i = InnerConfig 

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

170 self.outer.i = InnerConfig() 

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

172 

173 self.comp.validate() 

174 self.comp.r = None 

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

176 self.comp.r = "BBB" 

177 self.comp.validate() 

178 

179 def testRangeFieldConstructor(self): 

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

181 """ 

182 val = 3 

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

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

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

186 if inclusiveMin and inclusiveMax: 

187 # should not raise 

188 class Cfg1(pexConfig.Config): 

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

190 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

191 inclusiveMax=inclusiveMax) 

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

193 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

194 inclusiveMax=inclusiveMax) 

195 Cfg1() 

196 else: 

197 # raise while constructing the RangeField (hence cannot make 

198 # it part of a Config) 

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

200 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

201 inclusiveMax=inclusiveMax) 

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

203 default=val, min=val, max=val, inclusiveMin=inclusiveMin, 

204 inclusiveMax=inclusiveMax) 

205 

206 def testRangeFieldDefault(self): 

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

208 """ 

209 minVal = 3 

210 maxVal = 4 

211 for val, inclusiveMin, inclusiveMax, shouldRaise in ( 

212 (minVal, False, True, True), 

213 (minVal, True, True, False), 

214 (maxVal, True, False, True), 

215 (maxVal, True, True, False), 

216 ): 

217 class Cfg1(pexConfig.Config): 

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

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

220 inclusiveMin=inclusiveMin, inclusiveMax=inclusiveMax) 

221 

222 class Cfg2(pexConfig.Config): 

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

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

225 inclusiveMin=inclusiveMin, inclusiveMax=inclusiveMax) 

226 if shouldRaise: 

227 self.assertRaises(pexConfig.FieldValidationError, Cfg1) 

228 self.assertRaises(pexConfig.FieldValidationError, Cfg2) 

229 else: 

230 Cfg1() 

231 Cfg2() 

232 

233 def testSave(self): 

234 self.comp.r = "BBB" 

235 self.comp.p = "AAA" 

236 self.comp.c.f = 5. 

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

238 

239 roundTrip = Complex() 

240 roundTrip.load("roundtrip.test") 

241 os.remove("roundtrip.test") 

242 

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

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

245 

246 del roundTrip 

247 # test saving to an open file 

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

249 self.comp.saveToStream(outfile) 

250 outfile.close() 

251 

252 roundTrip = Complex() 

253 roundTrip.load("roundtrip.test") 

254 os.remove("roundtrip.test") 

255 

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

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

258 

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

260 # "config" 

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

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

263 outfile.close() 

264 

265 roundTrip = Complex() 

266 roundTrip.load("roundtrip.test") 

267 os.remove("roundtrip.test") 

268 

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

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

271 

272 def testDuplicateRegistryNames(self): 

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

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

275 

276 def testInheritance(self): 

277 class AAA(pexConfig.Config): 

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

279 

280 class BBB(AAA): 

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

282 

283 class CCC(BBB): 

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

285 

286 # test multi-level inheritance 

287 c = CCC() 

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

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

290 self.assertEqual(c.a, 4) 

291 

292 # test conflicting multiple inheritance 

293 class DDD(pexConfig.Config): 

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

295 

296 class EEE(DDD, AAA): 

297 pass 

298 

299 e = EEE() 

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

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

302 self.assertEqual(e.a, 0.0) 

303 

304 class FFF(AAA, DDD): 

305 pass 

306 f = FFF() 

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

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

309 self.assertEqual(f.a, 4) 

310 

311 # test inheritance from non Config objects 

312 class GGG: 

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

314 

315 class HHH(GGG, AAA): 

316 pass 

317 h = HHH() 

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

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

320 self.assertEqual(h.a, 10.0) 

321 

322 # test partial Field redefinition 

323 

324 class III(AAA): 

325 pass 

326 III.a.default = 5 

327 

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

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

330 

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

332 def testConvertPropertySet(self): 

333 ps = pexConfig.makePropertySet(self.simple) 

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

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

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

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

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

339 

340 ps = pexConfig.makePropertySet(self.comp) 

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

342 

343 def testFreeze(self): 

344 self.comp.freeze() 

345 

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

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

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

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

350 

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

352 self.comp.c.f = 5. 

353 

354 # Generate a Config through loading 

355 stream = io.StringIO() 

356 stream.write(str(importStatement)) 

357 self.comp.saveToStream(stream) 

358 roundtrip = Complex() 

359 roundtrip.loadFromStream(stream.getvalue()) 

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

361 

362 # Check the save stream 

363 stream = io.StringIO() 

364 roundtrip.saveToStream(stream) 

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

366 streamStr = stream.getvalue() 

367 if shouldBeThere: 

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

369 else: 

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

371 

372 def testImports(self): 

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

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

375 self.checkImportRoundTrip(importing, importing, True) 

376 

377 def testBadImports(self): 

378 dummy = "somethingThatDoesntExist" 

379 importing = """ 

380try: 

381 import %s 

382except ImportError: 

383 pass 

384""" % dummy 

385 self.checkImportRoundTrip(importing, dummy, False) 

386 

387 def testPickle(self): 

388 self.simple.f = 5 

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

390 self.assertIsInstance(simple, Simple) 

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

392 

393 self.comp.c.f = 5 

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

395 self.assertIsInstance(comp, Complex) 

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

397 

398 def testCompare(self): 

399 comp2 = Complex() 

400 inner2 = InnerConfig() 

401 simple2 = Simple() 

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

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

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

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

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

407 self.assertEqual(self.simple, simple2) 

408 self.assertEqual(simple2, self.simple) 

409 outList = [] 

410 

411 def outFunc(msg): 

412 outList.append(msg) 

413 simple2.b = True 

414 simple2.ll.append(4) 

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

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

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

418 del outList[:] 

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

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

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

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

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

424 del outList[:] 

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

426 self.simple.ll.append(5) 

427 self.simple.b = True 

428 self.simple.f += 1E8 

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

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

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

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

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

434 del outList[:] 

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

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

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

438 comp2.c.f = 1.0 

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

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

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

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

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

444 

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

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

447 # Before DM-16561, this raised. 

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

449 

450 def testLoadError(self): 

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

452 propagate. 

453 """ 

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

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

456 

457 def testNames(self): 

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

459 

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

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

462 """ 

463 

464 names = self.simple.names() 

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

466 for name in names: 

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

468 

469 

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

471 unittest.main()