Coverage for tests/test_Config.py: 16%
359 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-10 09:56 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-10 09:56 +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 tempfile
34import unittest
36try:
37 import yaml
38except ImportError:
39 yaml = None
41import lsst.pex.config as pexConfig
43# Some tests depend on daf_base.
44# Skip them if it is not found.
45try:
46 import lsst.daf.base as dafBase
47except ImportError:
48 dafBase = None
50GLOBAL_REGISTRY = {}
53class Simple(pexConfig.Config):
54 """A simple config used for testing."""
56 i = pexConfig.Field("integer test", int, optional=True)
57 f = pexConfig.Field("float test", float, default=3.0)
58 b = pexConfig.Field("boolean test", bool, default=False, optional=False)
59 c = pexConfig.ChoiceField(
60 "choice test", str, default="Hello", allowed={"Hello": "First choice", "World": "second choice"}
61 )
62 r = pexConfig.RangeField("Range test", float, default=3.0, optional=False, min=3.0, inclusiveMin=True)
63 ll = pexConfig.ListField( 63 ↛ exitline 63 didn't jump to the function exit
64 "list test", int, default=[1, 2, 3], maxLength=5, itemCheck=lambda x: x is not None and x > 0
65 )
66 d = pexConfig.DictField( 66 ↛ exitline 66 didn't jump to the function exit
67 "dict test", str, str, default={"key": "value"}, itemCheck=lambda x: x.startswith("v")
68 )
69 n = pexConfig.Field("nan test", float, default=float("NAN"))
72GLOBAL_REGISTRY["AAA"] = Simple
75class InnerConfig(pexConfig.Config):
76 """Inner config used for testing."""
78 f = pexConfig.Field("Inner.f", float, default=0.0, check=lambda x: x >= 0, optional=False) 78 ↛ exitline 78 didn't run the lambda on line 78
81GLOBAL_REGISTRY["BBB"] = InnerConfig
84class OuterConfig(InnerConfig, pexConfig.Config):
85 """Outer config used for testing."""
87 i = pexConfig.ConfigField("Outer.i", InnerConfig)
89 def __init__(self):
90 pexConfig.Config.__init__(self)
91 self.i.f = 5.0
93 def validate(self):
94 pexConfig.Config.validate(self)
95 if self.i.f < 5:
96 raise ValueError("validation failed, outer.i.f must be greater than 5")
99class Complex(pexConfig.Config):
100 """A complex config for testing."""
102 c = pexConfig.ConfigField("an inner config", InnerConfig)
103 r = pexConfig.ConfigChoiceField(
104 "a registry field", typemap=GLOBAL_REGISTRY, default="AAA", optional=False
105 )
106 p = pexConfig.ConfigChoiceField("another registry", typemap=GLOBAL_REGISTRY, default="BBB", optional=True)
109class Deprecation(pexConfig.Config):
110 """A test config with a deprecated field."""
112 old = pexConfig.Field("Something.", int, default=10, deprecated="not used!")
115class ConfigTest(unittest.TestCase):
116 """Tests of basic Config functionality."""
118 def setUp(self):
119 self.simple = Simple()
120 self.inner = InnerConfig()
121 self.outer = OuterConfig()
122 self.comp = Complex()
123 self.deprecation = Deprecation()
125 def tearDown(self):
126 del self.simple
127 del self.inner
128 del self.outer
129 del self.comp
131 def testFieldTypeAnnotationRuntime(self):
132 # test parsing type annotation for runtime dtype
133 testField = pexConfig.Field[str](doc="test")
134 self.assertEqual(testField.dtype, str)
136 # verify that forward references work correctly
137 testField = pexConfig.Field["float"](doc="test")
138 self.assertEqual(testField.dtype, float)
140 # verify that Field rejects multiple types
141 with self.assertRaises(ValueError):
142 pexConfig.Field[str, int](doc="test") # type: ignore
144 # verify that Field raises in conflict with dtype:
145 with self.assertRaises(ValueError):
146 pexConfig.Field[str](doc="test", dtype=int)
148 # verify that Field does not raise if dtype agrees
149 testField = pexConfig.Field[int](doc="test", dtype=int)
150 self.assertEqual(testField.dtype, int)
152 def testInit(self):
153 self.assertIsNone(self.simple.i)
154 self.assertEqual(self.simple.f, 3.0)
155 self.assertFalse(self.simple.b)
156 self.assertEqual(self.simple.c, "Hello")
157 self.assertEqual(list(self.simple.ll), [1, 2, 3])
158 self.assertEqual(self.simple.d["key"], "value")
159 self.assertEqual(self.inner.f, 0.0)
160 self.assertEqual(self.deprecation.old, 10)
162 self.assertEqual(self.deprecation._fields["old"].doc, "Something. Deprecated: not used!")
164 self.assertEqual(self.outer.i.f, 5.0)
165 self.assertEqual(self.outer.f, 0.0)
167 self.assertEqual(self.comp.c.f, 0.0)
168 self.assertEqual(self.comp.r.name, "AAA")
169 self.assertEqual(self.comp.r.active.f, 3.0)
170 self.assertEqual(self.comp.r["BBB"].f, 0.0)
172 def testDeprecationWarning(self):
173 """Test that a deprecated field emits a warning when it is set."""
174 with self.assertWarns(FutureWarning) as w:
175 self.deprecation.old = 5
176 self.assertEqual(self.deprecation.old, 5)
178 self.assertIn(self.deprecation._fields["old"].deprecated, str(w.warnings[-1].message))
180 def testDeprecationOutput(self):
181 """Test that a deprecated field is not written out unless it is set."""
182 stream = io.StringIO()
183 self.deprecation.saveToStream(stream)
184 self.assertNotIn("config.old", stream.getvalue())
185 with self.assertWarns(FutureWarning):
186 self.deprecation.old = 5
187 stream = io.StringIO()
188 self.deprecation.saveToStream(stream)
189 self.assertIn("config.old=5\n", stream.getvalue())
191 def testDocstring(self):
192 """Test that the docstring is not allowed to be empty."""
193 with self.assertRaises(ValueError):
194 pexConfig.Field("", int, default=1)
196 with self.assertRaises(ValueError):
197 pexConfig.RangeField("", int, default=3, min=3, max=4)
199 with self.assertRaises(ValueError):
200 pexConfig.DictField("", str, str, default={"key": "value"})
202 with self.assertRaises(ValueError):
203 pexConfig.ListField("", int, default=[1, 2, 3])
205 with self.assertRaises(ValueError):
206 pexConfig.ConfigField("", InnerConfig)
208 with self.assertRaises(ValueError):
209 pexConfig.ConfigChoiceField("", typemap=GLOBAL_REGISTRY, default="AAA")
211 def testValidate(self):
212 self.simple.validate()
214 self.inner.validate()
215 self.assertRaises(ValueError, setattr, self.outer.i, "f", -5)
216 self.outer.i.f = 10.0
217 self.outer.validate()
219 try:
220 self.simple.d["failKey"] = "failValue"
221 except pexConfig.FieldValidationError:
222 pass
223 except Exception:
224 raise "Validation error Expected"
225 self.simple.validate()
227 self.outer.i = InnerConfig
228 self.assertRaises(ValueError, self.outer.validate)
229 self.outer.i = InnerConfig()
230 self.assertRaises(ValueError, self.outer.validate)
232 self.comp.validate()
233 self.comp.r = None
234 self.assertRaises(ValueError, self.comp.validate)
235 self.comp.r = "BBB"
236 self.comp.validate()
238 def testRangeFieldConstructor(self):
239 """Test RangeField constructor's checking of min, max."""
240 val = 3
241 self.assertRaises(ValueError, pexConfig.RangeField, "test", int, default=val, min=val, max=val - 1)
242 self.assertRaises(
243 ValueError, pexConfig.RangeField, "test", float, default=val, min=val, max=val - 1e-15
244 )
245 for inclusiveMin, inclusiveMax in itertools.product((False, True), (False, True)):
246 if inclusiveMin and inclusiveMax:
247 # should not raise
248 class Cfg1(pexConfig.Config):
249 r1 = pexConfig.RangeField(
250 doc="test",
251 dtype=int,
252 default=val,
253 min=val,
254 max=val,
255 inclusiveMin=inclusiveMin,
256 inclusiveMax=inclusiveMax,
257 )
258 r2 = pexConfig.RangeField(
259 doc="test",
260 dtype=float,
261 default=val,
262 min=val,
263 max=val,
264 inclusiveMin=inclusiveMin,
265 inclusiveMax=inclusiveMax,
266 )
268 Cfg1()
269 else:
270 # raise while constructing the RangeField (hence cannot make
271 # it part of a Config)
272 self.assertRaises(
273 ValueError,
274 pexConfig.RangeField,
275 doc="test",
276 dtype=int,
277 default=val,
278 min=val,
279 max=val,
280 inclusiveMin=inclusiveMin,
281 inclusiveMax=inclusiveMax,
282 )
283 self.assertRaises(
284 ValueError,
285 pexConfig.RangeField,
286 doc="test",
287 dtype=float,
288 default=val,
289 min=val,
290 max=val,
291 inclusiveMin=inclusiveMin,
292 inclusiveMax=inclusiveMax,
293 )
295 def testRangeFieldDefault(self):
296 """Test RangeField's checking of the default value."""
297 minVal = 3
298 maxVal = 4
299 for val, inclusiveMin, inclusiveMax, shouldRaise in (
300 (minVal, False, True, True),
301 (minVal, True, True, False),
302 (maxVal, True, False, True),
303 (maxVal, True, True, False),
304 ):
306 class Cfg1(pexConfig.Config):
307 r = pexConfig.RangeField(
308 doc="test",
309 dtype=int,
310 default=val,
311 min=minVal,
312 max=maxVal,
313 inclusiveMin=inclusiveMin,
314 inclusiveMax=inclusiveMax,
315 )
317 class Cfg2(pexConfig.Config):
318 r2 = pexConfig.RangeField(
319 doc="test",
320 dtype=float,
321 default=val,
322 min=minVal,
323 max=maxVal,
324 inclusiveMin=inclusiveMin,
325 inclusiveMax=inclusiveMax,
326 )
328 if shouldRaise:
329 self.assertRaises(pexConfig.FieldValidationError, Cfg1)
330 self.assertRaises(pexConfig.FieldValidationError, Cfg2)
331 else:
332 Cfg1()
333 Cfg2()
335 def testSave(self):
336 self.comp.r = "BBB"
337 self.comp.p = "AAA"
338 self.comp.c.f = 5.0
339 with tempfile.TemporaryDirectory(prefix="config-save-test", ignore_cleanup_errors=True) as tmpdir:
340 roundtrip_path = os.path.join(tmpdir, "roundtrip.test")
341 self.comp.save(roundtrip_path)
343 roundTrip = Complex()
344 roundTrip.load(roundtrip_path)
345 self.assertEqual(self.comp.c.f, roundTrip.c.f)
346 self.assertEqual(self.comp.r.name, roundTrip.r.name)
347 del roundTrip
349 # test saving to an open file
350 roundtrip_path = os.path.join(tmpdir, "roundtrip_open.test")
351 with open(roundtrip_path, "w") as outfile:
352 self.comp.saveToStream(outfile)
353 roundTrip = Complex()
354 with open(roundtrip_path) as infile:
355 roundTrip.loadFromStream(infile)
356 self.assertEqual(self.comp.c.f, roundTrip.c.f)
357 self.assertEqual(self.comp.r.name, roundTrip.r.name)
358 del roundTrip
360 # Test an override of the default variable name.
361 roundtrip_path = os.path.join(tmpdir, "roundtrip_def.test")
362 with open(roundtrip_path, "w") as outfile:
363 self.comp.saveToStream(outfile, root="root")
364 roundTrip = Complex()
365 with self.assertRaises(NameError):
366 roundTrip.load(roundtrip_path)
367 roundTrip.load(roundtrip_path, root="root")
368 self.assertEqual(self.comp.c.f, roundTrip.c.f)
369 self.assertEqual(self.comp.r.name, roundTrip.r.name)
371 # test saving to a string.
372 saved_string = self.comp.saveToString()
373 roundTrip = Complex()
374 roundTrip.loadFromString(saved_string)
375 self.assertEqual(self.comp.c.f, roundTrip.c.f)
376 self.assertEqual(self.comp.r.name, roundTrip.r.name)
377 del roundTrip
379 def testDuplicateRegistryNames(self):
380 self.comp.r["AAA"].f = 5.0
381 self.assertEqual(self.comp.p["AAA"].f, 3.0)
383 def testInheritance(self):
384 class AAA(pexConfig.Config):
385 a = pexConfig.Field("AAA.a", int, default=4)
387 class BBB(AAA):
388 b = pexConfig.Field("BBB.b", int, default=3)
390 class CCC(BBB):
391 c = pexConfig.Field("CCC.c", int, default=2)
393 # test multi-level inheritance
394 c = CCC()
395 self.assertIn("a", c.toDict())
396 self.assertEqual(c._fields["a"].dtype, int)
397 self.assertEqual(c.a, 4)
399 # test conflicting multiple inheritance
400 class DDD(pexConfig.Config):
401 a = pexConfig.Field("DDD.a", float, default=0.0)
403 class EEE(DDD, AAA):
404 pass
406 e = EEE()
407 self.assertEqual(e._fields["a"].dtype, float)
408 self.assertIn("a", e.toDict())
409 self.assertEqual(e.a, 0.0)
411 class FFF(AAA, DDD):
412 pass
414 f = FFF()
415 self.assertEqual(f._fields["a"].dtype, int)
416 self.assertIn("a", f.toDict())
417 self.assertEqual(f.a, 4)
419 # test inheritance from non Config objects
420 class GGG:
421 a = pexConfig.Field("AAA.a", float, default=10.0)
423 class HHH(GGG, AAA):
424 pass
426 h = HHH()
427 self.assertEqual(h._fields["a"].dtype, float)
428 self.assertIn("a", h.toDict())
429 self.assertEqual(h.a, 10.0)
431 # test partial Field redefinition
433 class III(AAA):
434 pass
436 III.a.default = 5
438 self.assertEqual(III.a.default, 5)
439 self.assertEqual(AAA.a.default, 4)
441 @unittest.skipIf(dafBase is None, "lsst.daf.base is required")
442 def testConvertPropertySet(self):
443 ps = pexConfig.makePropertySet(self.simple)
444 self.assertFalse(ps.exists("i"))
445 self.assertEqual(ps.getScalar("f"), self.simple.f)
446 self.assertEqual(ps.getScalar("b"), self.simple.b)
447 self.assertEqual(ps.getScalar("c"), self.simple.c)
448 self.assertEqual(list(ps.getArray("ll")), list(self.simple.ll))
450 ps = pexConfig.makePropertySet(self.comp)
451 self.assertEqual(ps.getScalar("c.f"), self.comp.c.f)
453 def testFreeze(self):
454 self.comp.freeze()
456 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.c, "f", 10.0)
457 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "r", "AAA")
458 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp, "p", "AAA")
459 self.assertRaises(pexConfig.FieldValidationError, setattr, self.comp.p["AAA"], "f", 5.0)
461 def checkImportRoundTrip(self, importStatement, searchString, shouldBeThere):
462 self.comp.c.f = 5.0
464 # Generate a Config through loading
465 stream = io.StringIO()
466 stream.write(str(importStatement))
467 self.comp.saveToStream(stream)
468 roundtrip = Complex()
469 roundtrip.loadFromStream(stream.getvalue())
470 self.assertEqual(self.comp.c.f, roundtrip.c.f)
472 # Check the save stream
473 stream = io.StringIO()
474 roundtrip.saveToStream(stream)
475 self.assertEqual(self.comp.c.f, roundtrip.c.f)
476 streamStr = stream.getvalue()
477 if shouldBeThere:
478 self.assertTrue(re.search(searchString, streamStr))
479 else:
480 self.assertFalse(re.search(searchString, streamStr))
482 def testImports(self):
483 # A module not used by anything else, but which exists
484 importing = "import lsst.pex.config._doNotImportMe\n"
485 self.checkImportRoundTrip(importing, importing, True)
487 def testBadImports(self):
488 dummy = "somethingThatDoesntExist"
489 importing = (
490 """
491try:
492 import %s
493except ImportError:
494 pass
495"""
496 % dummy
497 )
498 self.checkImportRoundTrip(importing, dummy, False)
500 def testPickle(self):
501 self.simple.f = 5
502 simple = pickle.loads(pickle.dumps(self.simple))
503 self.assertIsInstance(simple, Simple)
504 self.assertEqual(self.simple.f, simple.f)
506 self.comp.c.f = 5
507 comp = pickle.loads(pickle.dumps(self.comp))
508 self.assertIsInstance(comp, Complex)
509 self.assertEqual(self.comp.c.f, comp.c.f)
511 @unittest.skipIf(yaml is None, "Test requires pyyaml")
512 def testYaml(self):
513 self.simple.f = 5
514 simple = yaml.safe_load(yaml.dump(self.simple))
515 self.assertIsInstance(simple, Simple)
516 self.assertEqual(self.simple.f, simple.f)
518 self.comp.c.f = 5
519 # Use a different loader to check that it also works
520 comp = yaml.load(yaml.dump(self.comp), Loader=yaml.FullLoader)
521 self.assertIsInstance(comp, Complex)
522 self.assertEqual(self.comp.c.f, comp.c.f)
524 def testCompare(self):
525 comp2 = Complex()
526 inner2 = InnerConfig()
527 simple2 = Simple()
528 self.assertTrue(self.comp.compare(comp2))
529 self.assertTrue(comp2.compare(self.comp))
530 self.assertTrue(self.comp.c.compare(inner2))
531 self.assertTrue(self.simple.compare(simple2))
532 self.assertTrue(simple2.compare(self.simple))
533 self.assertEqual(self.simple, simple2)
534 self.assertEqual(simple2, self.simple)
535 outList = []
537 def outFunc(msg):
538 outList.append(msg)
540 simple2.b = True
541 simple2.ll.append(4)
542 simple2.d["foo"] = "var"
543 self.assertFalse(self.simple.compare(simple2, shortcut=True, output=outFunc))
544 self.assertEqual(len(outList), 1)
545 del outList[:]
546 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc))
547 output = "\n".join(outList)
548 self.assertIn("Inequality in b", output)
549 self.assertIn("Inequality in size for ll", output)
550 self.assertIn("Inequality in keys for d", output)
551 del outList[:]
552 self.simple.d["foo"] = "vast"
553 self.simple.ll.append(5)
554 self.simple.b = True
555 self.simple.f += 1e8
556 self.assertFalse(self.simple.compare(simple2, shortcut=False, output=outFunc))
557 output = "\n".join(outList)
558 self.assertIn("Inequality in f", output)
559 self.assertIn("Inequality in ll[3]", output)
560 self.assertIn("Inequality in d['foo']", output)
561 del outList[:]
562 comp2.r["BBB"].f = 1.0 # changing the non-selected item shouldn't break equality
563 self.assertTrue(self.comp.compare(comp2))
564 comp2.r["AAA"].i = 56 # changing the selected item should break equality
565 comp2.c.f = 1.0
566 self.assertFalse(self.comp.compare(comp2, shortcut=False, output=outFunc))
567 output = "\n".join(outList)
568 self.assertIn("Inequality in c.f", output)
569 self.assertIn("Inequality in r['AAA']", output)
570 self.assertNotIn("Inequality in r['BBB']", output)
572 # Before DM-16561, this incorrectly returned `True`.
573 self.assertFalse(self.inner.compare(self.outer))
574 # Before DM-16561, this raised.
575 self.assertFalse(self.outer.compare(self.inner))
577 def testLoadError(self):
578 """Check that loading allows errors in the file being loaded to
579 propagate.
580 """
581 self.assertRaises(SyntaxError, self.simple.loadFromStream, "bork bork bork")
582 self.assertRaises(NameError, self.simple.loadFromStream, "config.f = bork")
584 def testNames(self):
585 """Check that the names() method returns valid keys.
587 Also check that we have the right number of keys, and as they are
588 all known to be valid we know that we got them all.
589 """
590 names = self.simple.names()
591 self.assertEqual(len(names), 8)
592 for name in names:
593 self.assertTrue(hasattr(self.simple, name))
595 def testIteration(self):
596 self.assertIn("ll", self.simple)
597 self.assertIn("ll", self.simple.keys())
598 self.assertIn("Hello", self.simple.values())
599 self.assertEqual(len(self.simple.values()), 8)
601 for k, v, (k1, v1) in zip(self.simple.keys(), self.simple.values(), self.simple.items()):
602 self.assertEqual(k, k1)
603 if k == "n":
604 self.assertNotEqual(v, v1)
605 else:
606 self.assertEqual(v, v1)
609if __name__ == "__main__": 609 ↛ 610line 609 didn't jump to line 610, because the condition on line 609 was never true
610 unittest.main()