Coverage for tests/test_config.py: 15%

417 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 19:55 +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/>. 

21 

22import unittest 

23import os 

24import contextlib 

25import collections 

26import itertools 

27from pathlib import Path 

28 

29from lsst.daf.butler import ConfigSubset, Config 

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

31 

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

33 

34 

35@contextlib.contextmanager 

36def modified_environment(**environ): 

37 """ 

38 Temporarily set environment variables. 

39 

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

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

42 True 

43 

44 >>> "DAF_BUTLER_CONFIG_PATHS" != "/somewhere" 

45 True 

46 

47 Parameters 

48 ---------- 

49 environ : `dict` 

50 Key value pairs of environment variables to temporarily set. 

51 """ 

52 old_environ = dict(os.environ) 

53 os.environ.update(environ) 

54 try: 

55 yield 

56 finally: 

57 os.environ.clear() 

58 os.environ.update(old_environ) 

59 

60 

61class ExampleWithConfigFileReference: 

62 defaultConfigFile = "viacls.yaml" 

63 

64 

65class ExampleWithConfigFileReference2: 

66 defaultConfigFile = "viacls2.yaml" 

67 

68 

69class ConfigTest(ConfigSubset): 

70 component = "comp" 

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

72 defaultConfigFile = "testconfig.yaml" 

73 

74 

75class ConfigTestPathlib(ConfigTest): 

76 defaultConfigFile = Path("testconfig.yaml") 

77 

78 

79class ConfigTestEmpty(ConfigTest): 

80 defaultConfigFile = "testconfig_empty.yaml" 

81 requiredKeys = () 

82 

83 

84class ConfigTestButlerDir(ConfigTest): 

85 defaultConfigFile = "testConfigs/testconfig.yaml" 

86 

87 

88class ConfigTestNoDefaults(ConfigTest): 

89 defaultConfigFile = None 

90 requiredKeys = () 

91 

92 

93class ConfigTestAbsPath(ConfigTest): 

94 defaultConfigFile = None 

95 requiredKeys = () 

96 

97 

98class ConfigTestCls(ConfigTest): 

99 defaultConfigFile = "withcls.yaml" 

100 

101 

102class ConfigTestCase(unittest.TestCase): 

103 """Tests of simple Config""" 

104 

105 def testBadConfig(self): 

106 for badArg in ([], # Bad argument 

107 __file__, # Bad file extension for existing file 

108 ): 

109 with self.assertRaises(RuntimeError): 

110 Config(badArg) 

111 for badArg in ("file.fits", # File that does not exist with bad extension 

112 "b/c/d/", # Directory that does not exist 

113 "file.yaml", # Good extension for missing file 

114 ): 

115 with self.assertRaises(FileNotFoundError): 

116 Config(badArg) 

117 

118 def testBasics(self): 

119 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}}) 

120 pretty = c.ppprint() 

121 self.assertIn("key3", pretty) 

122 r = repr(c) 

123 self.assertIn("key3", r) 

124 regex = r"^Config\(\{.*\}\)$" 

125 self.assertRegex(r, regex) 

126 c2 = eval(r) 

127 self.assertIn("1", c) 

128 for n in c.names(): 

129 self.assertEqual(c2[n], c[n]) 

130 self.assertEqual(c, c2) 

131 s = str(c) 

132 self.assertIn("\n", s) 

133 self.assertNotRegex(s, regex) 

134 

135 self.assertCountEqual(c.keys(), ["1", "3", "key3", "dict"]) 

136 self.assertEqual(list(c), list(c.keys())) 

137 self.assertEqual(list(c.values()), [c[k] for k in c.keys()]) 

138 self.assertEqual(list(c.items()), [(k, c[k]) for k in c.keys()]) 

139 

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

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

142 remainingKey = "1" 

143 

144 # Check get with existing key 

145 for k in oldKeys: 

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

147 

148 # Check get, pop with nonexistent key 

149 for k in newKeys: 

150 self.assertEqual(c.get(k, "missing"), "missing") 

151 self.assertEqual(c.pop(k, "missing"), "missing") 

152 

153 # Check setdefault with existing key 

154 for k in oldKeys: 

155 c.setdefault(k, 8) 

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

157 

158 # Check setdefault with nonexistent key (mutates c, adding newKeys) 

159 for k in newKeys: 

160 c.setdefault(k, 8) 

161 self.assertEqual(c[k], 8) 

162 

163 # Check pop with existing key (mutates c, removing newKeys) 

