Coverage for tests/test_Config.py: 16%
357 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-28 10:15 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-28 10:15 +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="test")
121 self.assertEqual(testField.dtype, str)
123 # verify that forward references work correctly
124 testField = pexConfig.Field["float"](doc="test")
125 self.assertEqual(testField.dtype, float)
127 # verify that Field rejects multiple types
128 with self.assertRaises(ValueError):
129 pexConfig.Field[str, int](doc="test") # type: ignore
131 # verify that Field raises in conflict with dtype:
132 with self.assertRaises(ValueError):
133 pexConfig.Field[str](doc="test", dtype=int)
135 # verify that Field does not raise if dtype agrees
136 testField = pexConfig.Field[int](doc="test", 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 testDocstring(self):
179 """Test that the docstring is not allowed to be empty."""
180 with self.assertRaises(ValueError):
181 pexConfig.Field("", int, default=1)
183 with self.assertRaises(ValueError):
184 pexConfig.RangeField("", int, default=3, min=3, max=4)
186 with self.assertRaises(ValueError):
187 pexConfig.DictField("", str, str, default={"key": "value"})
189 with self.assertRaises(ValueError):
190 pexConfig.ListField("", int, default=[1, 2, 3])
192 with self.assertRaises(ValueError):
193 pexConfig.ConfigField("", InnerConfig)
195 with self.assertRaises(ValueError):
196 pexConfig.ConfigChoiceField("", typemap=GLOBAL_REGISTRY, default="AAA")
198 def testValidate(self):
199 self.simple.validate()
201 self.inner.validate()
202 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5)
203 self.outer.i.f = 10.0
204 self.outer.validate()
206 try:
207 self.simple.d["failKey"] = "failValue"
208 except pexConfig.FieldValidationError:
209 pass
210 except Exception:
211 raise "Validation error Expected"
212 self.simple.validate()
214 self.outer.i = InnerConfig
215 self.assertRaises(ValueError, self.outer.validate)
216 self.outer.i = InnerConfig()
217 self.assertRaises(ValueError, self.outer.validate)
219 self.comp.validate()
220 self.comp.r = None
221 self.assertRaises(ValueError, self.comp.validate)
222 self.comp.r = "BBB"
223 self.comp.validate()
225 def testRangeFieldConstructor(self):
226 """Test RangeField constructor's checking of min, max"""
227 val = 3
228 self.assertRaises(ValueError, pexConfig.RangeField, "test", int, default=val, min=val, max=val - 1)
229 self.assertRaises(
230 ValueError, pexConfig.RangeField, "test", float, default=val, min=val, max=val - 1e-15
231 )
232 for inclusiveMin, inclusiveMax in itertools.product((False, True), (False, True)):
233 if inclusiveMin and inclusiveMax:
234 # should not raise
235 class Cfg1(pexConfig.Config):
236 r1 = pexConfig.RangeField(
237 doc="test",
238 dtype=int,
239 default=val,
240 min=val,
241 max=val,
242 inclusiveMin=inclusiveMin,
243 inclusiveMax=inclusiveMax,
244 )
245 r2 = pexConfig.RangeField(
246 doc="test",
247 dtype=float,
248 default=val,
249 min=val,
250 max=val,
251 inclusiveMin=inclusiveMin,
252 inclusiveMax=inclusiveMax,
253 )
255 Cfg1()
256 else:
257 # raise while constructing the RangeField (hence cannot make
258 # it part of a Config)
259 self.assertRaises(
260 ValueError,
261 pexConfig.RangeField,
262 doc="test",
263 dtype=int,
264 default=val,
265 min=val,
266 max=val,
267 inclusiveMin=inclusiveMin,
268 inclusiveMax=inclusiveMax,
269 )
270 self.assertRaises(
271 ValueError,
272 pexConfig.RangeField,
273 doc="test",
274 dtype=float,
275 default=val,
276 min=val,
277 max=val,
278 inclusiveMin=inclusiveMin,
279 inclusiveMax=inclusiveMax,
280 )
282 def testRangeFieldDefault(self):
283 """Test RangeField's checking of the default value"""
284 minVal = 3
285 maxVal = 4
286 for val, inclusiveMin, inclusiveMax, shouldRaise in (
287 (minVal, False, True, True),
288 (minVal, True, True, False),
289 (maxVal, True, False, True),
290 (maxVal, True, True, False),
291 ):
293 class Cfg1(pexConfig.Config):
294 r = pexConfig.RangeField(
295 doc="test",
296 dtype=int,
297 default=val,
298 min=minVal,
299 max=maxVal,
300 inclusiveMin=inclusiveMin,
301 inclusiveMax=inclusiveMax,
302 )
304 class Cfg2(pexConfig.Config):
305 r2 = pexConfig.RangeField(
306 doc="test",
307 dtype=float,
308 default=val,
309 min=minVal,
310 max=maxVal,
311 inclusiveMin=inclusiveMin,
312 inclusiveMax=inclusiveMax,
313 )
315 if shouldRaise:
316 self.assertRaises(pexConfig.FieldValidationError, Cfg1)
317 self.assertRaises(pexConfig.FieldValidationError, Cfg2)
318 else:
319 Cfg1()
320 Cfg2()
322 def testSave(self):
323 self.comp.r = "BBB"
324 self.comp.p = "AAA"
325 self.comp.c.f = 5.0
326 self.comp.save("roundtrip.test")
328 roundTrip = Complex()
329 roundTrip.load("roundtrip.test")
330 os.remove("roundtrip.test")
331 self.assertEqual(self.comp.c.f, roundTrip.c.f)
332 self.assertEqual(self.comp.r.name, roundTrip.r.name)
333 del roundTrip
335 # test saving to an open file
336 with open("roundtrip.test", "w") as outfile:
337 self.comp.saveToStream(outfile)
338 roundTrip = Complex()
339 with open("roundtrip.test", "r") as infile:
340 roundTrip.loadFromStream(infile)
341 os.remove("roundtrip.test")
342 self.assertEqual(self.comp.c.f, roundTrip.c.f)
343 self.assertEqual(self.comp.r.name, roundTrip.r.name)
344 del roundTrip
346 # test saving to a string.
347 saved_string = self.comp.saveToString()
348 roundTrip = Complex()
349 roundTrip.loadFromString(saved_string)
350 self.assertEqual(self.comp.c.f, roundTrip.c.f)
351 self.assertEqual(self.comp.r.name, roundTrip.r.name)
352 del roundTrip
354 # Test an override of the default variable name.
355 with open("roundtrip.test", "w") as outfile:
356 self.comp.saveToStream(outfile, root="root")
357 roundTrip = Complex()
358 with self.assertRaises(NameError):
359 roundTrip.load("roundtrip.test")
360 roundTrip.load("roundtrip.test", root="root")
361 os.remove("roundtrip.test")
362 self.assertEqual(self.comp.c.f, roundTrip.c.f)
363 self.assertEqual(self.comp.r.name, roundTrip.r.name)
365 def testDuplicateRegistryNames(self):
366 self.comp.r["AAA"].f = 5.0
367 self.assertEqual(self.comp.p["AAA"].f, 3.0)
369 def testInheritance(self):
370 class AAA(pexConfig.Config):
371 a = pexConfig.Field("AAA.a", int, default=4)
373 class BBB(AAA):
374 b = pexConfig.Field("BBB.b", int, default=3)
376 class CCC(BBB):
377 c = pexConfig.Field("CCC.c", int, default=2)
379 # test multi-level inheritance
380 c = CCC()
381 self.assertIn("a", c.toDict())
382 self.assertEqual(c._fields["a"].dtype, int)
383 self.assertEqual(c.a, 4)
385 # test conflicting multiple inheritance
386 class DDD(pexConfig.Config):
387 a = pexConfig.Field("DDD.a", float, default=0.0)
389 class EEE(DDD, AAA):
390 pass
392 e = EEE()
393 self.assertEqual(e._fields["a"].dtype, float)
394 self.assertIn("a", e.toDict())
395 self.assertEqual(e.a, 0.0)
397 class FFF(AAA, DDD):
398 pass
400 f = FFF()
401 self.assertEqual(f._fields["a"].dtype, int)
402 self.assertIn("a", f.toDict())
403 self.assertEqual(f.a, 4)
405 # test inheritance from non Config objects
406 class GGG:
407 a = pexConfig.Field("AAA.a", float, default=10.0)
409 class HHH(GGG, AAA):
410 pass
412 h = HHH()
413 self.assertEqual(h._fields["a"].dtype, float)
414 self.assertIn("a", h.toDict())
415 self.assertEqual(h.a, 10.0)
417 # test partial Field redefinition
419 class III(AAA):
420 pass
422 III.a.default = 5
424 self.assertEqual(III.a.default, 5)
425 self.assertEqual(AAA.a.default, 4)
427 @unittest.skipIf(dafBase is None, "lsst.daf.base is required")
428 def testConvertPropertySet(self):
429 ps = pexConfig.makePropertySet(self.simple)
430 self.assertFalse(ps.exists("i"))
431 self.assertEqual(ps.getScalar("f"), self.simple.f)
432 self.assertEqual(ps.getScalar("b"), self.simple.b)
433 self.assertEqual(ps.getScalar("c"), self.simple.c)
434 self.assertEqual(list(ps.getArray("ll")), list(self.simple.ll))
436 ps = pexConfig.makePropertySet(self.comp)
437 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
439 def testFreeze(self):
440 self.comp.freeze()
442 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.c, "f", 10.0)
443 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "r", "AAA")
444 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "p", "AAA")
445 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.p["AAA"], "f", 5.0)
447 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
448 self.comp.c.f = 5.0
450 # Generate a Config through loading
451 stream = io.StringIO()
452 stream.write(str(importStatement))
453 self.comp.saveToStream(stream)
454 roundtrip = Complex()
455 roundtrip.loadFromStream(stream.getvalue())
456 self.assertEqual(self.comp.c.f, roundtrip.c.f)
458 # Check the save stream
459 stream = io.StringIO()
460 roundtrip.saveToStream(stream)
461 self.assertEqual(self.comp.c.f, roundtrip.c.f)
462 streamStr = stream.getvalue()
463 if shouldBeThere:
464 self.assertTrue(re.search(searchString, streamStr))
465 else:
466 self.assertFalse(re.search(searchString, streamStr))
468 def testImports(self):
469 # A module not used by anything else, but which exists
470 importing = "import lsst.pex.config._doNotImportMe\n"
471 self.checkImportRoundTrip(importing, importing, True)
473 def testBadImports(self):
474 dummy = "somethingThatDoesntExist"
475 importing = (
476 """
477try:
478 import %s
479except ImportError:
480 pass
481"""
482 % dummy
483 )
484 self.checkImportRoundTrip(importing, dummy, False)
486 def testPickle(self):
487 self.simple.f = 5
488 simple = pickle.loads(pickle.dumps(self.simple))
489 self.assertIsInstance(simple, Simple)
490 self.assertEqual(self.simple.f, simple.f)
492 self.comp.c.f = 5
493 comp = pickle.loads(pickle.dumps(self.comp))
494 self.assertIsInstance(comp, Complex)
495 self.assertEqual(self.comp.c.f, comp.c.f)
497 @unittest.skipIf(yaml is None, "Test requires pyyaml")
498 def testYaml(self):
499 self.simple.f = 5
500 simple = yaml.safe_load(yaml.dump(self.simple))
501 self.assertIsInstance(simple, Simple)
502 self.assertEqual(self.simple.f, simple.f)
504 self.comp.c.f = 5
505 # Use a different loader to check that it also works
506 comp = yaml.load(yaml.dump(self.comp), Loader=yaml.FullLoader)
507 self.assertIsInstance(comp, Complex)
508 self.assertEqual(self.comp.c.f, comp.c.f)
510 def testCompare(self):
511 comp2 = Complex()
512 inner2 = InnerConfig()
513 simple2 = Simple()
514 self.assertTrue(self.comp.compare(comp2))
515 self.assertTrue(comp2.compare(self.comp))
516 self.assertTrue(self.comp.c.compare(inner2))
517 self.assertTrue(self.simple.compare(simple2))
518 self.assertTrue(simple2.compare(self.simple))
519 self.assertEqual(self.simple, simple2)
520 self.assertEqual(simple2, self.simple)
521 outList = []
523 def outFunc(msg):
524 outList.append(msg)
526 simple2.b = True
527 simple2.ll.append(4)
528 simple2.d["foo"] = "var"
529 self.assertFalse(self.simple.compare(simple2, shortcut=True, output=outFunc))
530 self.assertEqual(len(outList), 1)
531 del outList[:]
532 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc))
533 output = "\n".join(outList)
534 self.assertIn("Inequality in b", output)
535 self.assertIn("Inequality in size for ll", output)
536 self.assertIn("Inequality in keys for d", output)
537 del outList[:]
538 self.simple.d["foo"] = "vast"
539 self.simple.ll.append(5)
540 self.simple.b = True
541 self.simple.f += 1e8
542 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc))
543 output = "\n".join(outList)
544 self.assertIn("Inequality in f", output)
545 self.assertIn("Inequality in ll[3]", output)
546 self.assertIn("Inequality in d['foo']", output)
547 del outList[:]
548 comp2.r["BBB"].f = 1.0 # changing the non-selected item shouldn't break equality
549 self.assertTrue(self.comp.compare(comp2))
550 comp2.r["AAA"].i = 56 # changing the selected item should break equality
551 comp2.c.f = 1.0
552 self.assertFalse(self.comp.compare(comp2, shortcut=False, output=outFunc))
553 output = "\n".join(outList)
554 self.assertIn("Inequality in c.f", output)
555 self.assertIn("Inequality in r['AAA']", output)
556 self.assertNotIn("Inequality in r['BBB']", output)
558 # Before DM-16561, this incorrectly returned `True`.
559 self.assertFalse(self.inner.compare(self.outer))
560 # Before DM-16561, this raised.
561 self.assertFalse(self.outer.compare(self.inner))
563 def testLoadError(self):
564 """Check that loading allows errors in the file being loaded to
565 propagate.
566 """
567 self.assertRaises(SyntaxError, self.simple.loadFromStream, "bork bork bork")
568 self.assertRaises(NameError, self.simple.loadFromStream, "config.f = bork")
570 def testNames(self):
571 """Check that the names() method returns valid keys.
573 Also check that we have the right number of keys, and as they are
574 all known to be valid we know that we got them all.
575 """
577 names = self.simple.names()
578 self.assertEqual(len(names), 8)
579 for name in names:
580 self.assertTrue(hasattr(self.simple, name))
582 def testIteration(self):
583 self.assertIn("ll", self.simple)
584 self.assertIn("ll", self.simple.keys())
585 self.assertIn("Hello", self.simple.values())
586 self.assertEqual(len(self.simple.values()), 8)
588 for k, v, (k1, v1) in zip(self.simple.keys(), self.simple.values(), self.simple.items()):
589 self.assertEqual(k, k1)
590 if k == "n":
591 self.assertNotEqual(v, v1)
592 else:
593 self.assertEqual(v, v1)
596if __name__ == "__main__": 596 ↛ 597line 596 didn't jump to line 597, because the condition on line 596 was never true
597 unittest.main()