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")
248 self.assertEqual(self.comp.c.f, roundTrip.c.f)
249 self.assertEqual(self.comp.r.name, roundTrip.r.name)
251 del roundTrip
252 # test saving to an open file
253 outfile = open("roundtrip.test", "w")
254 self.comp.saveToStream(outfile)
255 outfile.close()
257 roundTrip = Complex()
258 roundTrip.load("roundtrip.test")
259 os.remove("roundtrip.test")
261 self.assertEqual(self.comp.c.f, roundTrip.c.f)
262 self.assertEqual(self.comp.r.name, roundTrip.r.name)
264 # test backwards compatibility feature of allowing "root" instead of
265 # "config"
266 outfile = open("roundtrip.test", "w")
267 self.comp.saveToStream(outfile, root="root")
268 outfile.close()
270 roundTrip = Complex()
271 roundTrip.load("roundtrip.test")
272 os.remove("roundtrip.test")
274 self.assertEqual(self.comp.c.f, roundTrip.c.f)
275 self.assertEqual(self.comp.r.name, roundTrip.r.name)
277 def testDuplicateRegistryNames(self):
278 self.comp.r["AAA"].f = 5.0
279 self.assertEqual(self.comp.p["AAA"].f, 3.0)
281 def testInheritance(self):
282 class AAA(pexConfig.Config):
283 a = pexConfig.Field("AAA.a", int, default=4)
285 class BBB(AAA):
286 b = pexConfig.Field("BBB.b", int, default=3)
288 class CCC(BBB):
289 c = pexConfig.Field("CCC.c", int, default=2)
291 # test multi-level inheritance
292 c = CCC()
293 self.assertIn("a", c.toDict())
294 self.assertEqual(c._fields["a"].dtype, int)
295 self.assertEqual(c.a, 4)
297 # test conflicting multiple inheritance
298 class DDD(pexConfig.Config):
299 a = pexConfig.Field("DDD.a", float, default=0.0)
301 class EEE(DDD, AAA):
302 pass
304 e = EEE()
305 self.assertEqual(e._fields["a"].dtype, float)
306 self.assertIn("a", e.toDict())
307 self.assertEqual(e.a, 0.0)
309 class FFF(AAA, DDD):
310 pass
311 f = FFF()
312 self.assertEqual(f._fields["a"].dtype, int)
313 self.assertIn("a", f.toDict())
314 self.assertEqual(f.a, 4)
316 # test inheritance from non Config objects
317 class GGG:
318 a = pexConfig.Field("AAA.a", float, default=10.)
320 class HHH(GGG, AAA):
321 pass
322 h = HHH()
323 self.assertEqual(h._fields["a"].dtype, float)
324 self.assertIn("a", h.toDict())
325 self.assertEqual(h.a, 10.0)
327 # test partial Field redefinition
329 class III(AAA):
330 pass
331 III.a.default = 5
333 self.assertEqual(III.a.default, 5)
334 self.assertEqual(AAA.a.default, 4)
336 @unittest.skipIf(dafBase is None, "lsst.daf.base is required")
337 def testConvertPropertySet(self):
338 ps = pexConfig.makePropertySet(self.simple)
339 self.assertFalse(ps.exists("i"))
340 self.assertEqual(ps.getScalar("f"), self.simple.f)
341 self.assertEqual(ps.getScalar("b"), self.simple.b)
342 self.assertEqual(ps.getScalar("c"), self.simple.c)
343 self.assertEqual(list(ps.getArray("ll")), list(self.simple.ll))
345 ps = pexConfig.makePropertySet(self.comp)
346 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
348 def testFreeze(self):
349 self.comp.freeze()
351 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.c, "f", 10.0)
352 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "r", "AAA")
353 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "p", "AAA")
354 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.p["AAA"], "f", 5.0)
356 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
357 self.comp.c.f = 5.
359 # Generate a Config through loading
360 stream = io.StringIO()
361 stream.write(str(importStatement))
362 self.comp.saveToStream(stream)
363 roundtrip = Complex()
364 roundtrip.loadFromStream(stream.getvalue())
365 self.assertEqual(self.comp.c.f, roundtrip.c.f)
367 # Check the save stream
368 stream = io.StringIO()
369 roundtrip.saveToStream(stream)
370 self.assertEqual(self.comp.c.f, roundtrip.c.f)
371 streamStr = stream.getvalue()
372 if shouldBeThere:
373 self.assertTrue(re.search(searchString, streamStr))
374 else:
375 self.assertFalse(re.search(searchString, streamStr))
377 def testImports(self):
378 # A module not used by anything else, but which exists
379 importing = "import lsst.pex.config._doNotImportMe\n"
380 self.checkImportRoundTrip(importing, importing, True)
382 def testBadImports(self):
383 dummy = "somethingThatDoesntExist"
384 importing = """
385try:
386 import %s
387except ImportError:
388 pass
389""" % dummy
390 self.checkImportRoundTrip(importing, dummy, False)
392 def testPickle(self):
393 self.simple.f = 5
394 simple = pickle.loads(pickle.dumps(self.simple))
395 self.assertIsInstance(simple, Simple)
396 self.assertEqual(self.simple.f, simple.f)
398 self.comp.c.f = 5
399 comp = pickle.loads(pickle.dumps(self.comp))
400 self.assertIsInstance(comp, Complex)
401 self.assertEqual(self.comp.c.f, comp.c.f)
403 @unittest.skipIf(yaml is None, "Test requires pyyaml")
404 def testYaml(self):
405 self.simple.f = 5
406 simple = yaml.safe_load(yaml.dump(self.simple))
407 self.assertIsInstance(simple, Simple)
408 self.assertEqual(self.simple.f, simple.f)
410 self.comp.c.f = 5
411 # Use a different loader to check that it also works
412 comp = yaml.load(yaml.dump(self.comp), Loader=yaml.FullLoader)
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()