Coverage for tests/test_config.py: 13%
415 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-28 10:10 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-28 10:10 +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 program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22import collections
23import contextlib
24import itertools
25import os
26import unittest
27from pathlib import Path
29from lsst.daf.butler import Config, ConfigSubset
30from lsst.daf.butler.tests.utils import makeTestTempDir, removeTestTempDir
32TESTDIR = os.path.abspath(os.path.dirname(__file__))
35@contextlib.contextmanager
36def modified_environment(**environ):
37 """Temporarily set environment variables.
39 >>> with modified_environment(DAF_BUTLER_CONFIG_PATHS="/somewhere"):
40 ... os.environ["DAF_BUTLER_CONFIG_PATHS"] == "/somewhere"
41 True
43 >>> "DAF_BUTLER_CONFIG_PATHS" != "/somewhere"
44 True
46 Parameters
47 ----------
48 environ : `dict`
49 Key value pairs of environment variables to temporarily set.
50 """
51 old_environ = dict(os.environ)
52 os.environ.update(environ)
53 try:
54 yield
55 finally:
56 os.environ.clear()
57 os.environ.update(old_environ)
60class ExampleWithConfigFileReference:
61 """Example class referenced from config file."""
63 defaultConfigFile = "viacls.yaml"
66class ExampleWithConfigFileReference2:
67 """Example class referenced from config file."""
69 defaultConfigFile = "viacls2.yaml"
72class ConfigTest(ConfigSubset):
73 """Default config class for testing."""
75 component = "comp"
76 requiredKeys = ("item1", "item2")
77 defaultConfigFile = "testconfig.yaml"
80class ConfigTestPathlib(ConfigTest):
81 """Config with default using `pathlib.Path`."""
83 defaultConfigFile = Path("testconfig.yaml")
86class ConfigTestEmpty(ConfigTest):
87 """Config pointing to empty file."""
89 defaultConfigFile = "testconfig_empty.yaml"
90 requiredKeys = ()
93class ConfigTestButlerDir(ConfigTest):
94 """Simple config."""
96 defaultConfigFile = "testConfigs/testconfig.yaml"
99class ConfigTestNoDefaults(ConfigTest):
100 """Test config with no defaults."""
102 defaultConfigFile = None
103 requiredKeys = ()
106class ConfigTestAbsPath(ConfigTest):
107 """Test config with absolute paths."""
109 defaultConfigFile = None
110 requiredKeys = ()
113class ConfigTestCls(ConfigTest):
114 """Test config."""
116 defaultConfigFile = "withcls.yaml"
119class ConfigTestCase(unittest.TestCase):
120 """Tests of simple Config"""
122 def testBadConfig(self):
123 for badArg in (
124 [], # Bad argument
125 __file__, # Bad file extension for existing file
126 ):
127 with self.assertRaises(RuntimeError):
128 Config(badArg)
129 for badArg in (
130 "file.fits", # File that does not exist with bad extension
131 "b/c/d/", # Directory that does not exist
132 "file.yaml", # Good extension for missing file
133 ):
134 with self.assertRaises(FileNotFoundError):
135 Config(badArg)
137 def testBasics(self):
138 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}})
139 pretty = c.ppprint()
140 self.assertIn("key3", pretty)
141 r = repr(c)
142 self.assertIn("key3", r)
143 regex = r"^Config\(\{.*\}\)$"
144 self.assertRegex(r, regex)
145 c2 = eval(r)
146 self.assertIn("1", c)
147 for n in c.names():
148 self.assertEqual(c2[n], c[n])
149 self.assertEqual(c, c2)
150 s = str(c)
151 self.assertIn("\n", s)
152 self.assertNotRegex(s, regex)
154 self.assertCountEqual(c.keys(), ["1", "3", "key3", "dict"])
155 self.assertEqual(list(c), list(c.keys()))
156 self.assertEqual(list(c.values()), [c[k] for k in c.keys()])
157 self.assertEqual(list(c.items()), [(k, c[k]) for k in c.keys()])
159 newKeys = ("key4", ".dict.q", ("dict", "r"), "5")
160 oldKeys = ("key3", ".dict.a", ("dict", "b"), "3")
161 remainingKey = "1"
163 # Check get with existing key
164 for k in oldKeys:
165 self.assertEqual(c.get(k, "missing"), c[k])
167 # Check get, pop with nonexistent key
168 for k in newKeys:
169 self.assertEqual(c.get(k, "missing"), "missing")
170 self.assertEqual(c.pop(k, "missing"), "missing")
172 # Check setdefault with existing key
173 for k in oldKeys:
174 c.setdefault(k, 8)
175 self.assertNotEqual(c[k], 8)
177 # Check setdefault with nonexistent key (mutates c, adding newKeys)
178 for k in newKeys:
179 c.setdefault(k, 8)
180 self.assertEqual(c[k], 8)
182 # Check pop with existing key (mutates c, removing newKeys)
183 for k in newKeys:
184 v = c[k]
185 self.assertEqual(c.pop(k, "missing"), v)
187 # Check deletion (mutates c, removing oldKeys)
188 for k in ("key3", ".dict.a", ("dict", "b"), "3"):
189 self.assertIn(k, c)
190 del c[k]
191 self.assertNotIn(k, c)
193 # Check that `dict` still exists, but is now empty (then remove
194 # it, mutatic c)
195 self.assertIn("dict", c)
196 del c["dict"]
198 # Check popitem (mutates c, removing remainingKey)
199 v = c[remainingKey]
200 self.assertEqual(c.popitem(), (remainingKey, v))
202 # Check that c is now empty
203 self.assertFalse(c)
205 def testDict(self):
206 """Test toDict()."""
207 c1 = Config({"a": {"b": 1}, "c": 2})
208 self.assertIsInstance(c1["a"], Config)
209 d1 = c1.toDict()
210 self.assertIsInstance(d1["a"], dict)
211 self.assertEqual(d1["a"], c1["a"])
213 # Modifying one does not change the other
214 d1["a"]["c"] = 2
215 self.assertNotEqual(d1["a"], c1["a"])
217 def assertSplit(self, answer, *args):
218 """Assert that string splitting was correct."""
219 for s in (answer, *args):
220 split = Config._splitIntoKeys(s)
221 self.assertEqual(split, answer)
223 def testSplitting(self):
224 """Test of the internal splitting API."""
225 # Try lots of keys that will return the same answer
226 answer = ["a", "b", "c", "d"]
227 self.assertSplit(answer, ".a.b.c.d", ":a:b:c:d", "\ta\tb\tc\td", "\ra\rb\rc\rd")
229 answer = ["a", "calexp.wcs", "b"]
230 self.assertSplit(answer, r".a.calexp\.wcs.b", ":a:calexp.wcs:b")
232 self.assertSplit(["a.b.c"])
233 self.assertSplit(["a", r"b\.c"], r"_a_b\.c")
235 # Escaping a backslash before a delimiter currently fails
236 with self.assertRaises(ValueError):
237 Config._splitIntoKeys(r".a.calexp\\.wcs.b")
239 # The next two fail because internally \r is magic when escaping
240 # a delimiter.
241 with self.assertRaises(ValueError):
242 Config._splitIntoKeys("\ra\rcalexp\\\rwcs\rb")
244 with self.assertRaises(ValueError):
245 Config._splitIntoKeys(".a.cal\rexp\\.wcs.b")
247 def testEscape(self):
248 c = Config({"a": {"foo.bar": 1}, "b😂c": {"bar_baz": 2}})
249 self.assertEqual(c[r".a.foo\.bar"], 1)
250 self.assertEqual(c[":a:foo.bar"], 1)
251 self.assertEqual(c[".b😂c.bar_baz"], 2)
252 self.assertEqual(c[r"😂b\😂c😂bar_baz"], 2)
253 self.assertEqual(c[r"\a\foo.bar"], 1)
254 self.assertEqual(c["\ra\rfoo.bar"], 1)
255 with self.assertRaises(ValueError):
256 c[".a.foo\\.bar\r"]
258 def testOperators(self):
259 c1 = Config({"a": {"b": 1}, "c": 2})
260 c2 = c1.copy()
261 self.assertEqual(c1, c2)
262 self.assertIsInstance(c2, Config)
263 c2[".a.b"] = 5
264 self.assertNotEqual(c1, c2)
266 def testMerge(self):
267 c1 = Config({"a": 1, "c": 3})
268 c2 = Config({"a": 4, "b": 2})
269 c1.merge(c2)
270 self.assertEqual(c1, {"a": 1, "b": 2, "c": 3})
272 # Check that c2 was not changed
273 self.assertEqual(c2, {"a": 4, "b": 2})
275 # Repeat with a simple dict
276 c1.merge({"b": 5, "d": 42})
277 self.assertEqual(c1, {"a": 1, "b": 2, "c": 3, "d": 42})
279 with self.assertRaises(TypeError):
280 c1.merge([1, 2, 3])
282 def testUpdate(self):
283 c = Config({"a": {"b": 1}})
284 c.update({"a": {"c": 2}})
285 self.assertEqual(c[".a.b"], 1)
286 self.assertEqual(c[".a.c"], 2)
287 c.update({"a": {"d": [3, 4]}})
288 self.assertEqual(c[".a.d.0"], 3)
289 c.update({"z": [5, 6, {"g": 2, "h": 3}]})
290 self.assertEqual(c[".z.1"], 6)
292 # This is detached from parent
293 c2 = c[".z.2"]
294 self.assertEqual(c2["g"], 2)
295 c2.update({"h": 4, "j": 5})
296 self.assertEqual(c2["h"], 4)
297 self.assertNotIn(".z.2.j", c)
298 self.assertNotEqual(c[".z.2.h"], 4)
300 with self.assertRaises(RuntimeError):
301 c.update([1, 2, 3])
303 def testHierarchy(self):
304 c = Config()
306 # Simple dict
307 c["a"] = {"z": 52, "x": "string"}
308 self.assertIn(".a.z", c)
309 self.assertEqual(c[".a.x"], "string")
311 # Try different delimiters
312 self.assertEqual(c["⇛a⇛z"], 52)
313 self.assertEqual(c[("a", "z")], 52)
314 self.assertEqual(c["a", "z"], 52)
316 c[".b.new.thing1"] = "thing1"
317 c[".b.new.thing2"] = "thing2"
318 c[".b.new.thing3.supp"] = "supplemental"
319 self.assertEqual(c[".b.new.thing1"], "thing1")
320 tmp = c[".b.new"]
321 self.assertEqual(tmp["thing2"], "thing2")
322 self.assertEqual(c[".b.new.thing3.supp"], "supplemental")
324 # Test that we can index into lists
325 c[".a.b.c"] = [1, "7", 3, {"1": 4, "5": "Five"}, "hello"]
326 self.assertIn(".a.b.c.3.5", c)
327 self.assertNotIn(".a.b.c.10", c)
328 self.assertNotIn(".a.b.c.10.d", c)
329 self.assertEqual(c[".a.b.c.3.5"], "Five")
330 # Is the value in the list?
331 self.assertIn(".a.b.c.hello", c)
332 self.assertNotIn(".a.b.c.hello.not", c)
334 # And assign to an element in the list
335 self.assertEqual(c[".a.b.c.1"], "7")
336 c[".a.b.c.1"] = 8
337 self.assertEqual(c[".a.b.c.1"], 8)
338 self.assertIsInstance(c[".a.b.c"], collections.abc.Sequence)
340 # Test we do get lists back from asArray
341 a = c.asArray(".a.b.c")
342 self.assertIsInstance(a, list)
344 # Is it the *same* list as in the config
345 a.append("Sentinel")
346 self.assertIn("Sentinel", c[".a.b.c"])
347 self.assertIn(".a.b.c.Sentinel", c)
349 # Test we always get a list
350 for k in c.names():
351 a = c.asArray(k)
352 self.assertIsInstance(a, list)
354 # Check we get the same top level keys
355 self.assertEqual(set(c.names(topLevelOnly=True)), set(c._data.keys()))
357 # Check that we can iterate through items
358 for k, v in c.items():
359 self.assertEqual(c[k], v)
361 # Check that lists still work even if assigned a dict
362 c = Config(
363 {
364 "cls": "lsst.daf.butler",
365 "formatters": {"calexp.wcs": "{component}", "calexp": "{datasetType}"},
366 "datastores": [{"datastore": {"cls": "datastore1"}}, {"datastore": {"cls": "datastore2"}}],
367 }
368 )
369 c[".datastores.1.datastore"] = {"cls": "datastore2modified"}
370 self.assertEqual(c[".datastores.0.datastore.cls"], "datastore1")
371 self.assertEqual(c[".datastores.1.datastore.cls"], "datastore2modified")
372 self.assertIsInstance(c["datastores"], collections.abc.Sequence)
374 # Test that we can get all the listed names.
375 # and also that they are marked as "in" the Config
376 # Try delimited names and tuples
377 for n in itertools.chain(c.names(), c.nameTuples()):
378 val = c[n]
379 self.assertIsNotNone(val)
380 self.assertIn(n, c)
382 names = c.names()
383 nameTuples = c.nameTuples()
384 self.assertEqual(len(names), len(nameTuples))
385 self.assertEqual(len(names), 11)
386 self.assertEqual(len(nameTuples), 11)
388 # Test that delimiter escaping works
389 names = c.names(delimiter=".")
390 for n in names:
391 self.assertIn(n, c)
392 self.assertIn(".formatters.calexp\\.wcs", names)
394 # Use a name that includes the internal default delimiter
395 # to test automatic adjustment of delimiter
396 strangeKey = f"calexp{c._D}wcs"
397 c["formatters", strangeKey] = "dynamic"
398 names = c.names()
399 self.assertIn(strangeKey, "-".join(names))
400 self.assertFalse(names[0].startswith(c._D))
401 for n in names:
402 self.assertIn(n, c)
404 top = c.nameTuples(topLevelOnly=True)
405 self.assertIsInstance(top[0], tuple)
407 # Investigate a possible delimeter in a key
408 c = Config({"formatters": {"calexp.wcs": 2, "calexp": 3}})
409 self.assertEqual(c[":formatters:calexp.wcs"], 2)
410 self.assertEqual(c[":formatters:calexp"], 3)
411 for k, v in c["formatters"].items():
412 self.assertEqual(c["formatters", k], v)
414 # Check internal delimiter inheritance
415 c._D = "."
416 c2 = c["formatters"]
417 self.assertEqual(c._D, c2._D) # Check that the child inherits
418 self.assertNotEqual(c2._D, Config._D)
420 def testSerializedString(self):
421 """Test that we can create configs from strings"""
422 serialized = {
423 "yaml": """
424testing: hello
425formatters:
426 calexp: 3""",
427 "json": '{"testing": "hello", "formatters": {"calexp": 3}}',
428 }
430 for format, string in serialized.items():
431 c = Config.fromString(string, format=format)
432 self.assertEqual(c["formatters", "calexp"], 3)
433 self.assertEqual(c["testing"], "hello")
435 with self.assertRaises(ValueError):
436 Config.fromString("", format="unknown")
438 with self.assertRaises(ValueError):
439 Config.fromString(serialized["yaml"], format="json")
441 # This JSON can be parsed by YAML parser
442 j = Config.fromString(serialized["json"])
443 y = Config.fromString(serialized["yaml"])
444 self.assertEqual(j["formatters", "calexp"], 3)
445 self.assertEqual(j.toDict(), y.toDict())
447 # Round trip JSON -> Config -> YAML -> Config -> JSON -> Config
448 c1 = Config.fromString(serialized["json"], format="json")
449 yaml = c1.dump(format="yaml")
450 c2 = Config.fromString(yaml, format="yaml")
451 json = c2.dump(format="json")
452 c3 = Config.fromString(json, format="json")
453 self.assertEqual(c3.toDict(), c1.toDict())
456class ConfigSubsetTestCase(unittest.TestCase):
457 """Tests for ConfigSubset"""
459 def setUp(self):
460 self.testDir = os.path.abspath(os.path.dirname(__file__))
461 self.configDir = os.path.join(self.testDir, "config", "testConfigs")
462 self.configDir2 = os.path.join(self.testDir, "config", "testConfigs", "test2")
463 self.configDir3 = os.path.join(self.testDir, "config", "testConfigs", "test3")
465 def testEmpty(self):
466 """Ensure that we can read an empty file."""
467 c = ConfigTestEmpty(searchPaths=(self.configDir,))
468 self.assertIsInstance(c, ConfigSubset)
470 def testPathlib(self):
471 """Ensure that we can read an empty file."""
472 c = ConfigTestPathlib(searchPaths=(self.configDir,))
473 self.assertIsInstance(c, ConfigSubset)
475 def testDefaults(self):
476 """Read of defaults"""
477 # Supply the search path explicitly
478 c = ConfigTest(searchPaths=(self.configDir,))
479 self.assertIsInstance(c, ConfigSubset)
480 self.assertIn("item3", c)
481 self.assertEqual(c["item3"], 3)
483 # Use environment
484 with modified_environment(DAF_BUTLER_CONFIG_PATH=self.configDir):
485 c = ConfigTest()
486 self.assertIsInstance(c, ConfigSubset)
487 self.assertEqual(c["item3"], 3)
489 # No default so this should fail
490 with self.assertRaises(KeyError):
491 c = ConfigTest()
493 def testExternalOverride(self):
494 """Ensure that external values win"""
495 c = ConfigTest({"item3": "newval"}, searchPaths=(self.configDir,))
496 self.assertIn("item3", c)
497 self.assertEqual(c["item3"], "newval")
499 def testSearchPaths(self):
500 """Two search paths"""
501 c = ConfigTest(searchPaths=(self.configDir2, self.configDir))
502 self.assertIsInstance(c, ConfigSubset)
503 self.assertIn("item3", c)
504 self.assertEqual(c["item3"], "override")
505 self.assertEqual(c["item4"], "new")
507 c = ConfigTest(searchPaths=(self.configDir, self.configDir2))
508 self.assertIsInstance(c, ConfigSubset)
509 self.assertIn("item3", c)
510 self.assertEqual(c["item3"], 3)
511 self.assertEqual(c["item4"], "new")
513 def testExternalHierarchy(self):
514 """Test that we can provide external config parameters in hierarchy"""
515 c = ConfigTest({"comp": {"item1": 6, "item2": "a", "a": "b", "item3": 7}, "item4": 8})
516 self.assertIn("a", c)
517 self.assertEqual(c["a"], "b")
518 self.assertNotIn("item4", c)
520 def testNoDefaults(self):
521 """Ensure that defaults can be turned off."""
522 # Mandatory keys but no defaults
523 c = ConfigTest({"item1": "a", "item2": "b", "item6": 6})
524 self.assertEqual(len(c.filesRead), 0)
525 self.assertIn("item1", c)
526 self.assertEqual(c["item6"], 6)
528 c = ConfigTestNoDefaults()
529 self.assertEqual(len(c.filesRead), 0)
531 def testAbsPath(self):
532 """Read default config from an absolute path"""
533 # Force the path to be absolute in the class
534 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, "abspath.yaml")
535 c = ConfigTestAbsPath()
536 self.assertEqual(c["item11"], "eleventh")
537 self.assertEqual(len(c.filesRead), 1)
539 # Now specify the normal config file with an absolute path
540 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, ConfigTest.defaultConfigFile)
541 c = ConfigTestAbsPath()
542 self.assertEqual(c["item11"], 11)
543 self.assertEqual(len(c.filesRead), 1)
545 # and a search path that will also include the file
546 c = ConfigTestAbsPath(
547 searchPaths=(
548 self.configDir,
549 self.configDir2,
550 )
551 )
552 self.assertEqual(c["item11"], 11)
553 self.assertEqual(len(c.filesRead), 1)
555 # Same as above but this time with relative path and two search paths
556 # to ensure the count changes
557 ConfigTestAbsPath.defaultConfigFile = ConfigTest.defaultConfigFile
558 c = ConfigTestAbsPath(
559 searchPaths=(
560 self.configDir,
561 self.configDir2,
562 )
563 )
564 self.assertEqual(len(c.filesRead), 2)
566 # Reset the class
567 ConfigTestAbsPath.defaultConfigFile = None
569 def testClassDerived(self):
570 """Read config specified in class determined from config"""
571 c = ConfigTestCls(searchPaths=(self.configDir,))
572 self.assertEqual(c["item50"], 50)
573 self.assertEqual(c["help"], "derived")
575 # Same thing but additional search path
576 c = ConfigTestCls(searchPaths=(self.configDir, self.configDir2))
577 self.assertEqual(c["item50"], 50)
578 self.assertEqual(c["help"], "derived")
579 self.assertEqual(c["help2"], "second")
581 # Same thing but reverse the two paths
582 c = ConfigTestCls(searchPaths=(self.configDir2, self.configDir))
583 self.assertEqual(c["item50"], 500)
584 self.assertEqual(c["help"], "class")
585 self.assertEqual(c["help2"], "second")
586 self.assertEqual(c["help3"], "third")
588 def testInclude(self):
589 """Read a config that has an include directive"""
590 c = Config(os.path.join(self.configDir, "testinclude.yaml"))
591 self.assertEqual(c[".comp1.item1"], 58)
592 self.assertEqual(c[".comp2.comp.item1"], 1)
593 self.assertEqual(c[".comp3.1.comp.item1"], "posix")
594 self.assertEqual(c[".comp4.0.comp.item1"], "posix")
595 self.assertEqual(c[".comp4.1.comp.item1"], 1)
596 self.assertEqual(c[".comp5.comp6.comp.item1"], "posix")
598 # Test a specific name and then test that all
599 # returned names are "in" the config.
600 names = c.names()
601 self.assertIn(c._D.join(("", "comp3", "1", "comp", "item1")), names)
602 for n in names:
603 self.assertIn(n, c)
605 # Test that override delimiter works
606 delimiter = "-"
607 names = c.names(delimiter=delimiter)
608 self.assertIn(delimiter.join(("", "comp3", "1", "comp", "item1")), names)
610 def testStringInclude(self):
611 """Using include directives in strings"""
612 # See if include works for absolute path
613 c = Config.fromYaml(f"something: !include {os.path.join(self.configDir, 'testconfig.yaml')}")
614 self.assertEqual(c["something", "comp", "item3"], 3)
616 with self.assertRaises(FileNotFoundError) as cm:
617 Config.fromYaml("something: !include /not/here.yaml")
618 # Test that it really was trying to open the absolute path
619 self.assertIn("'/not/here.yaml'", str(cm.exception))
621 def testIncludeConfigs(self):
622 """Test the special includeConfigs key for pulling in additional
623 files.
624 """
625 c = Config(os.path.join(self.configDir, "configIncludes.yaml"))
626 self.assertEqual(c["comp", "item2"], "hello")
627 self.assertEqual(c["comp", "item50"], 5000)
628 self.assertEqual(c["comp", "item1"], "first")
629 self.assertEqual(c["comp", "item10"], "tenth")
630 self.assertEqual(c["comp", "item11"], "eleventh")
631 self.assertEqual(c["unrelated"], 1)
632 self.assertEqual(c["addon", "comp", "item1"], "posix")
633 self.assertEqual(c["addon", "comp", "item11"], -1)
634 self.assertEqual(c["addon", "comp", "item50"], 500)
636 c = Config(os.path.join(self.configDir, "configIncludes.json"))
637 self.assertEqual(c["comp", "item2"], "hello")
638 self.assertEqual(c["comp", "item50"], 5000)
639 self.assertEqual(c["comp", "item1"], "first")
640 self.assertEqual(c["comp", "item10"], "tenth")
641 self.assertEqual(c["comp", "item11"], "eleventh")
642 self.assertEqual(c["unrelated"], 1)
643 self.assertEqual(c["addon", "comp", "item1"], "posix")
644 self.assertEqual(c["addon", "comp", "item11"], -1)
645 self.assertEqual(c["addon", "comp", "item50"], 500)
647 # Now test with an environment variable in includeConfigs
648 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir3):
649 c = Config(os.path.join(self.configDir, "configIncludesEnv.yaml"))
650 self.assertEqual(c["comp", "item2"], "hello")
651 self.assertEqual(c["comp", "item50"], 5000)
652 self.assertEqual(c["comp", "item1"], "first")
653 self.assertEqual(c["comp", "item10"], "tenth")
654 self.assertEqual(c["comp", "item11"], "eleventh")
655 self.assertEqual(c["unrelated"], 1)
656 self.assertEqual(c["addon", "comp", "item1"], "envvar")
657 self.assertEqual(c["addon", "comp", "item11"], -1)
658 self.assertEqual(c["addon", "comp", "item50"], 501)
660 # This will fail
661 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir2):
662 with self.assertRaises(FileNotFoundError):
663 Config(os.path.join(self.configDir, "configIncludesEnv.yaml"))
665 def testResource(self):
666 c = Config("resource://lsst.daf.butler/configs/datastore.yaml")
667 self.assertIn("datastore", c)
669 # Test that we can include a resource URI
670 yaml = """
671toplevel: true
672resource: !include resource://lsst.daf.butler/configs/datastore.yaml
673"""
674 c = Config.fromYaml(yaml)
675 self.assertIn(("resource", "datastore", "cls"), c)
677 # Test that we can include a resource URI with includeConfigs
678 yaml = """
679toplevel: true
680resource:
681 includeConfigs: resource://lsst.daf.butler/configs/datastore.yaml
682"""
683 c = Config.fromYaml(yaml)
684 self.assertIn(("resource", "datastore", "cls"), c)
687class FileWriteConfigTestCase(unittest.TestCase):
688 """Test writing of configs."""
690 def setUp(self):
691 self.tmpdir = makeTestTempDir(TESTDIR)
693 def tearDown(self):
694 removeTestTempDir(self.tmpdir)
696 def testDump(self):
697 """Test that we can write and read a configuration."""
698 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}})
700 for format in ("yaml", "json"):
701 outpath = os.path.join(self.tmpdir, f"test.{format}")
702 c.dumpToUri(outpath)
704 c2 = Config(outpath)
705 self.assertEqual(c2, c)
707 c.dumpToUri(outpath, overwrite=True)
708 with self.assertRaises(FileExistsError):
709 c.dumpToUri(outpath, overwrite=False)
712if __name__ == "__main__":
713 unittest.main()