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.
38# Skip them if it is not found.
39try:
40 import lsst.daf.base as dafBase
41except ImportError:
42 dafBase = None
44GLOBAL_REGISTRY = {}
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"))
62GLOBAL_REGISTRY["AAA"] = Simple
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
69GLOBAL_REGISTRY["BBB"] = InnerConfig
72class OuterConfig(InnerConfig, pexConfig.Config):
73 i = pexConfig.ConfigField("Outer.i", InnerConfig)
75 def __init__(self):
76 pexConfig.Config.__init__(self)
77 self.i.f = 5.0
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")
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)
93class Deprecation(pexConfig.Config):
94 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!")
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()
105 def tearDown(self):
106 del self.simple
107 del self.inner
108 del self.outer
109 del self.comp
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)
121 self.assertEqual(self.deprecation._fields['old'].doc, "Something. Deprecated: not used!")
123 self.assertEqual(self.outer.i.f, 5.0)
124 self.assertEqual(self.outer.f, 0.0)
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)
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)
138 self.assertIn(self.deprecation._fields['old'].deprecated, str(w.warnings[-1].message))
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())
152 def testValidate(self):
153 self.simple.validate()
155 self.inner.validate()
156 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5)
157 self.outer.i.f = 10.
158 self.outer.validate()
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()
168 self.outer.i = InnerConfig
169 self.assertRaises(ValueError, self.outer.validate)
170 self.outer.i = InnerConfig()
171 self.assertRaises(ValueError, self.outer.validate)
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()
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)
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)
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()
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")
239 roundTrip = Complex()
240 roundTrip.load("roundtrip.test")
241 os.remove("roundtrip.test")
243 self.assertEqual(self.comp.c.f, roundTrip.c.f)
244 self.assertEqual(self.comp.r.name, roundTrip.r.name)
246 del roundTrip
247 # test saving to an open file
248 outfile = open("roundtrip.test", "w")
249 self.comp.saveToStream(outfile)
250 outfile.close()
252 roundTrip = Complex()
253 roundTrip.load("roundtrip.test")
254 os.remove("roundtrip.test")
256 self.assertEqual(self.comp.c.f, roundTrip.c.f)
257 self.assertEqual(self.comp.r.name, roundTrip.r.name)
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()
265 roundTrip = Complex()
266 roundTrip.load("roundtrip.test")
267 os.remove("roundtrip.test")
269 self.assertEqual(self.comp.c.f, roundTrip.c.f)
270 self.assertEqual(self.comp.r.name, roundTrip.r.name)
272 def testDuplicateRegistryNames(self):
273 self.comp.r["AAA"].f = 5.0
274 self.assertEqual(self.comp.p["AAA"].f, 3.0)
276 def testInheritance(self):
277 class AAA(pexConfig.Config):
278 a = pexConfig.Field("AAA.a", int, default=4)
280 class BBB(AAA):
281 b = pexConfig.Field("BBB.b", int, default=3)
283 class CCC(BBB):
284 c = pexConfig.Field("CCC.c", int, default=2)
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)
292 # test conflicting multiple inheritance
293 class DDD(pexConfig.Config):
294 a = pexConfig.Field("DDD.a", float, default=0.0)
296 class EEE(DDD, AAA):
297 pass
299 e = EEE()
300 self.assertEqual(e._fields["a"].dtype, float)
301 self.assertIn("a", e.toDict())
302 self.assertEqual(e.a, 0.0)
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)
311 # test inheritance from non Config objects
312 class GGG:
313 a = pexConfig.Field("AAA.a", float, default=10.)
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)
322 # test partial Field redefinition
324 class III(AAA):
325 pass
326 III.a.default = 5
328 self.assertEqual(III.a.default, 5)
329 self.assertEqual(AAA.a.default, 4)
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))
340 ps = pexConfig.makePropertySet(self.comp)
341 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
343 def testFreeze(self):
344 self.comp.freeze()
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)
351 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
352 self.comp.c.f = 5.
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)
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))
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)
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)
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)
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)
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 = []
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)
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))
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")
457 def testNames(self):
458 """Check that the names() method returns valid keys.
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 """
464 names = self.simple.names()
465 self.assertEqual(len(names), 8)
466 for name in names:
467 self.assertTrue(hasattr(self.simple, name))
470if __name__ == "__main__": 470 ↛ 471line 470 didn't jump to line 471, because the condition on line 470 was never true
471 unittest.main()