Coverage for tests/test_Config.py: 17%
344 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-04 10:48 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-04 10:48 +0000
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 os
31import pickle
32import re
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(
57 "choice test", str, default="Hello", allowed={"Hello": "First choice", "World": "second choice"}
58 )
59 r = pexConfig.RangeField("Range test", float, default=3.0, optional=False, min=3.0, inclusiveMin=True)
60 ll = pexConfig.ListField( 60 ↛ exitline 60 didn't jump to the function exit
61 "list test", int, default=[1, 2, 3], maxLength=5, itemCheck=lambda x: x is not None and x > 0
62 )
63 d = pexConfig.DictField( 63 ↛ exitline 63 didn't jump to the function exit
64 "dict test", str, str, default={"key": "value"}, itemCheck=lambda x: x.startswith("v")
65 )
66 n = pexConfig.Field("nan test", float, default=float("NAN"))
69GLOBAL_REGISTRY["AAA"] = Simple
72class InnerConfig(pexConfig.Config):
73 f = pexConfig.Field("Inner.f", float, default=0.0, check=lambda x: x >= 0, optional=False) 73 ↛ exitline 73 didn't run the lambda on line 73
76GLOBAL_REGISTRY["BBB"] = InnerConfig
79class OuterConfig(InnerConfig, pexConfig.Config):
80 i = pexConfig.ConfigField("Outer.i", InnerConfig)
82 def __init__(self):
83 pexConfig.Config.__init__(self)
84 self.i.f = 5.0
86 def validate(self):
87 pexConfig.Config.validate(self)
88 if self.i.f < 5:
89 raise ValueError("validation failed, outer.i.f must be greater than 5")
92class Complex(pexConfig.Config):
93 c = pexConfig.ConfigField("an inner config", InnerConfig)
94 r = pexConfig.ConfigChoiceField(
95 "a registry field", typemap=GLOBAL_REGISTRY, default="AAA", optional=False
96 )
97 p = pexConfig.ConfigChoiceField("another registry", typemap=GLOBAL_REGISTRY, default="BBB", optional=True)
100class Deprecation(pexConfig.Config):
101 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!")
104class ConfigTest(unittest.TestCase):
105 def setUp(self):
106 self.simple = Simple()
107 self.inner = InnerConfig()
108 self.outer = OuterConfig()
109 self.comp = Complex()
110 self.deprecation = Deprecation()
112 def tearDown(self):
113 del self.simple
114 del self.inner
115 del self.outer
116 del self.comp
118 def testFieldTypeAnnotationRuntime(self):
119 # test parsing type annotation for runtime dtype
120 testField = pexConfig.Field[str](doc="")
121 self.assertEqual(testField.dtype, str)
123 # verify that forward references work correctly
124 testField = pexConfig.Field["float"](doc="")
125 self.assertEqual(testField.dtype, float)
127 # verify that Field rejects multiple types
128 with self.assertRaises(ValueError):
129 pexConfig.Field[str, int](doc="") # type: ignore
131 # verify that Field raises in conflict with dtype:
132 with self.assertRaises(ValueError):
133 pexConfig.Field[str](doc="", dtype=int)
135 # verify that Field does not raise if dtype agrees
136 testField = pexConfig.Field[int](doc="", dtype=int)
137 self.assertEqual(testField.dtype, int)
139 def testInit(self):
140 self.assertIsNone(self.simple.i)
141 self.assertEqual(self.simple.f, 3.0)
142 self.assertFalse(self.simple.b)
143 self.assertEqual(self.simple.c, "Hello")
144 self.assertEqual(list(self.simple.ll), [1, 2, 3])
145 self.assertEqual(self.simple.d["key"], "value")
146 self.assertEqual(self.inner.f, 0.0)
147 self.assertEqual(self.deprecation.old, 10)
149 self.assertEqual(self.deprecation._fields["old"].doc, "Something. Deprecated: not used!")
151 self.assertEqual(self.outer.i.f, 5.0)
152 self.assertEqual(self.outer.f, 0.0)
154 self.assertEqual(self.comp.c.f, 0.0)
155 self.assertEqual(self.comp.r.name, "AAA")
156 self.assertEqual(self.comp.r.active.f, 3.0)
157 self.assertEqual(self.comp.r["BBB"].f, 0.0)
159 def testDeprecationWarning(self):
160 """Test that a deprecated field emits a warning when it is set."""
161 with self.assertWarns(FutureWarning) as w:
162 self.deprecation.old = 5
163 self.assertEqual(self.deprecation.old, 5)
165 self.assertIn(self.deprecation._fields["old"].deprecated, str(w.warnings[-1].message))
167 def testDeprecationOutput(self):
168 """Test that a deprecated field is not written out unless it is set."""
169 stream = io.StringIO()
170 self.deprecation.saveToStream(stream)
171 self.assertNotIn("config.old", stream.getvalue())
172 with self.assertWarns(FutureWarning):
173 self.deprecation.old = 5
174 stream = io.StringIO()
175 self.deprecation.saveToStream(stream)
176 self.assertIn("config.old=5\n", stream.getvalue())
178 def testValidate(self):
179 self.simple.validate()
181 self.inner.validate()
182 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5)
183 self.outer.i.f = 10.0
184 self.outer.validate()
186 try:
187 self.simple.d["failKey"] = "failValue"
188 except pexConfig.FieldValidationError:
189 pass
190 except Exception:
191 raise "Validation error Expected"
192 self.simple.validate()
194 self.outer.i = InnerConfig
195 self.assertRaises(ValueError, self.outer.validate)
196 self.outer.i = InnerConfig()
197 self.assertRaises(ValueError, self.outer.validate)
199 self.comp.validate()
200 self.comp.r = None
201 self.assertRaises(ValueError, self.comp.validate)
202 self.comp.r = "BBB"
203 self.comp.validate()
205 def testRangeFieldConstructor(self):
206 """Test RangeField constructor's checking of min, max"""
207 val = 3
208 self.assertRaises(ValueError, pexConfig.RangeField, "", int, default=val, min=val, max=val - 1)
209 self.assertRaises(ValueError, pexConfig.RangeField, "", float, default=val, min=val, max=val - 1e-15)
210 for inclusiveMin, inclusiveMax in itertools.product((False, True), (False, True)):
211 if inclusiveMin and inclusiveMax:
212 # should not raise
213 class Cfg1(pexConfig.Config):
214 r1 = pexConfig.RangeField(
215 doc="",
216 dtype=int,
217 default=val,
218 min=val,
219 max=val,
220 inclusiveMin=inclusiveMin,
221 inclusiveMax=inclusiveMax,
222 )
223 r2 = pexConfig.RangeField(
224 doc="",
225 dtype=float,
226 default=val,
227 min=val,
228 max=val,
229 inclusiveMin=inclusiveMin,
230 inclusiveMax=inclusiveMax,
231 )
233 Cfg1()
234 else:
235 # raise while constructing the RangeField (hence cannot make
236 # it part of a Config)
237 self.assertRaises(
238 ValueError,
239 pexConfig.RangeField,
240 doc="",
241 dtype=int,
242 default=val,
243 min=val,
244 max=val,
245 inclusiveMin=inclusiveMin,
246 inclusiveMax=inclusiveMax,
247 )
248 self.assertRaises(
249 ValueError,
250 pexConfig.RangeField,
251 doc="",
252 dtype=float,
253 default=val,
254 min=val,
255 max=val,
256 inclusiveMin=inclusiveMin,
257 inclusiveMax=inclusiveMax,
258 )
260 def testRangeFieldDefault(self):
261 """Test RangeField's checking of the default value"""
262 minVal = 3
263 maxVal = 4
264 for val, inclusiveMin, inclusiveMax, shouldRaise in (
265 (minVal, False, True, True),
266 (minVal, True, True, False),
267 (maxVal, True, False, True),
268 (maxVal, True, True, False),
269 ):
271 class Cfg1(pexConfig.Config):
272 r = pexConfig.RangeField(
273 doc="",
274 dtype=int,
275 default=val,
276 min=minVal,
277 max=maxVal,
278 inclusiveMin=inclusiveMin,
279 inclusiveMax=inclusiveMax,
280 )
282 class Cfg2(pexConfig.Config):
283 r2 = pexConfig.RangeField(
284 doc="",
285 dtype=float,
286 default=val,
287 min=minVal,
288 max=maxVal,
289 inclusiveMin=inclusiveMin,
290 inclusiveMax=inclusiveMax,
291 )
293 if shouldRaise:
294 self.assertRaises(pexConfig.FieldValidationError, Cfg1)
295 self.assertRaises(pexConfig.FieldValidationError, Cfg2)
296 else:
297 Cfg1()
298 Cfg2()
300 def testSave(self):
301 self.comp.r = "BBB"
302 self.comp.p = "AAA"
303 self.comp.c.f = 5.0
304 self.comp.save("roundtrip.test")
306 roundTrip = Complex()
307 roundTrip.load("roundtrip.test")
308 os.remove("roundtrip.test")
309 self.assertEqual(self.comp.c.f, roundTrip.c.f)
310 self.assertEqual(self.comp.r.name, roundTrip.r.name)
311 del roundTrip
313 # test saving to an open file
314 with open("roundtrip.test", "w") as outfile:
315 self.comp.saveToStream(outfile)
316 roundTrip = Complex()
317 with open("roundtrip.test", "r") as infile:
318 roundTrip.loadFromStream(infile)
319 os.remove("roundtrip.test")
320 self.assertEqual(self.comp.c.f, roundTrip.c.f)
321 self.assertEqual(self.comp.r.name, roundTrip.r.name)
322 del roundTrip
324 # test saving to a string.
325 saved_string = self.comp.saveToString()
326 roundTrip = Complex()
327 roundTrip.loadFromString(saved_string)
328 self.assertEqual(self.comp.c.f, roundTrip.c.f)
329 self.assertEqual(self.comp.r.name, roundTrip.r.name)
330 del roundTrip
332 # Test an override of the default variable name.
333 with open("roundtrip.test", "w") as outfile:
334 self.comp.saveToStream(outfile, root="root")
335 roundTrip = Complex()
336 with self.assertRaises(NameError):
337 roundTrip.load("roundtrip.test")
338 roundTrip.load("roundtrip.test", root="root")
339 os.remove("roundtrip.test")
340 self.assertEqual(self.comp.c.f, roundTrip.c.f)
341 self.assertEqual(self.comp.r.name, roundTrip.r.name)
343 def testDuplicateRegistryNames(self):
344 self.comp.r["AAA"].f = 5.0
345 self.assertEqual(self.comp.p["AAA"].f, 3.0)
347 def testInheritance(self):
348 class AAA(pexConfig.Config):
349 a = pexConfig.Field("AAA.a", int, default=4)
351 class BBB(AAA):
352 b = pexConfig.Field("BBB.b", int, default=3)
354 class CCC(BBB):
355 c = pexConfig.Field("CCC.c", int, default=2)
357 # test multi-level inheritance
358 c = CCC()
359 self.assertIn("a", c.toDict())
360 self.assertEqual(c._fields["a"].dtype, int)
361 self.assertEqual(c.a, 4)
363 # test conflicting multiple inheritance
364 class DDD(pexConfig.Config):
365 a = pexConfig.Field("DDD.a", float, default=0.0)
367 class EEE(DDD, AAA):
368 pass
370 e = EEE()
371 self.assertEqual(e._fields["a"].dtype, float)
372 self.assertIn("a", e.toDict())
373 self.assertEqual(e.a, 0.0)
375 class FFF(AAA, DDD):
376 pass
378 f = FFF()
379 self.assertEqual(f._fields["a"].dtype, int)
380 self.assertIn("a", f.toDict())
381 self.assertEqual(f.a, 4)
383 # test inheritance from non Config objects
384 class GGG:
385 a = pexConfig.Field("AAA.a", float, default=10.0)
387 class HHH(GGG, AAA):
388 pass
390 h = HHH()
391 self.assertEqual(h._fields["a"].dtype, float)
392 self.assertIn("a", h.toDict())
393 self.assertEqual(h.a, 10.0)
395 # test partial Field redefinition
397 class III(AAA):
398 pass
400 III.a.default = 5
402 self.assertEqual(III.a.default, 5)
403 self.assertEqual(AAA.a.default, 4)
405 @unittest.skipIf(dafBase is None, "lsst.daf.base is required")
406 def testConvertPropertySet(self):
407 ps = pexConfig.makePropertySet(self.simple)
408 self.assertFalse(ps.exists("i"))
409 self.assertEqual(ps.getScalar("f"), self.simple.f)
410 self.assertEqual(ps.getScalar("b"), self.simple.b)
411 self.assertEqual(ps.getScalar("c"), self.simple.c)
412 self.assertEqual(list(ps.getArray("ll")), list(self.simple.ll))
414 ps = pexConfig.makePropertySet(self.comp)
415 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
417 def testFreeze(self):
418 self.comp.freeze()
420 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.c, "f", 10.0)
421 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "r", "AAA")
422 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "p", "AAA")
423 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.p["AAA"], "f", 5.0)
425 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
426 self.comp.c.f = 5.0
428 # Generate a Config through loading
429 stream = io.StringIO()
430 stream.write(str(importStatement))
431 self.comp.saveToStream(stream)
432 roundtrip = Complex()
433 roundtrip.loadFromStream(stream.getvalue())
434 self.assertEqual(self.comp.c.f, roundtrip.c.f)
436 # Check the save stream
437 stream = io.StringIO()
438 roundtrip.saveToStream(stream)
439 self.assertEqual(self.comp.c.f, roundtrip.c.f)
440 streamStr = stream.getvalue()
441 if shouldBeThere:
442 self.assertTrue(re.search(searchString, streamStr))
443 else:
444 self.assertFalse(re.search(searchString, streamStr))
446 def testImports(self):
447 # A module not used by anything else, but which exists
448 importing = "import lsst.pex.config._doNotImportMe\n"
449 self.checkImportRoundTrip(importing, importing, True)
451 def testBadImports(self):
452 dummy = "somethingThatDoesntExist"
453 importing = (
454 """
455try:
456 import %s
457except ImportError:
458 pass
459"""
460 % dummy
461 )
462 self.checkImportRoundTrip(importing, dummy, False)
464 def testPickle(self):
465 self.simple.f = 5
466 simple = pickle.loads(pickle.dumps(self.simple))
467 self.assertIsInstance(simple, Simple)
468 self.assertEqual(self.simple.f, simple.f)
470 self.comp.c.f = 5
471 comp = pickle.loads(pickle.dumps(self.comp))
472 self.assertIsInstance(comp, Complex)
473 self.assertEqual(self.comp.c.f, comp.c.f)
475 @unittest.skipIf(yaml is None, "Test requires pyyaml")
476 def testYaml(self):
477 self.simple.f = 5
478 simple = yaml.safe_load(yaml.dump(self.simple))
479 self.assertIsInstance(simple, Simple)
480 self.assertEqual(self.simple.f, simple.f)
482 self.comp.c.f = 5
483 # Use a different loader to check that it also works
484 comp = yaml.load(yaml.dump(self.comp), Loader=yaml.FullLoader)
485 self.assertIsInstance(comp, Complex)
486 self.assertEqual(self.comp.c.f, comp.c.f)
488 def testCompare(self):
489 comp2 = Complex()
490 inner2 = InnerConfig()
491 simple2 = Simple()
492 self.assertTrue(self.comp.compare(comp2))
493 self.assertTrue(comp2.compare(self.comp))
494 self.assertTrue(self.comp.c.compare(inner2))
495 self.assertTrue(self.simple.compare(simple2))
496 self.assertTrue(simple2.compare(self.simple))
497 self.assertEqual(self.simple, simple2)
498 self.assertEqual(simple2, self.simple)
499 outList = []
501 def outFunc(msg):
502 outList.append(msg)
504 simple2.b = True
505 simple2.ll.append(4)
506 simple2.d["foo"] = "var"
507 self.assertFalse(self.simple.compare(simple2, shortcut=True, output=outFunc))
508 self.assertEqual(len(outList), 1)
509 del outList[:]
510 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc))
511 output = "\n".join(outList)
512 self.assertIn("Inequality in b", output)
513 self.assertIn("Inequality in size for ll", output)
514 self.assertIn("Inequality in keys for d", output)
515 del outList[:]
516 self.simple.d["foo"] = "vast"
517 self.simple.ll.append(5)
518 self.simple.b = True
519 self.simple.f += 1e8
520 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc))
521 output = "\n".join(outList)
522 self.assertIn("Inequality in f", output)
523 self.assertIn("Inequality in ll[3]", output)
524 self.assertIn("Inequality in d['foo']", output)
525 del outList[:]
526 comp2.r["BBB"].f = 1.0 # changing the non-selected item shouldn't break equality
527 self.assertTrue(self.comp.compare(comp2))
528 comp2.r["AAA"].i = 56 # changing the selected item should break equality
529 comp2.c.f = 1.0
530 self.assertFalse(self.comp.compare(comp2, shortcut=False, output=outFunc))
531 output = "\n".join(outList)
532 self.assertIn("Inequality in c.f", output)
533 self.assertIn("Inequality in r['AAA']", output)
534 self.assertNotIn("Inequality in r['BBB']", output)
536 # Before DM-16561, this incorrectly returned `True`.
537 self.assertFalse(self.inner.compare(self.outer))
538 # Before DM-16561, this raised.
539 self.assertFalse(self.outer.compare(self.inner))
541 def testLoadError(self):
542 """Check that loading allows errors in the file being loaded to
543 propagate.
544 """
545 self.assertRaises(SyntaxError, self.simple.loadFromStream, "bork bork bork")
546 self.assertRaises(NameError, self.simple.loadFromStream, "config.f = bork")
548 def testNames(self):
549 """Check that the names() method returns valid keys.
551 Also check that we have the right number of keys, and as they are
552 all known to be valid we know that we got them all.
553 """
555 names = self.simple.names()
556 self.assertEqual(len(names), 8)
557 for name in names:
558 self.assertTrue(hasattr(self.simple, name))
560 def testIteration(self):
561 self.assertIn("ll", self.simple)
562 self.assertIn("ll", self.simple.keys())
563 self.assertIn("Hello", self.simple.values())
564 self.assertEqual(len(self.simple.values()), 8)
566 for k, v, (k1, v1) in zip(self.simple.keys(), self.simple.values(), self.simple.items()):
567 self.assertEqual(k, k1)
568 if k == "n":
569 self.assertNotEqual(v, v1)
570 else:
571 self.assertEqual(v, v1)
574if __name__ == "__main__": 574 ↛ 575line 574 didn't jump to line 575, because the condition on line 574 was never true
575 unittest.main()