164 for k in newKeys: 

165 v = c[k] 

166 self.assertEqual(c.pop(k, "missing"), v) 

167 

168 # Check deletion (mutates c, removing oldKeys) 

169 for k in ("key3", ".dict.a", ("dict", "b"), "3"): 

170 self.assertIn(k, c) 

171 del c[k] 

172 self.assertNotIn(k, c) 

173 

174 # Check that `dict` still exists, but is now empty (then remove 

175 # it, mutatic c) 

176 self.assertIn("dict", c) 

177 del c["dict"] 

178 

179 # Check popitem (mutates c, removing remainingKey) 

180 v = c[remainingKey] 

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

182 

183 # Check that c is now empty 

184 self.assertFalse(c) 

185 

186 def testDict(self): 

187 """Test toDict()""" 

188 c1 = Config({"a": {"b": 1}, "c": 2}) 

189 self.assertIsInstance(c1["a"], Config) 

190 d1 = c1.toDict() 

191 self.assertIsInstance(d1["a"], dict) 

192 self.assertEqual(d1["a"], c1["a"]) 

193 

194 # Modifying one does not change the other 

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

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

197 

198 def assertSplit(self, answer, *args): 

199 """Helper function to compare string splitting""" 

200 for s in (answer, *args): 

201 split = Config._splitIntoKeys(s) 

202 self.assertEqual(split, answer) 

203 

204 def testSplitting(self): 

205 """Test of the internal splitting API.""" 

206 # Try lots of keys that will return the same answer 

207 answer = ["a", "b", "c", "d"] 

208 self.assertSplit(answer, ".a.b.c.d", ":a:b:c:d", "\ta\tb\tc\td", "\ra\rb\rc\rd") 

209 

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

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

212 

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

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

215 

216 # Escaping a backslash before a delimiter currently fails 

217 with self.assertRaises(ValueError): 

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

219 

220 # The next two fail because internally \r is magic when escaping 

221 # a delimiter. 

222 with self.assertRaises(ValueError): 

223 Config._splitIntoKeys("\ra\rcalexp\\\rwcs\rb") 

224 

225 with self.assertRaises(ValueError): 

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

227 

228 def testEscape(self): 

229 c = Config({"a": {"foo.bar": 1}, "b😂c": {"bar_baz": 2}}) 

230 self.assertEqual(c[r".a.foo\.bar"], 1) 

231 self.assertEqual(c[":a:foo.bar"], 1) 

232 self.assertEqual(c[".b😂c.bar_baz"], 2) 

233 self.assertEqual(c[r"😂b\😂c😂bar_baz"], 2) 

234 self.assertEqual(c[r"\a\foo.bar"], 1) 

235 self.assertEqual(c["\ra\rfoo.bar"], 1) 

236 with self.assertRaises(ValueError): 

237 c[".a.foo\\.bar\r"] 

238 

239 def testOperators(self): 

240 c1 = Config({"a": {"b": 1}, "c": 2}) 

241 c2 = c1.copy() 

242 self.assertEqual(c1, c2) 

243 self.assertIsInstance(c2, Config) 

244 c2[".a.b"] = 5 

245 self.assertNotEqual(c1, c2) 

246 

247 def testMerge(self): 

248 c1 = Config({"a": 1, "c": 3}) 

249 c2 = Config({"a": 4, "b": 2}) 

250 c1.merge(c2) 

251 self.assertEqual(c1, {"a": 1, "b": 2, "c": 3}) 

252 

253 # Check that c2 was not changed 

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

255 

256 # Repeat with a simple dict 

257 c1.merge({"b": 5, "d": 42}) 

258 self.assertEqual(c1, {"a": 1, "b": 2, "c": 3, "d": 42}) 

259 

260 with self.assertRaises(TypeError): 

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

262 

263 def testUpdate(self): 

264 c = Config({"a": {"b": 1}}) 

265 c.update({"a": {"c": 2}}) 

266 self.assertEqual(c[".a.b"], 1) 

267 self.assertEqual(c[".a.c"], 2) 

268 c.update({"a": {"d": [3, 4]}}) 

269 self.assertEqual(c[".a.d.0"], 3) 

270 c.update({"z": [5, 6, {"g": 2, "h": 3}]}) 

271 self.assertEqual(c[".z.1"], 6) 

272 

273 # This is detached from parent 

274 c2 = c[".z.2"] 

275 self.assertEqual(c2["g"], 2) 

276 c2.update({"h": 4, "j": 5}) 

