Coverage for tests/test_config.py: 14%
415 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-13 09:58 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-13 09:58 +0000
1# This file is part of daf_butler.
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 collections
29import contextlib
30import itertools
31import os
32import unittest
33from pathlib import Path
35from lsst.daf.butler import Config, ConfigSubset
36from lsst.daf.butler.tests.utils import makeTestTempDir, removeTestTempDir
38TESTDIR = os.path.abspath(os.path.dirname(__file__))
41@contextlib.contextmanager
42def modified_environment(**environ):
43 """Temporarily set environment variables.
45 >>> with modified_environment(DAF_BUTLER_CONFIG_PATHS="/somewhere"):
46 ... os.environ["DAF_BUTLER_CONFIG_PATHS"] == "/somewhere"
47 True
49 >>> "DAF_BUTLER_CONFIG_PATHS" != "/somewhere"
50 True
52 Parameters
53 ----------
54 **environ : `dict`
55 Key value pairs of environment variables to temporarily set.
56 """
57 old_environ = dict(os.environ)
58 os.environ.update(environ)
59 try:
60 yield
61 finally:
62 os.environ.clear()
63 os.environ.update(old_environ)
66class ExampleWithConfigFileReference:
67 """Example class referenced from config file."""
69 defaultConfigFile = "viacls.yaml"
72class ExampleWithConfigFileReference2:
73 """Example class referenced from config file."""
75 defaultConfigFile = "viacls2.yaml"
78class ConfigTest(ConfigSubset):
79 """Default config class for testing."""
81 component = "comp"
82 requiredKeys = ("item1", "item2")
83 defaultConfigFile = "testconfig.yaml"
86class ConfigTestPathlib(ConfigTest):
87 """Config with default using `pathlib.Path`."""
89 defaultConfigFile = Path("testconfig.yaml")
92class ConfigTestEmpty(ConfigTest):
93 """Config pointing to empty file."""
95 defaultConfigFile = "testconfig_empty.yaml"
96 requiredKeys = ()
99class ConfigTestButlerDir(ConfigTest):
100 """Simple config."""
102 defaultConfigFile = "testConfigs/testconfig.yaml"
105class ConfigTestNoDefaults(ConfigTest):
106 """Test config with no defaults."""
108 defaultConfigFile = None
109 requiredKeys = ()
112class ConfigTestAbsPath(ConfigTest):
113 """Test config with absolute paths."""
115 defaultConfigFile = None
116 requiredKeys = ()
119class ConfigTestCls(ConfigTest):
120 """Test config."""
122 defaultConfigFile = "withcls.yaml"
125class ConfigTestCase(unittest.TestCase):
126 """Tests of simple Config"""
128 def testBadConfig(self):
129 for badArg in (
130 [], # Bad argument
131 __file__, # Bad file extension for existing file
132 ):
133 with self.assertRaises(RuntimeError):
134 Config(badArg)
135 for badArg in (
136 "file.fits", # File that does not exist with bad extension
137 "b/c/d/", # Directory that does not exist
138 "file.yaml", # Good extension for missing file
139 ):
140 with self.assertRaises((FileNotFoundError, ValueError)):
141 Config(badArg)
143 def testBasics(self):
144 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}})
145 pretty = c.ppprint()
146 self.assertIn("key3", pretty)
147 r = repr(c)
148 self.assertIn("key3", r)
149 regex = r"^Config\(\{.*\}\)$"
150 self.assertRegex(r, regex)
151 c2 = eval(r)
152 self.assertIn("1", c)
153 for n in c.names():
154 self.assertEqual(c2[n], c[n])
155 self.assertEqual(c, c2)
156 s = str(c)
157 self.assertIn("\n", s)
158 self.assertNotRegex(s, regex)
160 self.assertCountEqual(c.keys(), ["1", "3", "key3", "dict"])
161 self.assertEqual(list(c), list(c.keys()))
162 self.assertEqual(list(c.values()), [c[k] for k in c])
163 self.assertEqual(list(c.items()), [(k, c[k]) for k in c])
165 newKeys = ("key4", ".dict.q", ("dict", "r"), "5")
166 oldKeys = ("key3", ".dict.a", ("dict", "b"), "3")
167 remainingKey = "1"
169 # Check get with existing key
170 for k in oldKeys:
171 self.assertEqual(c.get(k, "missing"), c[k])
173 # Check get, pop with nonexistent key
174 for k in newKeys:
175 self.assertEqual(c.get(k, "missing"), "missing")
176 self.assertEqual(c.pop(k, "missing"), "missing")
178 # Check setdefault with existing key
179 for k in oldKeys:
180 c.setdefault(k, 8)
181 self.assertNotEqual(c[k], 8)
183 # Check setdefault with nonexistent key (mutates c, adding newKeys)
184 for k in newKeys:
185 c.setdefault(k, 8)
186 self.assertEqual(c[k], 8)
188 # Check pop with existing key (mutates c, removing newKeys)
189 for k in newKeys:
190 v = c[k]
191 self.assertEqual(c.pop(k, "missing"), v)
193 # Check deletion (mutates c, removing oldKeys)
194 for k in ("key3", ".dict.a", ("dict", "b"), "3"):
195 self.assertIn(k, c)
196 del c[k]
197 self.assertNotIn(k, c)
199 # Check that `dict` still exists, but is now empty (then remove
200 # it, mutatic c)
201 self.assertIn("dict", c)
202 del c["dict"]
204 # Check popitem (mutates c, removing remainingKey)
205 v = c[remainingKey]
206 self.assertEqual(c.popitem(), (remainingKey, v))
208 # Check that c is now empty
209 self.assertFalse(c)
211 def testDict(self):
212 """Test toDict()."""
213 c1 = Config({"a": {"b": 1}, "c": 2})
214 self.assertIsInstance(c1["a"], Config)
215 d1 = c1.toDict()
216 self.assertIsInstance(d1["a"], dict)
217 self.assertEqual(d1["a"], c1["a"])
219 # Modifying one does not change the other
220 d1["a"]["c"] = 2
221 self.assertNotEqual(d1["a"], c1["a"])
223 def assertSplit(self, answer, *args):
224 """Assert that string splitting was correct."""
225 for s in (answer, *args):
226 split = Config._splitIntoKeys(s)
227 self.assertEqual(split, answer)
229 def testSplitting(self):
230 """Test of the internal splitting API."""
231 # Try lots of keys that will return the same answer
232 answer = ["a", "b", "c", "d"]
233 self.assertSplit(answer, ".a.b.c.d", ":a:b:c:d", "\ta\tb\tc\td", "\ra\rb\rc\rd")
235 answer = ["a", "calexp.wcs", "b"]
236 self.assertSplit(answer, r".a.calexp\.wcs.b", ":a:calexp.wcs:b")
238 self.assertSplit(["a.b.c"])
239 self.assertSplit(["a", r"b\.c"], r"_a_b\.c")
241 # Escaping a backslash before a delimiter currently fails
242 with self.assertRaises(ValueError):
243 Config._splitIntoKeys(r".a.calexp\\.wcs.b")
245 # The next two fail because internally \r is magic when escaping
246 # a delimiter.
247 with self.assertRaises(ValueError):
248 Config._splitIntoKeys("\ra\rcalexp\\\rwcs\rb")
250 with self.assertRaises(ValueError):
251 Config._splitIntoKeys(".a.cal\rexp\\.wcs.b")
253 def testEscape(self):
254 c = Config({"a": {"foo.bar": 1}, "b😂c": {"bar_baz": 2}})
255 self.assertEqual(c[r".a.foo\.bar"], 1)
256 self.assertEqual(c[":a:foo.bar"], 1)
257 self.assertEqual(c[".b😂c.bar_baz"], 2)
258 self.assertEqual(c[r"😂b\😂c😂bar_baz"], 2)
259 self.assertEqual(c[r"\a\foo.bar"], 1)
260 self.assertEqual(c["\ra\rfoo.bar"], 1)
261 with self.assertRaises(ValueError):
262 c[".a.foo\\.bar\r"]
264 def testOperators(self):
265 c1 = Config({"a": {"b": 1}, "c": 2})
266 c2 = c1.copy()
267 self.assertEqual(c1, c2)
268 self.assertIsInstance(c2, Config)
269 c2[".a.b"] = 5
270 self.assertNotEqual(c1, c2)
272 def testMerge(self):
273 c1 = Config({"a": 1, "c": 3})
274 c2 = Config({"a": 4, "b": 2})
275 c1.merge(c2)
276 self.assertEqual(c1, {"a": 1, "b": 2, "c": 3})
278 # Check that c2 was not changed
279 self.assertEqual(c2, {"a": 4, "b": 2})
281 # Repeat with a simple dict
282 c1.merge({"b": 5, "d": 42})
283 self.assertEqual(c1, {"a": 1, "b": 2, "c": 3, "d": 42})
285 with self.assertRaises(TypeError):
286 c1.merge([1, 2, 3])
288 def testUpdate(self):
289 c = Config({"a": {"b": 1}})
290 c.update({"a": {"c": 2}})
291 self.assertEqual(c[".a.b"], 1)
292 self.assertEqual(c[".a.c"], 2)
293 c.update({"a": {"d": [3, 4]}})
294 self.assertEqual(c[".a.d.0"], 3)
295 c.update({"z": [5, 6, {"g": 2, "h": 3}]})
296 self.assertEqual(c[".z.1"], 6)
298 # This is detached from parent
299 c2 = c[".z.2"]
300 self.assertEqual(c2["g"], 2)
301 c2.update({"h": 4, "j": 5})
302 self.assertEqual(c2["h"], 4)
303 self.assertNotIn(".z.2.j", c)
304 self.assertNotEqual(c[".z.2.h"], 4)
306 with self.assertRaises(RuntimeError):
307 c.update([1, 2, 3])
309 def testHierarchy(self):
310 c = Config()
312 # Simple dict
313 c["a"] = {"z": 52, "x": "string"}
314 self.assertIn(".a.z", c)
315 self.assertEqual(c[".a.x"], "string")
317 # Try different delimiters
318 self.assertEqual(c["⇛a⇛z"], 52)
319 self.assertEqual(c[("a", "z")], 52)
320 self.assertEqual(c["a", "z"], 52)
322 c[".b.new.thing1"] = "thing1"
323 c[".b.new.thing2"] = "thing2"
324 c[".b.new.thing3.supp"] = "supplemental"
325 self.assertEqual(c[".b.new.thing1"], "thing1")
326 tmp = c[".b.new"]
327 self.assertEqual(tmp["thing2"], "thing2")
328 self.assertEqual(c[".b.new.thing3.supp"], "supplemental")
330 # Test that we can index into lists
331 c[".a.b.c"] = [1, "7", 3, {"1": 4, "5": "Five"}, "hello"]
332 self.assertIn(".a.b.c.3.5", c)
333 self.assertNotIn(".a.b.c.10", c)
334 self.assertNotIn(".a.b.c.10.d", c)
335 self.assertEqual(c[".a.b.c.3.5"], "Five")
336 # Is the value in the list?
337 self.assertIn(".a.b.c.hello", c)
338 self.assertNotIn(".a.b.c.hello.not", c)
340 # And assign to an element in the list
341 self.assertEqual(c[".a.b.c.1"], "7")
342 c[".a.b.c.1"] = 8
343 self.assertEqual(c[".a.b.c.1"], 8)
344 self.assertIsInstance(c[".a.b.c"], collections.abc.Sequence)
346 # Test we do get lists back from asArray
347 a = c.asArray(".a.b.c")
348 self.assertIsInstance(a, list)
350 # Is it the *same* list as in the config
351 a.append("Sentinel")
352 self.assertIn("Sentinel", c[".a.b.c"])
353 self.assertIn(".a.b.c.Sentinel", c)
355 # Test we always get a list
356 for k in c.names():
357 a = c.asArray(k)
358 self.assertIsInstance(a, list)
360 # Check we get the same top level keys
361 self.assertEqual(set(c.names(topLevelOnly=True)), set(c._data.keys()))
363 # Check that we can iterate through items
364 for k, v in c.items():
365 self.assertEqual(c[k], v)
367 # Check that lists still work even if assigned a dict
368 c = Config(
369 {
370 "cls": "lsst.daf.butler",
371 "formatters": {"calexp.wcs": "{component}", "calexp": "{datasetType}"},
372 "datastores": [{"datastore": {"cls": "datastore1"}}, {"datastore": {"cls": "datastore2"}}],
373 }
374 )
375 c[".datastores.1.datastore"] = {"cls": "datastore2modified"}
376 self.assertEqual(c[".datastores.0.datastore.cls"], "datastore1")
377 self.assertEqual(c[".datastores.1.datastore.cls"], "datastore2modified")
378 self.assertIsInstance(c["datastores"], collections.abc.Sequence)
380 # Test that we can get all the listed names.
381 # and also that they are marked as "in" the Config
382 # Try delimited names and tuples
383 for n in itertools.chain(c.names(), c.nameTuples()):
384 val = c[n]
385 self.assertIsNotNone(val)
386 self.assertIn(n, c)
388 names = c.names()
389 nameTuples = c.nameTuples()
390 self.assertEqual(len(names), len(nameTuples))
391 self.assertEqual(len(names), 11)
392 self.assertEqual(len(nameTuples), 11)
394 # Test that delimiter escaping works
395 names = c.names(delimiter=".")
396 for n in names:
397 self.assertIn(n, c)
398 self.assertIn(".formatters.calexp\\.wcs", names)
400 # Use a name that includes the internal default delimiter
401 # to test automatic adjustment of delimiter
402 strangeKey = f"calexp{c._D}wcs"
403 c["formatters", strangeKey] = "dynamic"
404 names = c.names()
405 self.assertIn(strangeKey, "-".join(names))
406 self.assertFalse(names[0].startswith(c._D))
407 for n in names:
408 self.assertIn(n, c)
410 top = c.nameTuples(topLevelOnly=True)
411 self.assertIsInstance(top[0], tuple)
413 # Investigate a possible delimeter in a key
414 c = Config({"formatters": {"calexp.wcs": 2, "calexp": 3}})
415 self.assertEqual(c[":formatters:calexp.wcs"], 2)
416 self.assertEqual(c[":formatters:calexp"], 3)
417 for k, v in c["formatters"].items():
418 self.assertEqual(c["formatters", k], v)
420 # Check internal delimiter inheritance
421 c._D = "."
422 c2 = c["formatters"]
423 self.assertEqual(c._D, c2._D) # Check that the child inherits
424 self.assertNotEqual(c2._D, Config._D)
426 def testSerializedString(self):
427 """Test that we can create configs from strings"""
428 serialized = {
429 "yaml": """
430testing: hello
431formatters:
432 calexp: 3""",
433 "json": '{"testing": "hello", "formatters": {"calexp": 3}}',
434 }
436 for format, string in serialized.items():
437 c = Config.fromString(string, format=format)
438 self.assertEqual(c["formatters", "calexp"], 3)
439 self.assertEqual(c["testing"], "hello")
441 with self.assertRaises(ValueError):
442 Config.fromString("", format="unknown")
444 with self.assertRaises(ValueError):
445 Config.fromString(serialized["yaml"], format="json")
447 # This JSON can be parsed by YAML parser
448 j = Config.fromString(serialized["json"])
449 y = Config.fromString(serialized["yaml"])
450 self.assertEqual(j["formatters", "calexp"], 3)
451 self.assertEqual(j.toDict(), y.toDict())
453 # Round trip JSON -> Config -> YAML -> Config -> JSON -> Config
454 c1 = Config.fromString(serialized["json"], format="json")
455 yaml = c1.dump(format="yaml")
456 c2 = Config.fromString(yaml, format="yaml")
457 json = c2.dump(format="json")
458 c3 = Config.fromString(json, format="json")
459 self.assertEqual(c3.toDict(), c1.toDict())
462class ConfigSubsetTestCase(unittest.TestCase):
463 """Tests for ConfigSubset"""
465 def setUp(self):
466 self.testDir = os.path.abspath(os.path.dirname(__file__))
467 self.configDir = os.path.join(self.testDir, "config", "testConfigs")
468 self.configDir2 = os.path.join(self.testDir, "config", "testConfigs", "test2")
469 self.configDir3 = os.path.join(self.testDir, "config", "testConfigs", "test3")
471 def testEmpty(self):
472 """Ensure that we can read an empty file."""
473 c = ConfigTestEmpty(searchPaths=(self.configDir,))
474 self.assertIsInstance(c, ConfigSubset)
476 def testPathlib(self):
477 """Ensure that we can read an empty file."""
478 c = ConfigTestPathlib(searchPaths=(self.configDir,))
479 self.assertIsInstance(c, ConfigSubset)
481 def testDefaults(self):
482 """Read of defaults"""
483 # Supply the search path explicitly
484 c = ConfigTest(searchPaths=(self.configDir,))
485 self.assertIsInstance(c, ConfigSubset)
486 self.assertIn("item3", c)
487 self.assertEqual(c["item3"], 3)
489 # Use environment
490 with modified_environment(DAF_BUTLER_CONFIG_PATH=self.configDir):
491 c = ConfigTest()
492 self.assertIsInstance(c, ConfigSubset)
493 self.assertEqual(c["item3"], 3)
495 # No default so this should fail
496 with self.assertRaises(KeyError):
497 c = ConfigTest()
499 def testExternalOverride(self):
500 """Ensure that external values win"""
501 c = ConfigTest({"item3": "newval"}, searchPaths=(self.configDir,))
502 self.assertIn("item3", c)
503 self.assertEqual(c["item3"], "newval")
505 def testSearchPaths(self):
506 """Two search paths"""
507 c = ConfigTest(searchPaths=(self.configDir2, self.configDir))
508 self.assertIsInstance(c, ConfigSubset)
509 self.assertIn("item3", c)
510 self.assertEqual(c["item3"], "override")
511 self.assertEqual(c["item4"], "new")
513 c = ConfigTest(searchPaths=(self.configDir, self.configDir2))
514 self.assertIsInstance(c, ConfigSubset)
515 self.assertIn("item3", c)
516 self.assertEqual(c["item3"], 3)
517 self.assertEqual(c["item4"], "new")
519 def testExternalHierarchy(self):
520 """Test that we can provide external config parameters in hierarchy"""
521 c = ConfigTest({"comp": {"item1": 6, "item2": "a", "a": "b", "item3": 7}, "item4": 8})
522 self.assertIn("a", c)
523 self.assertEqual(c["a"], "b")
524 self.assertNotIn("item4", c)
526 def testNoDefaults(self):
527 """Ensure that defaults can be turned off."""
528 # Mandatory keys but no defaults
529 c = ConfigTest({"item1": "a", "item2": "b", "item6": 6})
530 self.assertEqual(len(c.filesRead), 0)
531 self.assertIn("item1", c)
532 self.assertEqual(c["item6"], 6)
534 c = ConfigTestNoDefaults()
535 self.assertEqual(len(c.filesRead), 0)
537 def testAbsPath(self):
538 """Read default config from an absolute path"""
539 # Force the path to be absolute in the class
540 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, "abspath.yaml")
541 c = ConfigTestAbsPath()
542 self.assertEqual(c["item11"], "eleventh")
543 self.assertEqual(len(c.filesRead), 1)
545 # Now specify the normal config file with an absolute path
546 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, ConfigTest.defaultConfigFile)
547 c = ConfigTestAbsPath()
548 self.assertEqual(c["item11"], 11)
549 self.assertEqual(len(c.filesRead), 1)
551 # and a search path that will also include the file
552 c = ConfigTestAbsPath(
553 searchPaths=(
554 self.configDir,
555 self.configDir2,
556 )
557 )
558 self.assertEqual(c["item11"], 11)
559 self.assertEqual(len(c.filesRead), 1)
561 # Same as above but this time with relative path and two search paths
562 # to ensure the count changes
563 ConfigTestAbsPath.defaultConfigFile = ConfigTest.defaultConfigFile
564 c = ConfigTestAbsPath(
565 searchPaths=(
566 self.configDir,
567 self.configDir2,
568 )
569 )
570 self.assertEqual(len(c.filesRead), 2)
572 # Reset the class
573 ConfigTestAbsPath.defaultConfigFile = None
575 def testClassDerived(self):
576 """Read config specified in class determined from config"""
577 c = ConfigTestCls(searchPaths=(self.configDir,))
578 self.assertEqual(c["item50"], 50)
579 self.assertEqual(c["help"], "derived")
581 # Same thing but additional search path
582 c = ConfigTestCls(searchPaths=(self.configDir, self.configDir2))
583 self.assertEqual(c["item50"], 50)
584 self.assertEqual(c["help"], "derived")
585 self.assertEqual(c["help2"], "second")
587 # Same thing but reverse the two paths
588 c = ConfigTestCls(searchPaths=(self.configDir2, self.configDir))
589 self.assertEqual(c["item50"], 500)
590 self.assertEqual(c["help"], "class")
591 self.assertEqual(c["help2"], "second")
592 self.assertEqual(c["help3"], "third")
594 def testInclude(self):
595 """Read a config that has an include directive"""
596 c = Config(os.path.join(self.configDir, "testinclude.yaml"))
597 self.assertEqual(c[".comp1.item1"], 58)
598 self.assertEqual(c[".comp2.comp.item1"], 1)
599 self.assertEqual(c[".comp3.1.comp.item1"], "posix")
600 self.assertEqual(c[".comp4.0.comp.item1"], "posix")
601 self.assertEqual(c[".comp4.1.comp.item1"], 1)
602 self.assertEqual(c[".comp5.comp6.comp.item1"], "posix")
604 # Test a specific name and then test that all
605 # returned names are "in" the config.
606 names = c.names()
607 self.assertIn(c._D.join(("", "comp3", "1", "comp", "item1")), names)
608 for n in names:
609 self.assertIn(n, c)
611 # Test that override delimiter works
612 delimiter = "-"
613 names = c.names(delimiter=delimiter)
614 self.assertIn(delimiter.join(("", "comp3", "1", "comp", "item1")), names)
616 def testStringInclude(self):
617 """Using include directives in strings"""
618 # See if include works for absolute path
619 c = Config.fromYaml(f"something: !include {os.path.join(self.configDir, 'testconfig.yaml')}")
620 self.assertEqual(c["something", "comp", "item3"], 3)
622 with self.assertRaises(FileNotFoundError) as cm:
623 Config.fromYaml("something: !include /not/here.yaml")
624 # Test that it really was trying to open the absolute path
625 self.assertIn("'/not/here.yaml'", str(cm.exception))
627 def testIncludeConfigs(self):
628 """Test the special includeConfigs key for pulling in additional
629 files.
630 """
631 c = Config(os.path.join(self.configDir, "configIncludes.yaml"))
632 self.assertEqual(c["comp", "item2"], "hello")
633 self.assertEqual(c["comp", "item50"], 5000)
634 self.assertEqual(c["comp", "item1"], "first")
635 self.assertEqual(c["comp", "item10"], "tenth")
636 self.assertEqual(c["comp", "item11"], "eleventh")
637 self.assertEqual(c["unrelated"], 1)
638 self.assertEqual(c["addon", "comp", "item1"], "posix")
639 self.assertEqual(c["addon", "comp", "item11"], -1)
640 self.assertEqual(c["addon", "comp", "item50"], 500)
642 c = Config(os.path.join(self.configDir, "configIncludes.json"))
643 self.assertEqual(c["comp", "item2"], "hello")
644 self.assertEqual(c["comp", "item50"], 5000)
645 self.assertEqual(c["comp", "item1"], "first")
646 self.assertEqual(c["comp", "item10"], "tenth")
647 self.assertEqual(c["comp", "item11"], "eleventh")
648 self.assertEqual(c["unrelated"], 1)
649 self.assertEqual(c["addon", "comp", "item1"], "posix")
650 self.assertEqual(c["addon", "comp", "item11"], -1)
651 self.assertEqual(c["addon", "comp", "item50"], 500)
653 # Now test with an environment variable in includeConfigs
654 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir3):
655 c = Config(os.path.join(self.configDir, "configIncludesEnv.yaml"))
656 self.assertEqual(c["comp", "item2"], "hello")
657 self.assertEqual(c["comp", "item50"], 5000)
658 self.assertEqual(c["comp", "item1"], "first")
659 self.assertEqual(c["comp", "item10"], "tenth")
660 self.assertEqual(c["comp", "item11"], "eleventh")
661 self.assertEqual(c["unrelated"], 1)
662 self.assertEqual(c["addon", "comp", "item1"], "envvar")
663 self.assertEqual(c["addon", "comp", "item11"], -1)
664 self.assertEqual(c["addon", "comp", "item50"], 501)
666 # This will fail
667 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir2):
668 with self.assertRaises(FileNotFoundError):
669 Config(os.path.join(self.configDir, "configIncludesEnv.yaml"))
671 def testResource(self):
672 c = Config("resource://lsst.daf.butler/configs/datastore.yaml")
673 self.assertIn("datastore", c)
675 # Test that we can include a resource URI
676 yaml = """
677toplevel: true
678resource: !include resource://lsst.daf.butler/configs/datastore.yaml
679"""
680 c = Config.fromYaml(yaml)
681 self.assertIn(("resource", "datastore", "cls"), c)
683 # Test that we can include a resource URI with includeConfigs
684 yaml = """
685toplevel: true
686resource:
687 includeConfigs: resource://lsst.daf.butler/configs/datastore.yaml
688"""
689 c = Config.fromYaml(yaml)
690 self.assertIn(("resource", "datastore", "cls"), c)
693class FileWriteConfigTestCase(unittest.TestCase):
694 """Test writing of configs."""
696 def setUp(self):
697 self.tmpdir = makeTestTempDir(TESTDIR)
699 def tearDown(self):
700 removeTestTempDir(self.tmpdir)
702 def testDump(self):
703 """Test that we can write and read a configuration."""
704 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}})
706 for format in ("yaml", "json"):
707 outpath = os.path.join(self.tmpdir, f"test.{format}")
708 c.dumpToUri(outpath)
710 c2 = Config(outpath)
711 self.assertEqual(c2, c)
713 c.dumpToUri(outpath, overwrite=True)
714 with self.assertRaises(FileExistsError):
715 c.dumpToUri(outpath, overwrite=False)
718if __name__ == "__main__":
719 unittest.main()