Coverage for tests/test_config.py: 14%

415 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-06 10:53 +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/>. 

27 

28import collections 

29import contextlib 

30import itertools 

31import os 

32import unittest 

33from pathlib import Path 

34 

35from lsst.daf.butler import Config, ConfigSubset 

36from lsst.daf.butler.tests.utils import makeTestTempDir, removeTestTempDir 

37 

38TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

39 

40 

41@contextlib.contextmanager 

42def modified_environment(**environ): 

43 """Temporarily set environment variables. 

44 

45 >>> with modified_environment(DAF_BUTLER_CONFIG_PATHS="/somewhere"): 

46 ... os.environ["DAF_BUTLER_CONFIG_PATHS"] == "/somewhere" 

47 True 

48 

49 >>> "DAF_BUTLER_CONFIG_PATHS" != "/somewhere" 

50 True 

51 

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) 

64 

65 

66class ExampleWithConfigFileReference: 

67 """Example class referenced from config file.""" 

68 

69 defaultConfigFile = "viacls.yaml" 

70 

71 

72class ExampleWithConfigFileReference2: 

73 """Example class referenced from config file.""" 

74 

75 defaultConfigFile = "viacls2.yaml" 

76 

77 

78class ConfigTest(ConfigSubset): 

79 """Default config class for testing.""" 

80 

81 component = "comp" 

82 requiredKeys = ("item1", "item2") 

83 defaultConfigFile = "testconfig.yaml" 

84 

85 

86class ConfigTestPathlib(ConfigTest): 

87 """Config with default using `pathlib.Path`.""" 

88 

89 defaultConfigFile = Path("testconfig.yaml") 

90 

91 

92class ConfigTestEmpty(ConfigTest): 

93 """Config pointing to empty file.""" 

94 

95 defaultConfigFile = "testconfig_empty.yaml" 

96 requiredKeys = () 

97 

98 

99class ConfigTestButlerDir(ConfigTest): 

100 """Simple config.""" 

101 

102 defaultConfigFile = "testConfigs/testconfig.yaml" 

103 

104 

105class ConfigTestNoDefaults(ConfigTest): 

106 """Test config with no defaults.""" 

107 

108 defaultConfigFile = None 

109 requiredKeys = () 

110 

111 

112class ConfigTestAbsPath(ConfigTest): 

113 """Test config with absolute paths.""" 

114 

115 defaultConfigFile = None 

116 requiredKeys = () 

117 

118 

119class ConfigTestCls(ConfigTest): 

120 """Test config.""" 

121 

122 defaultConfigFile = "withcls.yaml" 

123 

124 

125class ConfigTestCase(unittest.TestCase): 

126 """Tests of simple Config""" 

127 

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): 

141 Config(badArg) 

142 

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) 

159 

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]) 

164 

165 newKeys = ("key4", ".dict.q", ("dict", "r"), "5") 

166 oldKeys = ("key3", ".dict.a", ("dict", "b"), "3") 

167 remainingKey = "1" 

168 

169 # Check get with existing key 

170 for k in oldKeys: 

171 self.assertEqual(c.get(k, "missing"), c[k]) 

172 

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") 

177 

178 # Check setdefault with existing key 

179 for k in oldKeys: 

180 c.setdefault(k, 8) 

181 self.assertNotEqual(c[k], 8) 

182 

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) 

187 

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) 

192 

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) 

198 

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"] 

203 

204 # Check popitem (mutates c, removing remainingKey) 

205 v = c[remainingKey] 

206 self.assertEqual(c.popitem(), (remainingKey, v)) 

207 

208 # Check that c is now empty 

209 self.assertFalse(c) 

210 

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"]) 

218 

219 # Modifying one does not change the other 

220 d1["a"]["c"] = 2 

221 self.assertNotEqual(d1["a"], c1["a"]) 

222 

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) 

228 

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") 

234 

235 answer = ["a", "calexp.wcs", "b"] 

236 self.assertSplit(answer, r".a.calexp\.wcs.b", ":a:calexp.wcs:b") 

237 

238 self.assertSplit(["a.b.c"]) 

239 self.assertSplit(["a", r"b\.c"], r"_a_b\.c") 

240 

241 # Escaping a backslash before a delimiter currently fails 

242 with self.assertRaises(ValueError): 

243 Config._splitIntoKeys(r".a.calexp\\.wcs.b") 

244 

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") 

249 

250 with self.assertRaises(ValueError): 

251 Config._splitIntoKeys(".a.cal\rexp\\.wcs.b") 

252 

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"] 

263 

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) 

271 

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}) 

277 

278 # Check that c2 was not changed 

279 self.assertEqual(c2, {"a": 4, "b": 2}) 

280 

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}) 

284 

285 with self.assertRaises(TypeError): 

286 c1.merge([1, 2, 3]) 

287 

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) 

297 

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) 

305 

306 with self.assertRaises(RuntimeError): 

307 c.update([1, 2, 3]) 

308 

309 def testHierarchy(self): 

310 c = Config() 

311 

312 # Simple dict 

313 c["a"] = {"z": 52, "x": "string"} 

314 self.assertIn(".a.z", c) 

315 self.assertEqual(c[".a.x"], "string") 

316 

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) 

321 

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") 

329 

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) 

339 

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) 

345 

346 # Test we do get lists back from asArray 

347 a = c.asArray(".a.b.c") 

348 self.assertIsInstance(a, list) 

349 

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) 

354 

355 # Test we always get a list 

356 for k in c.names(): 

357 a = c.asArray(k) 

358 self.assertIsInstance(a, list) 

359 

360 # Check we get the same top level keys 

361 self.assertEqual(set(c.names(topLevelOnly=True)), set(c._data.keys())) 

362 

363 # Check that we can iterate through items 

364 for k, v in c.items(): 

365 self.assertEqual(c[k], v) 

366 

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) 

379 

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) 

387 

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) 

393 

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) 

399 

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) 

409 

410 top = c.nameTuples(topLevelOnly=True) 

411 self.assertIsInstance(top[0], tuple) 

412 

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) 

419 

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) 

425 

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 } 

435 

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") 

440 

441 with self.assertRaises(ValueError): 

442 Config.fromString("", format="unknown") 

443 

444 with self.assertRaises(ValueError): 

445 Config.fromString(serialized["yaml"], format="json") 

446 

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()) 

452 

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()) 

460 

461 

462class ConfigSubsetTestCase(unittest.TestCase): 

463 """Tests for ConfigSubset""" 

464 

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") 

470 

471 def testEmpty(self): 

472 """Ensure that we can read an empty file.""" 

473 c = ConfigTestEmpty(searchPaths=(self.configDir,)) 

474 self.assertIsInstance(c, ConfigSubset) 

475 

476 def testPathlib(self): 

477 """Ensure that we can read an empty file.""" 

478 c = ConfigTestPathlib(searchPaths=(self.configDir,)) 

479 self.assertIsInstance(c, ConfigSubset) 

480 

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) 

488 

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) 

494 

495 # No default so this should fail 

496 with self.assertRaises(KeyError): 

497 c = ConfigTest() 

498 

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") 

504 

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") 

512 

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") 

518 

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) 

525 

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) 

533 

534 c = ConfigTestNoDefaults() 

535 self.assertEqual(len(c.filesRead), 0) 

536 

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) 

544 

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) 

550 

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) 

560 

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) 

571 

572 # Reset the class 

573 ConfigTestAbsPath.defaultConfigFile = None 

574 

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") 

580 

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") 

586 

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") 

593 

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") 

603 

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) 

610 

611 # Test that override delimiter works 

612 delimiter = "-" 

613 names = c.names(delimiter=delimiter) 

614 self.assertIn(delimiter.join(("", "comp3", "1", "comp", "item1")), names) 

615 

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) 

621 

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)) 

626 

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) 

641 

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) 

652 

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) 

665 

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")) 

670 

671 def testResource(self): 

672 c = Config("resource://lsst.daf.butler/configs/datastore.yaml") 

673 self.assertIn("datastore", c) 

674 

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) 

682 

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) 

691 

692 

693class FileWriteConfigTestCase(unittest.TestCase): 

694 """Test writing of configs.""" 

695 

696 def setUp(self): 

697 self.tmpdir = makeTestTempDir(TESTDIR) 

698 

699 def tearDown(self): 

700 removeTestTempDir(self.tmpdir) 

701 

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}}) 

705 

706 for format in ("yaml", "json"): 

707 outpath = os.path.join(self.tmpdir, f"test.{format}") 

708 c.dumpToUri(outpath) 

709 

710 c2 = Config(outpath) 

711 self.assertEqual(c2, c) 

712 

713 c.dumpToUri(outpath, overwrite=True) 

714 with self.assertRaises(FileExistsError): 

715 c.dumpToUri(outpath, overwrite=False) 

716 

717 

718if __name__ == "__main__": 

719 unittest.main()