277 self.assertEqual(c2["h"], 4) 

278 self.assertNotIn(".z.2.j", c) 

279 self.assertNotEqual(c[".z.2.h"], 4) 

280 

281 with self.assertRaises(RuntimeError): 

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

283 

284 def testHierarchy(self): 

285 c = Config() 

286 

287 # Simple dict 

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

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

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

291 

292 # Try different delimiters 

293 self.assertEqual(c["⇛a⇛z"], 52) 

294 self.assertEqual(c[("a", "z")], 52) 

295 self.assertEqual(c["a", "z"], 52) 

296 

297 c[".b.new.thing1"] = "thing1" 

298 c[".b.new.thing2"] = "thing2" 

299 c[".b.new.thing3.supp"] = "supplemental" 

300 self.assertEqual(c[".b.new.thing1"], "thing1") 

301 tmp = c[".b.new"] 

302 self.assertEqual(tmp["thing2"], "thing2") 

303 self.assertEqual(c[".b.new.thing3.supp"], "supplemental") 

304 

305 # Test that we can index into lists 

306 c[".a.b.c"] = [1, "7", 3, {"1": 4, "5": "Five"}, "hello"] 

307 self.assertIn(".a.b.c.3.5", c) 

308 self.assertNotIn(".a.b.c.10", c) 

309 self.assertNotIn(".a.b.c.10.d", c) 

310 self.assertEqual(c[".a.b.c.3.5"], "Five") 

311 # Is the value in the list? 

312 self.assertIn(".a.b.c.hello", c) 

313 self.assertNotIn(".a.b.c.hello.not", c) 

314 

315 # And assign to an element in the list 

316 self.assertEqual(c[".a.b.c.1"], "7") 

317 c[".a.b.c.1"] = 8 

318 self.assertEqual(c[".a.b.c.1"], 8) 

319 self.assertIsInstance(c[".a.b.c"], collections.abc.Sequence) 

320 

321 # Test we do get lists back from asArray 

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

323 self.assertIsInstance(a, list) 

324 

325 # Is it the *same* list as in the config 

326 a.append("Sentinel") 

327 self.assertIn("Sentinel", c[".a.b.c"]) 

328 self.assertIn(".a.b.c.Sentinel", c) 

329 

330 # Test we always get a list 

331 for k in c.names(): 

332 a = c.asArray(k) 

333 self.assertIsInstance(a, list) 

334 

335 # Check we get the same top level keys 

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

337 

338 # Check that we can iterate through items 

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

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

341 

342 # Check that lists still work even if assigned a dict 

343 c = Config({"cls": "lsst.daf.butler", 

344 "formatters": {"calexp.wcs": "{component}", 

345 "calexp": "{datasetType}"}, 

346 "datastores": [{"datastore": {"cls": "datastore1"}}, 

347 {"datastore": {"cls": "datastore2"}}]}) 

348 c[".datastores.1.datastore"] = {"cls": "datastore2modified"} 

349 self.assertEqual(c[".datastores.0.datastore.cls"], "datastore1") 

350 self.assertEqual(c[".datastores.1.datastore.cls"], "datastore2modified") 

351 self.assertIsInstance(c["datastores"], collections.abc.Sequence) 

352 

353 # Test that we can get all the listed names. 

354 # and also that they are marked as "in" the Config 

355 # Try delimited names and tuples 

356 for n in itertools.chain(c.names(), c.nameTuples()): 

357 val = c[n] 

358 self.assertIsNotNone(val) 

359 self.assertIn(n, c) 

360 

361 names = c.names() 

362 nameTuples = c.nameTuples() 

363 self.assertEqual(len(names), len(nameTuples)) 

364 self.assertEqual(len(names), 11) 

365 self.assertEqual(len(nameTuples), 11) 

366 

367 # Test that delimiter escaping works 

368 names = c.names(delimiter=".") 

369 for n in names: 

370 self.assertIn(n, c) 

371 self.assertIn(".formatters.calexp\\.wcs", names) 

372 

373 # Use a name that includes the internal default delimiter 

374 # to test automatic adjustment of delimiter 

375 strangeKey = f"calexp{c._D}wcs" 

376 c["formatters", strangeKey] = "dynamic" 

377 names = c.names() 

378 self.assertIn(strangeKey, "-".join(names)) 

379 self.assertFalse(names[0].startswith(c._D)) 

380 for n in names: 

381 self.assertIn(n, c) 

382 

383 top = c.nameTuples(topLevelOnly=True) 

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

