Coverage for tests/test_config.py : 14%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 unittest
23import os
24import contextlib
25import collections
26import itertools
27import shutil
28import tempfile
30from lsst.daf.butler import ConfigSubset, Config
33@contextlib.contextmanager
34def modified_environment(**environ):
35 """
36 Temporarily set environment variables.
38 >>> with modified_environment(DAF_BUTLER_CONFIG_PATHS="/somewhere"):
39 ... os.environ["DAF_BUTLER_CONFIG_PATHS"] == "/somewhere"
40 True
42 >>> "DAF_BUTLER_CONFIG_PATHS" != "/somewhere"
43 True
45 Parameters
46 ----------
47 environ : `dict`
48 Key value pairs of environment variables to temporarily set.
49 """
50 old_environ = dict(os.environ)
51 os.environ.update(environ)
52 try:
53 yield
54 finally:
55 os.environ.clear()
56 os.environ.update(old_environ)
59class ExampleWithConfigFileReference:
60 defaultConfigFile = "viacls.yaml"
63class ExampleWithConfigFileReference2:
64 defaultConfigFile = "viacls2.yaml"
67class ConfigTest(ConfigSubset):
68 component = "comp"
69 requiredKeys = ("item1", "item2")
70 defaultConfigFile = "testconfig.yaml"
73class ConfigTestEmpty(ConfigTest):
74 defaultConfigFile = "testconfig_empty.yaml"
75 requiredKeys = ()
78class ConfigTestButlerDir(ConfigTest):
79 defaultConfigFile = "testConfigs/testconfig.yaml"
82class ConfigTestNoDefaults(ConfigTest):
83 defaultConfigFile = None
84 requiredKeys = ()
87class ConfigTestAbsPath(ConfigTest):
88 defaultConfigFile = None
89 requiredKeys = ()
92class ConfigTestCls(ConfigTest):
93 defaultConfigFile = "withcls.yaml"
96class ConfigTestCase(unittest.TestCase):
97 """Tests of simple Config"""
99 def testBadConfig(self):
100 for badArg in ([], "file.fits"):
101 with self.assertRaises(RuntimeError):
102 Config(badArg)
104 def testBasics(self):
105 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}})
106 pretty = c.ppprint()
107 self.assertIn("key3", pretty)
108 r = repr(c)
109 self.assertIn("key3", r)
110 regex = r"^Config\(\{.*\}\)$"
111 self.assertRegex(r, regex)
112 c2 = eval(r)
113 self.assertIn("1", c)
114 for n in c.names():
115 self.assertEqual(c2[n], c[n])
116 self.assertEqual(c, c2)
117 s = str(c)
118 self.assertIn("\n", s)
119 self.assertNotRegex(s, regex)
121 self.assertCountEqual(c.keys(), ["1", "3", "key3", "dict"])
122 self.assertEqual(list(c), list(c.keys()))
123 self.assertEqual(list(c.values()), [c[k] for k in c.keys()])
124 self.assertEqual(list(c.items()), [(k, c[k]) for k in c.keys()])
126 newKeys = ("key4", ".dict.q", ("dict", "r"), "5")
127 oldKeys = ("key3", ".dict.a", ("dict", "b"), "3")
128 remainingKey = "1"
130 # Check get with existing key
131 for k in oldKeys:
132 self.assertEqual(c.get(k, "missing"), c[k])
134 # Check get, pop with nonexistent key
135 for k in newKeys:
136 self.assertEqual(c.get(k, "missing"), "missing")
137 self.assertEqual(c.pop(k, "missing"), "missing")
139 # Check setdefault with existing key
140 for k in oldKeys:
141 c.setdefault(k, 8)
142 self.assertNotEqual(c[k], 8)
144 # Check setdefault with nonexistent key (mutates c, adding newKeys)
145 for k in newKeys:
146 c.setdefault(k, 8)
147 self.assertEqual(c[k], 8)
149 # Check pop with existing key (mutates c, removing newKeys)
150 for k in newKeys:
151 v = c[k]
152 self.assertEqual(c.pop(k, "missing"), v)
154 # Check deletion (mutates c, removing oldKeys)
155 for k in ("key3", ".dict.a", ("dict", "b"), "3"):
156 self.assertIn(k, c)
157 del c[k]
158 self.assertNotIn(k, c)
160 # Check that `dict` still exists, but is now empty (then remove
161 # it, mutatic c)
162 self.assertIn("dict", c)
163 del c["dict"]
165 # Check popitem (mutates c, removing remainingKey)
166 v = c[remainingKey]
167 self.assertEqual(c.popitem(), (remainingKey, v))
169 # Check that c is now empty
170 self.assertFalse(c)
172 def testDict(self):
173 """Test toDict()"""
174 c1 = Config({"a": {"b": 1}, "c": 2})
175 self.assertIsInstance(c1["a"], Config)
176 d1 = c1.toDict()
177 self.assertIsInstance(d1["a"], dict)
178 self.assertEqual(d1["a"], c1["a"])
180 # Modifying one does not change the other
181 d1["a"]["c"] = 2
182 self.assertNotEqual(d1["a"], c1["a"])
184 def assertSplit(self, answer, *args):
185 """Helper function to compare string splitting"""
186 for s in (answer, *args):
187 split = Config._splitIntoKeys(s)
188 self.assertEqual(split, answer)
190 def testSplitting(self):
191 """Test of the internal splitting API."""
192 # Try lots of keys that will return the same answer
193 answer = ["a", "b", "c", "d"]
194 self.assertSplit(answer, ".a.b.c.d", ":a:b:c:d", "\ta\tb\tc\td", "\ra\rb\rc\rd")
196 answer = ["a", "calexp.wcs", "b"]
197 self.assertSplit(answer, r".a.calexp\.wcs.b", ":a:calexp.wcs:b")
199 self.assertSplit(["a.b.c"])
200 self.assertSplit(["a", r"b\.c"], r"_a_b\.c")
202 # Escaping a backslash before a delimiter currently fails
203 with self.assertRaises(ValueError):
204 Config._splitIntoKeys(r".a.calexp\\.wcs.b")
206 # The next two fail because internally \r is magic when escaping
207 # a delimiter.
208 with self.assertRaises(ValueError):
209 Config._splitIntoKeys("\ra\rcalexp\\\rwcs\rb")
211 with self.assertRaises(ValueError):
212 Config._splitIntoKeys(".a.cal\rexp\\.wcs.b")
214 def testEscape(self):
215 c = Config({"a": {"foo.bar": 1}, "b😂c": {"bar_baz": 2}})
216 self.assertEqual(c[r".a.foo\.bar"], 1)
217 self.assertEqual(c[":a:foo.bar"], 1)
218 self.assertEqual(c[".b😂c.bar_baz"], 2)
219 self.assertEqual(c[r"😂b\😂c😂bar_baz"], 2)
220 self.assertEqual(c[r"\a\foo.bar"], 1)
221 self.assertEqual(c["\ra\rfoo.bar"], 1)
222 with self.assertRaises(ValueError):
223 c[".a.foo\\.bar\r"]
225 def testOperators(self):
226 c1 = Config({"a": {"b": 1}, "c": 2})
227 c2 = c1.copy()
228 self.assertEqual(c1, c2)
229 self.assertIsInstance(c2, Config)
230 c2[".a.b"] = 5
231 self.assertNotEqual(c1, c2)
233 def testUpdate(self):
234 c = Config({"a": {"b": 1}})
235 c.update({"a": {"c": 2}})
236 self.assertEqual(c[".a.b"], 1)
237 self.assertEqual(c[".a.c"], 2)
238 c.update({"a": {"d": [3, 4]}})
239 self.assertEqual(c[".a.d.0"], 3)
240 c.update({"z": [5, 6, {"g": 2, "h": 3}]})
241 self.assertEqual(c[".z.1"], 6)
243 # This is detached from parent
244 c2 = c[".z.2"]
245 self.assertEqual(c2["g"], 2)
246 c2.update({"h": 4, "j": 5})
247 self.assertEqual(c2["h"], 4)
248 self.assertNotIn(".z.2.j", c)
249 self.assertNotEqual(c[".z.2.h"], 4)
251 with self.assertRaises(RuntimeError):
252 c.update([1, 2, 3])
254 def testHierarchy(self):
255 c = Config()
257 # Simple dict
258 c["a"] = {"z": 52, "x": "string"}
259 self.assertIn(".a.z", c)
260 self.assertEqual(c[".a.x"], "string")
262 # Try different delimiters
263 self.assertEqual(c["⇛a⇛z"], 52)
264 self.assertEqual(c[("a", "z")], 52)
265 self.assertEqual(c["a", "z"], 52)
267 c[".b.new.thing1"] = "thing1"
268 c[".b.new.thing2"] = "thing2"
269 c[".b.new.thing3.supp"] = "supplemental"
270 self.assertEqual(c[".b.new.thing1"], "thing1")
271 tmp = c[".b.new"]
272 self.assertEqual(tmp["thing2"], "thing2")
273 self.assertEqual(c[".b.new.thing3.supp"], "supplemental")
275 # Test that we can index into lists
276 c[".a.b.c"] = [1, "7", 3, {"1": 4, "5": "Five"}, "hello"]
277 self.assertIn(".a.b.c.3.5", c)
278 self.assertNotIn(".a.b.c.10", c)
279 self.assertNotIn(".a.b.c.10.d", c)
280 self.assertEqual(c[".a.b.c.3.5"], "Five")
281 # Is the value in the list?
282 self.assertIn(".a.b.c.hello", c)
283 self.assertNotIn(".a.b.c.hello.not", c)
285 # And assign to an element in the list
286 self.assertEqual(c[".a.b.c.1"], "7")
287 c[".a.b.c.1"] = 8
288 self.assertEqual(c[".a.b.c.1"], 8)
289 self.assertIsInstance(c[".a.b.c"], collections.abc.Sequence)
291 # Test we do get lists back from asArray
292 a = c.asArray(".a.b.c")
293 self.assertIsInstance(a, list)
295 # Is it the *same* list as in the config
296 a.append("Sentinel")
297 self.assertIn("Sentinel", c[".a.b.c"])
298 self.assertIn(".a.b.c.Sentinel", c)
300 # Test we always get a list
301 for k in c.names():
302 a = c.asArray(k)
303 self.assertIsInstance(a, list)
305 # Check we get the same top level keys
306 self.assertEqual(set(c.names(topLevelOnly=True)), set(c._data.keys()))
308 # Check that we can iterate through items
309 for k, v in c.items():
310 self.assertEqual(c[k], v)
312 # Check that lists still work even if assigned a dict
313 c = Config({"cls": "lsst.daf.butler",
314 "formatters": {"calexp.wcs": "{component}",
315 "calexp": "{datasetType}"},
316 "datastores": [{"datastore": {"cls": "datastore1"}},
317 {"datastore": {"cls": "datastore2"}}]})
318 c[".datastores.1.datastore"] = {"cls": "datastore2modified"}
319 self.assertEqual(c[".datastores.0.datastore.cls"], "datastore1")
320 self.assertEqual(c[".datastores.1.datastore.cls"], "datastore2modified")
321 self.assertIsInstance(c["datastores"], collections.abc.Sequence)
323 # Test that we can get all the listed names.
324 # and also that they are marked as "in" the Config
325 # Try delimited names and tuples
326 for n in itertools.chain(c.names(), c.nameTuples()):
327 val = c[n]
328 self.assertIsNotNone(val)
329 self.assertIn(n, c)
331 names = c.names()
332 nameTuples = c.nameTuples()
333 self.assertEqual(len(names), len(nameTuples))
334 self.assertEqual(len(names), 11)
335 self.assertEqual(len(nameTuples), 11)
337 # Test that delimiter escaping works
338 names = c.names(delimiter=".")
339 for n in names:
340 self.assertIn(n, c)
341 self.assertIn(".formatters.calexp\\.wcs", names)
343 # Use a name that includes the internal default delimiter
344 # to test automatic adjustment of delimiter
345 strangeKey = f"calexp{c._D}wcs"
346 c["formatters", strangeKey] = "dynamic"
347 names = c.names()
348 self.assertIn(strangeKey, "-".join(names))
349 self.assertFalse(names[0].startswith(c._D))
350 for n in names:
351 self.assertIn(n, c)
353 top = c.nameTuples(topLevelOnly=True)
354 self.assertIsInstance(top[0], tuple)
356 # Investigate a possible delimeter in a key
357 c = Config({"formatters": {"calexp.wcs": 2, "calexp": 3}})
358 self.assertEqual(c[":formatters:calexp.wcs"], 2)
359 self.assertEqual(c[":formatters:calexp"], 3)
360 for k, v in c["formatters"].items():
361 self.assertEqual(c["formatters", k], v)
363 # Check internal delimiter inheritance
364 c._D = "."
365 c2 = c["formatters"]
366 self.assertEqual(c._D, c2._D) # Check that the child inherits
367 self.assertNotEqual(c2._D, Config._D)
369 def testSerializedString(self):
370 """Test that we can create configs from strings"""
372 serialized = {
373 "yaml": """
374testing: hello
375formatters:
376 calexp: 3""",
377 "json": '{"testing": "hello", "formatters": {"calexp": 3}}'
378 }
380 for format, string in serialized.items():
381 c = Config.fromString(string, format=format)
382 self.assertEqual(c["formatters", "calexp"], 3)
383 self.assertEqual(c["testing"], "hello")
385 with self.assertRaises(ValueError):
386 Config.fromString("", format="unknown")
388 with self.assertRaises(ValueError):
389 Config.fromString(serialized["yaml"], format="json")
391 # This JSON can be parsed by YAML parser
392 j = Config.fromString(serialized["json"])
393 y = Config.fromString(serialized["yaml"])
394 self.assertEqual(j["formatters", "calexp"], 3)
395 self.assertEqual(j.toDict(), y.toDict())
397 # Round trip JSON -> Config -> YAML -> Config -> JSON -> Config
398 c1 = Config.fromString(serialized["json"], format="json")
399 yaml = c1.dump(format="yaml")
400 c2 = Config.fromString(yaml, format="yaml")
401 json = c2.dump(format="json")
402 c3 = Config.fromString(json, format="json")
403 self.assertEqual(c3.toDict(), c1.toDict())
406class ConfigSubsetTestCase(unittest.TestCase):
407 """Tests for ConfigSubset
408 """
410 def setUp(self):
411 self.testDir = os.path.abspath(os.path.dirname(__file__))
412 self.configDir = os.path.join(self.testDir, "config", "testConfigs")
413 self.configDir2 = os.path.join(self.testDir, "config", "testConfigs", "test2")
414 self.configDir3 = os.path.join(self.testDir, "config", "testConfigs", "test3")
416 def testEmpty(self):
417 """Ensure that we can read an empty file."""
418 c = ConfigTestEmpty(searchPaths=(self.configDir,))
419 self.assertIsInstance(c, ConfigSubset)
421 def testDefaults(self):
422 """Read of defaults"""
424 # Supply the search path explicitly
425 c = ConfigTest(searchPaths=(self.configDir,))
426 self.assertIsInstance(c, ConfigSubset)
427 self.assertIn("item3", c)
428 self.assertEqual(c["item3"], 3)
430 # Use environment
431 with modified_environment(DAF_BUTLER_CONFIG_PATH=self.configDir):
432 c = ConfigTest()
433 self.assertIsInstance(c, ConfigSubset)
434 self.assertEqual(c["item3"], 3)
436 # No default so this should fail
437 with self.assertRaises(KeyError):
438 c = ConfigTest()
440 def testExternalOverride(self):
441 """Ensure that external values win"""
442 c = ConfigTest({"item3": "newval"}, searchPaths=(self.configDir,))
443 self.assertIn("item3", c)
444 self.assertEqual(c["item3"], "newval")
446 def testSearchPaths(self):
447 """Two search paths"""
448 c = ConfigTest(searchPaths=(self.configDir2, self.configDir))
449 self.assertIsInstance(c, ConfigSubset)
450 self.assertIn("item3", c)
451 self.assertEqual(c["item3"], "override")
452 self.assertEqual(c["item4"], "new")
454 c = ConfigTest(searchPaths=(self.configDir, self.configDir2))
455 self.assertIsInstance(c, ConfigSubset)
456 self.assertIn("item3", c)
457 self.assertEqual(c["item3"], 3)
458 self.assertEqual(c["item4"], "new")
460 def testExternalHierarchy(self):
461 """Test that we can provide external config parameters in hierarchy"""
462 c = ConfigTest({"comp": {"item1": 6, "item2": "a", "a": "b",
463 "item3": 7}, "item4": 8})
464 self.assertIn("a", c)
465 self.assertEqual(c["a"], "b")
466 self.assertNotIn("item4", c)
468 def testNoDefaults(self):
469 """Ensure that defaults can be turned off."""
471 # Mandatory keys but no defaults
472 c = ConfigTest({"item1": "a", "item2": "b", "item6": 6})
473 self.assertEqual(len(c.filesRead), 0)
474 self.assertIn("item1", c)
475 self.assertEqual(c["item6"], 6)
477 c = ConfigTestNoDefaults()
478 self.assertEqual(len(c.filesRead), 0)
480 def testAbsPath(self):
481 """Read default config from an absolute path"""
482 # Force the path to be absolute in the class
483 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, "abspath.yaml")
484 c = ConfigTestAbsPath()
485 self.assertEqual(c["item11"], "eleventh")
486 self.assertEqual(len(c.filesRead), 1)
488 # Now specify the normal config file with an absolute path
489 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, ConfigTest.defaultConfigFile)
490 c = ConfigTestAbsPath()
491 self.assertEqual(c["item11"], 11)
492 self.assertEqual(len(c.filesRead), 1)
494 # and a search path that will also include the file
495 c = ConfigTestAbsPath(searchPaths=(self.configDir, self.configDir2,))
496 self.assertEqual(c["item11"], 11)
497 self.assertEqual(len(c.filesRead), 1)
499 # Same as above but this time with relative path and two search paths
500 # to ensure the count changes
501 ConfigTestAbsPath.defaultConfigFile = ConfigTest.defaultConfigFile
502 c = ConfigTestAbsPath(searchPaths=(self.configDir, self.configDir2,))
503 self.assertEqual(len(c.filesRead), 2)
505 # Reset the class
506 ConfigTestAbsPath.defaultConfigFile = None
508 def testClassDerived(self):
509 """Read config specified in class determined from config"""
510 c = ConfigTestCls(searchPaths=(self.configDir,))
511 self.assertEqual(c["item50"], 50)
512 self.assertEqual(c["help"], "derived")
514 # Same thing but additional search path
515 c = ConfigTestCls(searchPaths=(self.configDir, self.configDir2))
516 self.assertEqual(c["item50"], 50)
517 self.assertEqual(c["help"], "derived")
518 self.assertEqual(c["help2"], "second")
520 # Same thing but reverse the two paths
521 c = ConfigTestCls(searchPaths=(self.configDir2, self.configDir))
522 self.assertEqual(c["item50"], 500)
523 self.assertEqual(c["help"], "class")
524 self.assertEqual(c["help2"], "second")
525 self.assertEqual(c["help3"], "third")
527 def testInclude(self):
528 """Read a config that has an include directive"""
529 c = Config(os.path.join(self.configDir, "testinclude.yaml"))
530 self.assertEqual(c[".comp1.item1"], 58)
531 self.assertEqual(c[".comp2.comp.item1"], 1)
532 self.assertEqual(c[".comp3.1.comp.item1"], "posix")
533 self.assertEqual(c[".comp4.0.comp.item1"], "posix")
534 self.assertEqual(c[".comp4.1.comp.item1"], 1)
535 self.assertEqual(c[".comp5.comp6.comp.item1"], "posix")
537 # Test a specific name and then test that all
538 # returned names are "in" the config.
539 names = c.names()
540 self.assertIn(c._D.join(("", "comp3", "1", "comp", "item1")), names)
541 for n in names:
542 self.assertIn(n, c)
544 # Test that override delimiter works
545 delimiter = "-"
546 names = c.names(delimiter=delimiter)
547 self.assertIn(delimiter.join(("", "comp3", "1", "comp", "item1")), names)
549 def testStringInclude(self):
550 """Using include directives in strings"""
552 # See if include works for absolute path
553 c = Config.fromYaml(f"something: !include {os.path.join(self.configDir, 'testconfig.yaml')}")
554 self.assertEqual(c["something", "comp", "item3"], 3)
556 with self.assertRaises(FileNotFoundError) as cm:
557 Config.fromYaml("something: !include /not/here.yaml")
558 # Test that it really was trying to open the absolute path
559 self.assertIn("'/not/here.yaml'", str(cm.exception))
561 def testIncludeConfigs(self):
562 """Test the special includeConfigs key for pulling in additional
563 files."""
564 c = Config(os.path.join(self.configDir, "configIncludes.yaml"))
565 self.assertEqual(c["comp", "item2"], "hello")
566 self.assertEqual(c["comp", "item50"], 5000)
567 self.assertEqual(c["comp", "item1"], "first")
568 self.assertEqual(c["comp", "item10"], "tenth")
569 self.assertEqual(c["comp", "item11"], "eleventh")
570 self.assertEqual(c["unrelated"], 1)
571 self.assertEqual(c["addon", "comp", "item1"], "posix")
572 self.assertEqual(c["addon", "comp", "item11"], -1)
573 self.assertEqual(c["addon", "comp", "item50"], 500)
575 c = Config(os.path.join(self.configDir, "configIncludes.json"))
576 self.assertEqual(c["comp", "item2"], "hello")
577 self.assertEqual(c["comp", "item50"], 5000)
578 self.assertEqual(c["comp", "item1"], "first")
579 self.assertEqual(c["comp", "item10"], "tenth")
580 self.assertEqual(c["comp", "item11"], "eleventh")
581 self.assertEqual(c["unrelated"], 1)
582 self.assertEqual(c["addon", "comp", "item1"], "posix")
583 self.assertEqual(c["addon", "comp", "item11"], -1)
584 self.assertEqual(c["addon", "comp", "item50"], 500)
586 # Now test with an environment variable in includeConfigs
587 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir3):
588 c = Config(os.path.join(self.configDir, "configIncludesEnv.yaml"))
589 self.assertEqual(c["comp", "item2"], "hello")
590 self.assertEqual(c["comp", "item50"], 5000)
591 self.assertEqual(c["comp", "item1"], "first")
592 self.assertEqual(c["comp", "item10"], "tenth")
593 self.assertEqual(c["comp", "item11"], "eleventh")
594 self.assertEqual(c["unrelated"], 1)
595 self.assertEqual(c["addon", "comp", "item1"], "envvar")
596 self.assertEqual(c["addon", "comp", "item11"], -1)
597 self.assertEqual(c["addon", "comp", "item50"], 501)
599 # This will fail
600 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir2):
601 with self.assertRaises(FileNotFoundError):
602 Config(os.path.join(self.configDir, "configIncludesEnv.yaml"))
604 def testResource(self):
605 c = Config("resource://lsst.daf.butler/configs/datastore.yaml")
606 self.assertIn("datastore", c)
608 # Test that we can include a resource URI
609 yaml = """
610toplevel: true
611resource: !include resource://lsst.daf.butler/configs/datastore.yaml
612"""
613 c = Config.fromYaml(yaml)
614 self.assertIn(("resource", "datastore", "cls"), c)
616 # Test that we can include a resource URI with includeConfigs
617 yaml = """
618toplevel: true
619resource:
620 includeConfigs: resource://lsst.daf.butler/configs/datastore.yaml
621"""
622 c = Config.fromYaml(yaml)
623 self.assertIn(("resource", "datastore", "cls"), c)
626class FileWriteConfigTestCase(unittest.TestCase):
628 def setUp(self):
629 self.tmpdir = tempfile.mkdtemp()
631 def tearDown(self):
632 if os.path.exists(self.tmpdir):
633 shutil.rmtree(self.tmpdir, ignore_errors=True)
635 def testDump(self):
636 """Test that we can write and read a configuration."""
638 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}})
640 for format in ("yaml", "json"):
641 outpath = os.path.join(self.tmpdir, f"test.{format}")
642 c.dumpToUri(outpath)
644 c2 = Config(outpath)
645 self.assertEqual(c2, c)
647 c.dumpToUri(outpath, overwrite=True)
648 with self.assertRaises(FileExistsError):
649 c.dumpToUri(outpath, overwrite=False)
652if __name__ == "__main__": 652 ↛ 653line 652 didn't jump to line 653, because the condition on line 652 was never true
653 unittest.main()