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
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
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/>.
28import io
29import itertools
30import re
31import os
32import pickle
33import unittest
35try:
36 import yaml
37except ImportError:
38 yaml = None
40import lsst.pex.config as pexConfig
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
49GLOBAL_REGISTRY = {}
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"))
67GLOBAL_REGISTRY["AAA"] = Simple
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
74GLOBAL_REGISTRY["BBB"] = InnerConfig
77class OuterConfig(InnerConfig, pexConfig.Config):
78 i = pexConfig.ConfigField("Outer.i", InnerConfig)
80 def __init__(self):
81 pexConfig.Config.__init__(self)
82 self.i.f = 5.0
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")
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)
98class Deprecation(pexConfig.Config):
99 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!")
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()
110 def tearDown(self):
111 del self.simple
112 del self.inner
113 del self.outer
114 del self.comp
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)
126 self.assertEqual(self.deprecation._fields['old'].doc, "Something. Deprecated: not used!")
128 self.assertEqual(self.outer.i.f, 5.0)
129 self.assertEqual(self.outer.f, 0.0)
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)
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)
143 self.assertIn(self.deprecation._fields['old'].deprecated, str(w.warnings[-1].message))
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())
157 def testValidate(self):
158 self.simple.validate()
160 self.inner.validate()
161 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5)
162 self.outer.i.f = 10.
163 self.outer.validate()
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()
173 self.outer.i = InnerConfig
174 self.assertRaises(ValueError, self.outer.validate)
175 self.outer.i = InnerConfig()
176 self.assertRaises(ValueError, self.outer.validate)
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()
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)
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)
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()
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")
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
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
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
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)
280 def testDuplicateRegistryNames(self):
281 self.comp.r["AAA"].f = 5.0
282 self.assertEqual(self.comp.p["AAA"].f, 3.0)
284 def testInheritance(self):
285 class AAA(pexConfig.Config):
286 a = pexConfig.Field("AAA.a", int, default=4)
288 class BBB(AAA):
289 b = pexConfig.Field("BBB.b", int, default=3)
291 class CCC(BBB):
292 c = pexConfig.Field("CCC.c", int, default=2)
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)
300 # test conflicting multiple inheritance
301 class DDD(pexConfig.Config):
302 a = pexConfig.Field("DDD.a", float, default=0.0)
304 class EEE(DDD, AAA):
305 pass
307 e = EEE()
308 self.assertEqual(e._fields["a"].dtype, float)
309 self.assertIn("a", e.toDict())
310 self.assertEqual(e.a, 0.0)
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)
319 # test inheritance from non Config objects
320 class GGG:
321 a = pexConfig.Field("AAA.a", float, default=10.)
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)
330 # test partial Field redefinition
332 class III(AAA):
333 pass
334 III.a.default = 5
336 self.assertEqual(III.a.default, 5)
337 self.assertEqual(AAA.a.default, 4)
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))
348 ps = pexConfig.makePropertySet(self.comp)
349 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
351 def testFreeze(self):
352 self.comp.freeze()
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)
359 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
360 self.comp.c.f = 5.
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)
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))
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)
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)
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)
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)
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)
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)
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 = []
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)
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))
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")
478 def testNames(self):
479 """Check that the names() method returns valid keys.
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 """
485 names = self.simple.names()
486 self.assertEqual(len(names), 8)
487 for name in names:
488 self.assertTrue(hasattr(self.simple, name))
491if __name__ == "__main__": 491 ↛ 492line 491 didn't jump to line 492, because the condition on line 491 was never true
492 unittest.main()