385 

386 # Investigate a possible delimeter in a key 

387 c = Config({"formatters": {"calexp.wcs": 2, "calexp": 3}}) 

388 self.assertEqual(c[":formatters:calexp.wcs"], 2) 

389 self.assertEqual(c[":formatters:calexp"], 3) 

390 for k, v in c["formatters"].items(): 

391 self.assertEqual(c["formatters", k], v) 

392 

393 # Check internal delimiter inheritance 

394 c._D = "." 

395 c2 = c["formatters"] 

396 self.assertEqual(c._D, c2._D) # Check that the child inherits 

397 self.assertNotEqual(c2._D, Config._D) 

398 

399 def testSerializedString(self): 

400 """Test that we can create configs from strings""" 

401 

402 serialized = { 

403 "yaml": """ 

404testing: hello 

405formatters: 

406 calexp: 3""", 

407 "json": '{"testing": "hello", "formatters": {"calexp": 3}}' 

408 } 

409 

410 for format, string in serialized.items(): 

411 c = Config.fromString(string, format=format) 

412 self.assertEqual(c["formatters", "calexp"], 3) 

413 self.assertEqual(c["testing"], "hello") 

414 

415 with self.assertRaises(ValueError): 

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

417 

418 with self.assertRaises(ValueError): 

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

420 

421 # This JSON can be parsed by YAML parser 

422 j = Config.fromString(serialized["json"]) 

423 y = Config.fromString(serialized["yaml"]) 

424 self.assertEqual(j["formatters", "calexp"], 3) 

425 self.assertEqual(j.toDict(), y.toDict()) 

426 

427 # Round trip JSON -> Config -> YAML -> Config -> JSON -> Config 

428 c1 = Config.fromString(serialized["json"], format="json") 

429 yaml = c1.dump(format="yaml") 

430 c2 = Config.fromString(yaml, format="yaml") 

431 json = c2.dump(format="json") 

432 c3 = Config.fromString(json, format="json") 

433 self.assertEqual(c3.toDict(), c1.toDict()) 

434 

435 

436class ConfigSubsetTestCase(unittest.TestCase): 

437 """Tests for ConfigSubset 

438 """ 

439 

440 def setUp(self): 

441 self.testDir = os.path.abspath(os.path.dirname(__file__)) 

442 self.configDir = os.path.join(self.testDir, "config", "testConfigs") 

443 self.configDir2 = os.path.join(self.testDir, "config", "testConfigs", "test2") 

444 self.configDir3 = os.path.join(self.testDir, "config", "testConfigs", "test3") 

445 

446 def testEmpty(self): 

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

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

449 self.assertIsInstance(c, ConfigSubset) 

450 

451 def testPathlib(self): 

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

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

454 self.assertIsInstance(c, ConfigSubset) 

455 

456 def testDefaults(self): 

457 """Read of defaults""" 

458 

459 # Supply the search path explicitly 

460 c = ConfigTest(searchPaths=(self.configDir,)) 

461 self.assertIsInstance(c, ConfigSubset) 

462 self.assertIn("item3", c) 

463 self.assertEqual(c["item3"], 3) 

464 

465 # Use environment 

466 with modified_environment(DAF_BUTLER_CONFIG_PATH=self.configDir): 

467 c = ConfigTest() 

468 self.assertIsInstance(c, ConfigSubset) 

469 self.assertEqual(c["item3"], 3) 

470 

471 # No default so this should fail 

472 with self.assertRaises(KeyError): 

473 c = ConfigTest() 

474 

475 def testExternalOverride(self): 

476 """Ensure that external values win""" 

477 c = ConfigTest({"item3": "newval"}, searchPaths=(self.configDir,)) 

478 self.assertIn("item3", c) 

479 self.assertEqual(c["item3"], "newval") 

480 

481 def testSearchPaths(self): 

482 """Two search paths""" 

483 c = ConfigTest(searchPaths=(self.configDir2, self.configDir)) 

484 self.assertIsInstance(c, ConfigSubset) 

485 self.assertIn("item3", c) 

486 self.assertEqual(c["item3"], "override") 

487 self.assertEqual(c["item4"], "new") 

488 

489 c = ConfigTest(searchPaths=(self.configDir, self.configDir2)) 

490 self.assertIsInstance(c, ConfigSubset) 

491 self.assertIn("item3", c) 

492 self.assertEqual(c["item3"], 3) 

493 self.assertEqual(c["item4"], "new") 

494 

