Coverage for tests/test_Config.py : 18%

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/>.
28import io
29import itertools
30import re
31import os
32import pickle
33import unittest
35import lsst.pex.config as pexConfig
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
48GLOBAL_REGISTRY = {}
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"))
66GLOBAL_REGISTRY["AAA"] = Simple
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
73GLOBAL_REGISTRY["BBB"] = InnerConfig
76class OuterConfig(InnerConfig, pexConfig.Config):
77 i = pexConfig.ConfigField("Outer.i", InnerConfig)
79 def __init__(self):
80 pexConfig.Config.__init__(self)
81 self.i.f = 5.0
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")
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)
97class Deprecation(pexConfig.Config):
98 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!")
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()
109 def tearDown(self):
110 del self.simple
111 del self.inner
112 del self.outer
113 del self.comp
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)
125 self.assertEqual(self.deprecation._fields['old'].doc, "Something. Deprecated: not used!")
127 self.assertEqual(self.outer.i.f, 5.0)
128 self.assertEqual(self.outer.f, 0.0)
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)
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)
142 self.assertIn(self.deprecation._fields['old'].deprecated, str(w.warnings[-1].message))
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())
156 def testValidate(self):
157 self.simple.validate()
159 self.inner.validate()
160 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5)
161 self.outer.i.f = 10.
162 self.outer.validate()
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()
172 self.outer.i = InnerConfig
173 self.assertRaises(ValueError, self.outer.validate)
174 self.outer.i = InnerConfig()
175 self.assertRaises(ValueError, self.outer.validate)
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()
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)
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)
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()
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")
243 roundTrip = Complex()
244 roundTrip.load("roundtrip.test")
245 os.remove("roundtrip.test")
247 self.assertEqual(self.comp.c.f, roundTrip.c.f)
248 self.assertEqual(self.comp.r.name, roundTrip.r.name)
250 del roundTrip
251 # test saving to an open file
252 outfile = open("roundtrip.test", "w")
253 self.comp.saveToStream(outfile)
254 outfile.close()
256 roundTrip = Complex()
257 roundTrip.load("roundtrip.test")
258 os.remove("roundtrip.test")
260 self.assertEqual(self.comp.c.f, roundTrip.c.f)
261 self.assertEqual(self.comp.r.name, roundTrip.r.name)
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()
269 roundTrip = Complex()
270 roundTrip.load("roundtrip.test")
271 os.remove("roundtrip.test")
273 self.assertEqual(self.comp.c.f, roundTrip.c.f)
274 self.assertEqual(self.comp.r.name, roundTrip.r.name)
276 def testDuplicateRegistryNames(self):
277 self.comp.r["AAA"].f = 5.0
278 self.assertEqual(self.comp.p["AAA"].f, 3.0)
280 def testInheritance(self):
281 class AAA(pexConfig.Config):
282 a = pexConfig.Field("AAA.a", int, default=4)
284 class BBB(AAA):
285 b = pexConfig.Field("BBB.b", int, default=3)
287 class CCC(BBB):
288 c = pexConfig.Field("CCC.c", int, default=2)
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)
296 # test conflicting multiple inheritance
297 class DDD(pexConfig.Config):
298 a = pexConfig.Field("DDD.a", float, default=0.0)
300 class EEE(DDD, AAA):
301 pass
303 e = EEE()
304 self.assertEqual(e._fields["a"].dtype, float)
305 self.assertIn("a", e.toDict())
306 self.assertEqual(e.a, 0.0)
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)
315 # test inheritance from non Config objects
316 class GGG:
317 a = pexConfig.Field("AAA.a", float, default=10.)
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)
326 # test partial Field redefinition
328 class III(AAA):
329 pass
330 III.a.default = 5
332 self.assertEqual(III.a.default, 5)
333 self.assertEqual(AAA.a.default, 4)
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))
345 with self.assertWarns(FutureWarning):
346 pol = pexConfig.makePolicy(self.comp)
347 self.assertEqual(pol.get("c.f"), self.comp.c.f)
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))
358 ps = pexConfig.makePropertySet(self.comp)
359 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
361 def testFreeze(self):
362 self.comp.freeze()
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)
369 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
370 self.comp.c.f = 5.
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)
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))
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)
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)
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)
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)
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 = []
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)
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))
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")
475 def testNames(self):
476 """Check that the names() method returns valid keys.
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 """
482 names = self.simple.names()
483 self.assertEqual(len(names), 8)
484 for name in names:
485 self.assertTrue(hasattr(self.simple, name))
488if __name__ == "__main__": 488 ↛ 489line 488 didn't jump to line 489, because the condition on line 488 was never true
489 unittest.main()