495 def testExternalHierarchy(self): 

496 """Test that we can provide external config parameters in hierarchy""" 

497 c = ConfigTest({"comp": {"item1": 6, "item2": "a", "a": "b", 

498 "item3": 7}, "item4": 8}) 

499 self.assertIn("a", c) 

500 self.assertEqual(c["a"], "b") 

501 self.assertNotIn("item4", c) 

502 

503 def testNoDefaults(self): 

504 """Ensure that defaults can be turned off.""" 

505 

506 # Mandatory keys but no defaults 

507 c = ConfigTest({"item1": "a", "item2": "b", "item6": 6}) 

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

509 self.assertIn("item1", c) 

510 self.assertEqual(c["item6"], 6) 

511 

512 c = ConfigTestNoDefaults() 

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

514 

515 def testAbsPath(self): 

516 """Read default config from an absolute path""" 

517 # Force the path to be absolute in the class 

518 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, "abspath.yaml") 

519 c = ConfigTestAbsPath() 

520 self.assertEqual(c["item11"], "eleventh") 

521 self.assertEqual(len(c.filesRead), 1) 

522 

523 # Now specify the normal config file with an absolute path 

524 ConfigTestAbsPath.defaultConfigFile = os.path.join(self.configDir, ConfigTest.defaultConfigFile) 

525 c = ConfigTestAbsPath() 

526 self.assertEqual(c["item11"], 11) 

527 self.assertEqual(len(c.filesRead), 1) 

528 

529 # and a search path that will also include the file 

530 c = ConfigTestAbsPath(searchPaths=(self.configDir, self.configDir2,)) 

531 self.assertEqual(c["item11"], 11) 

532 self.assertEqual(len(c.filesRead), 1) 

533 

534 # Same as above but this time with relative path and two search paths 

535 # to ensure the count changes 

536 ConfigTestAbsPath.defaultConfigFile = ConfigTest.defaultConfigFile 

537 c = ConfigTestAbsPath(searchPaths=(self.configDir, self.configDir2,)) 

538 self.assertEqual(len(c.filesRead), 2) 

539 

540 # Reset the class 

541 ConfigTestAbsPath.defaultConfigFile = None 

542 

543 def testClassDerived(self): 

544 """Read config specified in class determined from config""" 

545 c = ConfigTestCls(searchPaths=(self.configDir,)) 

546 self.assertEqual(c["item50"], 50) 

547 self.assertEqual(c["help"], "derived") 

548 

549 # Same thing but additional search path 

550 c = ConfigTestCls(searchPaths=(self.configDir, self.configDir2)) 

551 self.assertEqual(c["item50"], 50) 

552 self.assertEqual(c["help"], "derived") 

553 self.assertEqual(c["help2"], "second") 

554 

555 # Same thing but reverse the two paths 

556 c = ConfigTestCls(searchPaths=(self.configDir2, self.configDir)) 

557 self.assertEqual(c["item50"], 500) 

558 self.assertEqual(c["help"], "class") 

559 self.assertEqual(c["help2"], "second") 

560 self.assertEqual(c["help3"], "third") 

561 

562 def testInclude(self): 

563 """Read a config that has an include directive""" 

564 c = Config(os.path.join(self.configDir, "testinclude.yaml")) 

565 self.assertEqual(c[".comp1.item1"], 58) 

566 self.assertEqual(c[".comp2.comp.item1"], 1) 

567 self.assertEqual(c[".comp3.1.comp.item1"], "posix") 

568 self.assertEqual(c[".comp4.0.comp.item1"], "posix") 

569 self.assertEqual(c[".comp4.1.comp.item1"], 1) 

570 self.assertEqual(c[".comp5.comp6.comp.item1"], "posix") 

571 

572 # Test a specific name and then test that all 

573 # returned names are "in" the config. 

574 names = c.names() 

575 self.assertIn(c._D.join(("", "comp3", "1", "comp", "item1")), names) 

576 for n in names: 

577 self.assertIn(n, c) 

578 

579 # Test that override delimiter works 

580 delimiter = "-" 

581 names = c.names(delimiter=delimiter) 

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

583 

584 def testStringInclude(self): 

585 """Using include directives in strings""" 

586 

587 # See if include works for absolute path 

588 c = Config.fromYaml(f"something: !include {os.path.join(self.configDir, 'testconfig.yaml')}") 

589 self.assertEqual(c["something", "comp", "item3"], 3) 

590 

591 with self.assertRaises(FileNotFoundError) as cm: 

592 Config.fromYaml("something: !include /not/here.yaml") 

593 # Test that it really was trying to open the absolute path 

594 self.assertIn("'/not/here.yaml'", str(cm.exception)) 

595 

596 def testIncludeConfigs(self): 

597 """Test the special includeConfigs key for pulling in additional 

598 files.""" 

599 c = Config(os.path.join(self.configDir, "configIncludes.yaml")) 

600 self.assertEqual(c["comp", "item2"], "hello") 

601 self.assertEqual(c["comp", "item50"], 5000) 

602 self.assertEqual(c["comp", "item1"], "first") 

603 self.assertEqual(c["comp", "item10"], "tenth") 

604 self.assertEqual(c["comp", "item11"], "eleventh") 

605 self.assertEqual(c["unrelated"], 1) 

606 self.assertEqual(c["addon", "comp", "item1"], "posix") 

607 self.assertEqual(c["addon", "comp", "item11"], -1) 

608 self.assertEqual(c["addon", "comp", "item50"], 500) 

609 

610 c = Config(os.path.join(self.configDir, "configIncludes.json")) 

611 self.assertEqual(c["comp", "item2"], "hello") 

612 self.assertEqual(c["comp", "item50"], 5000) 

613 self.assertEqual(c["comp", "item1"], "first") 

614 self.assertEqual(c["comp", "item10"], "tenth") 

615 self.assertEqual(c["comp", "item11"], "eleventh") 

616 self.assertEqual(c["unrelated"], 1) 

617 self.assertEqual(c["addon", "comp", "item1"], "posix") 

618 self.assertEqual(c["addon", "comp", "item11"], -1) 

619 self.assertEqual(c["addon", "comp", "item50"], 500) 

620 

621 # Now test with an environment variable in includeConfigs 

622 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir3): 

623 c = Config(os.path.join(self.configDir, "configIncludesEnv.yaml")) 

624 self.assertEqual(c["comp", "item2"], "hello") 

625 self.assertEqual(c["comp", "item50"], 5000) 

626 self.assertEqual(c["comp", "item1"], "first") 

627 self.assertEqual(c["comp", "item10"], "tenth") 

628 self.assertEqual(c["comp", "item11"], "eleventh") 

629 self.assertEqual(c["unrelated"], 1) 

630 self.assertEqual(c["addon", "comp", "item1"], "envvar") 

631 self.assertEqual(c["addon", "comp", "item11"], -1) 

632 self.assertEqual(c["addon", "comp", "item50"], 501) 

633 

634 # This will fail 

635 with modified_environment(SPECIAL_BUTLER_DIR=self.configDir2): 

636 with self.assertRaises(FileNotFoundError): 

637 Config(os.path.join(self.configDir, "configIncludesEnv.yaml")) 

638 

639 def testResource(self): 

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

641 self.assertIn("datastore", c) 

642 

643 # Test that we can include a resource URI 

644 yaml = """ 

645toplevel: true 

646resource: !include resource://lsst.daf.butler/configs/datastore.yaml 

647""" 

648 c = Config.fromYaml(yaml) 

649 self.assertIn(("resource", "datastore", "cls"), c) 

650 

651 # Test that we can include a resource URI with includeConfigs 

652 yaml = """ 

653toplevel: true 

654resource: 

655 includeConfigs: resource://lsst.daf.butler/configs/datastore.yaml 

656""" 

657 c = Config.fromYaml(yaml) 

658 self.assertIn(("resource", "datastore", "cls"), c) 

659 

660 

661class FileWriteConfigTestCase(unittest.TestCase): 

662 

663 def setUp(self): 

664 self.tmpdir = makeTestTempDir(TESTDIR) 

665 

666 def tearDown(self): 

667 removeTestTempDir(self.tmpdir) 

668 

669 def testDump(self): 

670 """Test that we can write and read a configuration.""" 

671 

672 c = Config({"1": 2, "3": 4, "key3": 6, "dict": {"a": 1, "b": 2}}) 

673 

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

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

676 c.dumpToUri(outpath) 

677 

678 c2 = Config(outpath) 

679 self.assertEqual(c2, c) 

680 

681 c.dumpToUri(outpath, overwrite=True) 

682 with self.assertRaises(FileExistsError): 

683 c.dumpToUri(outpath, overwrite=False) 

684 

685 

686if __name__ == "__main__": 686 ↛ 687line 686 didn't jump to line 687, because the condition on line 686 was never true

687 unittest.main()