Coverage for tests/test_navdict.py: 21%

315 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-07-10 23:05 +0200

1import enum 

2import os 

3from pathlib import Path 

4 

5import pytest 

6 

7from helpers import create_test_csv_file 

8from helpers import create_text_file 

9from navdict import navdict 

10from navdict.directive import Directive 

11from navdict.directive import get_directive_plugin 

12from navdict.directive import is_directive 

13from navdict.directive import register_directive 

14from navdict.navdict import expand_env_vars 

15from navdict.navdict import get_resource_location 

16 

17HERE = Path(__file__).parent 

18 

19 

20class TakeTwoOptionalArguments: 

21 """Test class for YAML load and save methods.""" 

22 

23 def __init__(self, a=23, b=24): 

24 super().__init__() 

25 self._a = a 

26 self._b = b 

27 

28 def __str__(self): 

29 return f"a={self._a}, b={self._b}" 

30 

31 

32class TakeOneKeywordArgument: 

33 def __init__(self, *, sim: bool): 

34 self._sim = sim 

35 

36 def __str__(self): 

37 return f"sim = {self._sim}" 

38 

39 

40YAML_STRING_SIMPLE = """ 

41Setup: 

42 site_id: KUL 

43  

44 gse: 

45 hexapod: 

46 id: PUNA_01 

47 

48""" 

49 

50YAML_STRING_WITH_RELATIVE_YAML = """ 

51Setup: 

52 camera: 

53 fm01: yaml//cameras/fm01.yaml 

54""" 

55 

56YAML_STRING_WITH_CLASS = """ 

57root: 

58 defaults: 

59 dev: class//test_navdict.TakeTwoOptionalArguments 

60 with_args: 

61 dev: class//test_navdict.TakeTwoOptionalArguments 

62 dev_args: [42, 73] 

63 with_kwarg: 

64 dev: class//test_navdict.TakeOneKeywordArgument 

65 dev_kwargs: 

66 sim: true 

67""" 

68 

69YAML_STRING_WITH_INT_ENUM = """ 

70F_FEE: 

71 ccd_sides: 

72 enum: int_enum//FEE_SIDES 

73 content: 

74 E: 

75 alias: ['E_SIDE', 'RIGHT_SIDE'] 

76 value: 1 

77 F: 

78 alias: ['F_SIDE', 'LEFT_SIDE'] 

79 value: 0 

80""" 

81 

82YAML_STRING_WITH_USER_DEFINED_FUNCTION = """ 

83telemetry: 

84 dictionary: pandas//data/telemetry.csv 

85 dictionary_kwargs: 

86 separator: ; 

87""" 

88 

89YAML_STRING_WITH_UNKNOWN_CLASS = """ 

90root: 

91 part_one: 

92 cls: class//navdict.navdict 

93 part_two: 

94 cls: class//unknown.navdict 

95""" 

96 

97YAML_STRING_INVALID_INDENTATION = """ 

98name: test 

99 age: 30 

100description: invalid indentation 

101""" 

102 

103YAML_STRING_MISSING_COLON = """ 

104name test 

105age: 30 

106""" 

107 

108YAML_STRING_EMPTY = """""" 

109 

110 

111def test_is_directive(): 

112 assert is_directive("yaml//sample.yaml") 

113 assert is_directive("class//navdict.navdict") 

114 assert is_directive("my_directive//value") 

115 

116 assert not is_directive("just a string") 

117 assert not is_directive("relative/path") 

118 assert not is_directive("my-directive//value") 

119 

120 assert not is_directive(42) 

121 assert not is_directive(23.7) 

122 

123 assert not is_directive("my-setup-001") 

124 

125 

126def test_get_directive_plugin(): 

127 assert isinstance(get_directive_plugin("yaml"), Directive) 

128 

129 assert not isinstance(get_directive_plugin("not-a-plugin"), Directive) 

130 

131 

132def test_use_a_directive_plugin(): 

133 yaml_string = """ 

134 Setup: 

135 info: my_yaml//../use/this/file.yaml 

136 info_args: [1, 2] 

137 info_kwargs: 

138 x: X 

139 y: Y 

140 """ 

141 data = navdict.from_yaml_string(yaml_string) 

142 # print(f"{data.Setup.info=}") 

143 assert data.Setup.info.startswith("my_yaml//") 

144 assert data.Setup.info.endswith("use/this/file.yaml") 

145 

146 

147def test_get_resource_location(): 

148 assert get_resource_location(None, None) == Path(".") 

149 assert get_resource_location(None, "../data") == Path(".") / "../data" 

150 assert get_resource_location(Path("~"), "data") == Path("~") / "data" 

151 assert get_resource_location(Path("~"), None) == Path("~") 

152 

153 

154def test_construction(): 

155 setup = navdict() 

156 

157 assert setup == {} 

158 assert setup.get_label() is None 

159 

160 setup = navdict(label="Setup") 

161 assert setup.get_label() == "Setup" 

162 

163 

164def test_label(): 

165 setup = navdict() 

166 

167 assert setup == {} 

168 assert setup.get_label() is None 

169 

170 setup.set_label("Setup") 

171 

172 assert setup == {} 

173 assert setup.get_label() == "Setup" 

174 

175 

176def test_navigation(): 

177 data = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

178 

179 assert isinstance(data, navdict) 

180 assert isinstance(data.Setup, navdict) 

181 

182 assert data.Setup.site_id == "KUL" 

183 assert data.Setup.gse.hexapod.id == "PUNA_01" 

184 

185 

186def test_from_yaml_string(): 

187 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

188 

189 assert "Setup" in setup 

190 assert "site_id" in setup.Setup 

191 assert "gse" in setup.Setup 

192 assert setup.Setup.gse.hexapod.id == "PUNA_01" 

193 

194 with pytest.raises( 

195 ValueError, 

196 match="Invalid YAML string: mapping values are not allowed in this context", 

197 ): 

198 setup = navdict.from_yaml_string(YAML_STRING_INVALID_INDENTATION) 

199 

200 with pytest.raises( 

201 ValueError, 

202 match="Invalid YAML string: mapping values are not allowed in this context", 

203 ): 

204 setup = navdict.from_yaml_string(YAML_STRING_MISSING_COLON) 

205 

206 with pytest.raises(ValueError, match="Invalid argument to function: No input string or None given"): 

207 setup = navdict.from_yaml_string(YAML_STRING_EMPTY) 

208 

209 

210def test_from_yaml_file(): 

211 with pytest.raises( 

212 ValueError, 

213 match=r"Invalid argument to function, filename does not exist: " 

214 r".*/simple.yaml", 

215 ): 

216 navdict.from_yaml_file("~/simple.yaml") 

217 

218 with create_text_file("simple.yaml", YAML_STRING_SIMPLE) as fn: 

219 setup = navdict.from_yaml_file(fn) 

220 assert "Setup" in setup 

221 assert "site_id" in setup.Setup 

222 assert "gse" in setup.Setup 

223 assert setup.Setup.gse.hexapod.id == "PUNA_01" 

224 

225 with create_text_file("with_unknown_class.yaml", YAML_STRING_WITH_UNKNOWN_CLASS) as fn: 

226 # The following line shall not generate an exception, meaning the `class//` 

227 # shall not be evaluated on load! 

228 data = navdict.from_yaml_file(fn) 

229 

230 assert "root" in data 

231 assert isinstance(data.root.part_one.cls, navdict) 

232 

233 # Only when accessed, it will generate an exception. 

234 with pytest.raises(ModuleNotFoundError, match="No module named 'unknown'"): 

235 _ = data.root.part_two.cls 

236 

237 

238def test_to_yaml_file(): 

239 """ 

240 This test loads the standard Setup and saves it without change to a new file. 

241 Loading back the saved Setup should show no differences. 

242 """ 

243 

244 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

245 

246 with pytest.raises(ValueError, match="No filename given or known, can not save navdict."): 

247 setup.to_yaml_file() 

248 

249 setup.to_yaml_file("simple.yaml") 

250 

251 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

252 setup.to_yaml_file("with_class.yaml") 

253 

254 Path("simple.yaml").unlink() 

255 Path("with_class.yaml").unlink() 

256 

257 

258def test_class_directive(): 

259 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

260 

261 obj = setup.root.defaults.dev 

262 assert isinstance(obj, TakeTwoOptionalArguments) 

263 assert str(obj) == "a=23, b=24" 

264 

265 obj = setup.root.with_args.dev 

266 assert isinstance(obj, TakeTwoOptionalArguments) 

267 assert str(obj) == "a=42, b=73" 

268 

269 obj = setup.root.with_kwarg.dev 

270 assert isinstance(obj, TakeOneKeywordArgument) 

271 assert str(obj) == "sim = True" 

272 

273 

274def test_user_defined_function_directive(): 

275 setup = navdict.from_yaml_string(YAML_STRING_WITH_USER_DEFINED_FUNCTION) 

276 

277 # By default, the 'pandas' directive is not registered, so, the 'dictionary' value will be returned as is. 

278 result = setup.telemetry.dictionary 

279 assert result == "pandas//data/telemetry.csv" 

280 

281 # Now we register a simple 'pandas' directive for testing purposes. 

282 def mock_pandas_directive(value: str, parent_location: Path | None, *args, **kwargs): 

283 return f"Mocked pandas loading of '{value}' with args={args} and kwargs={kwargs}" 

284 

285 register_directive("pandas", mock_pandas_directive) 

286 

287 result = setup.telemetry.dictionary 

288 print(f"{result=}") 

289 assert result == "Mocked pandas loading of 'data/telemetry.csv' with args=() and kwargs={'separator': ';'}" 

290 

291 

292def test_from_dict(): 

293 setup = navdict.from_dict({"ID": "my-setup-001", "version": "0.1.0"}, label="Setup") 

294 assert setup["ID"] == setup.ID == "my-setup-001" 

295 

296 assert setup._label == "Setup" 

297 

298 # If not all keys are of type 'str', the navdict will not be navigable. 

299 setup = navdict.from_dict({"ID": 1234, 42: "forty two"}, label="Setup") 

300 assert setup["ID"] == 1234 

301 

302 with pytest.raises(AttributeError): 

303 _ = setup.ID 

304 

305 # Only the (sub-)dictionary that contains non-str keys will not be navigable. 

306 setup = navdict.from_dict({"ID": 1234, "answer": {"book": "H2G2", 42: "forty two"}}, label="Setup") 

307 assert setup["ID"] == setup.ID == 1234 

308 assert setup.answer["book"] == "H2G2" 

309 

310 with pytest.raises(AttributeError): 

311 _ = setup.answer.book 

312 

313 

314def get_enum_metaclass(): 

315 """Get the enum metaclass in a version-compatible way.""" 

316 if hasattr(enum, "EnumMeta"): 

317 return enum.EnumMeta 

318 elif hasattr(enum, "EnumType"): # Python 3.11+ 

319 return enum.EnumType 

320 else: 

321 # Fallback: get it from a known enum 

322 return type(enum.IntEnum) 

323 

324 

325def test_int_enum(): 

326 setup = navdict.from_yaml_string(YAML_STRING_WITH_INT_ENUM) 

327 

328 assert "enum" in setup.F_FEE.ccd_sides 

329 assert "content" in setup.F_FEE.ccd_sides 

330 assert "E" in setup.F_FEE.ccd_sides.content 

331 assert "F" in setup.F_FEE.ccd_sides.content 

332 

333 assert setup.F_FEE.ccd_sides.enum.E.value == 1 

334 assert setup.F_FEE.ccd_sides.enum.E_SIDE.value == 1 

335 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.value == 1 

336 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.name == "E" 

337 

338 assert setup.F_FEE.ccd_sides.enum.F.value == 0 

339 assert setup.F_FEE.ccd_sides.enum.F_SIDE.value == 0 

340 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.value == 0 

341 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.name == "F" 

342 

343 assert issubclass(setup.F_FEE.ccd_sides.enum, enum.IntEnum) 

344 assert isinstance(setup.F_FEE.ccd_sides.enum, get_enum_metaclass()) 

345 assert isinstance(setup.F_FEE.ccd_sides.enum, type) 

346 assert isinstance(setup.F_FEE.ccd_sides.enum.E, enum.IntEnum) # noqa 

347 

348 

349YAML_STRING_LOADS_YAML_FILE = """ 

350root: 

351 simple: yaml//enum.yaml 

352""" 

353 

354 

355def test_recursive_load(): 

356 with ( 

357 create_text_file("load_yaml.yaml", YAML_STRING_LOADS_YAML_FILE) as fn, 

358 create_text_file("enum.yaml", YAML_STRING_WITH_INT_ENUM), 

359 ): 

360 data = navdict.from_yaml_file(fn) 

361 assert data.root.simple.F_FEE.ccd_sides.enum.E.value == 1 

362 

363 

364def test_relative_load(): 

365 with ( 

366 create_text_file(HERE / "data/conf/load_relative_yaml.yaml", YAML_STRING_WITH_RELATIVE_YAML) as fn, 

367 ): 

368 data = navdict.from_yaml_file(fn) 

369 assert data.Setup.camera.fm01.calibration.temperature.T1.name == "TRP99" 

370 

371 

372def test_relative_load_from_string(): 

373 """ 

374 The YAML string contains a directive to load another YAML file, but since 

375 we will load this navdict from the string instead of the file, it doesn't 

376 have a location and therefore the directive will be loaded relative to the 

377 working directory. 

378 

379 When we change the current working directory to the expected location, things 

380 work just fine. 

381 

382 """ 

383 data = navdict.from_yaml_string(YAML_STRING_WITH_RELATIVE_YAML) 

384 

385 assert "fm01" in data.Setup.camera 

386 

387 with pytest.raises(FileNotFoundError, match="No such file or directory: 'cameras/fm01.yaml'"): 

388 assert data.Setup.camera.fm01 

389 

390 cwd = os.getcwd() 

391 

392 os.chdir(HERE / "data/conf") 

393 

394 assert data.Setup.camera.fm01.name == "FM01" 

395 assert "T1" in data.Setup.camera.fm01.calibration.temperature 

396 

397 os.chdir(cwd) 

398 

399 

400YAML_STRING_LOADS_CSV_FILE = """ 

401root: 

402 sample: csv//data/sample.csv 

403 sample_kwargs: 

404 header_rows: 1 

405""" 

406 

407 

408def test_load_csv(): 

409 """ 

410 The sample.csv file will be read using the standard load_csv directive. 

411 

412 - one header row will be skipped (header_rows=1) 

413 - the comment line will be filtered 

414 - a list of list[str] will be returned 

415 

416 """ 

417 with ( 

418 create_text_file(HERE / "load_csv.yaml", YAML_STRING_LOADS_CSV_FILE) as fn, 

419 create_test_csv_file(HERE / "data/sample.csv"), 

420 ): 

421 data = navdict.from_yaml_file(fn) 

422 

423 csv_data = data.root.sample 

424 

425 assert len(csv_data[0]) == 9 

426 assert isinstance(csv_data, list) 

427 assert isinstance(csv_data[0], list) 

428 

429 assert csv_data[0][0] == "1001" 

430 assert csv_data[0][8] == "john.smith@company.com" 

431 

432 

433def test_expand_env_vars(): 

434 os.environ["NAVDICT_TEST_VAR"] = "some/value" 

435 

436 assert expand_env_vars("prefix/ENV[NAVDICT_TEST_VAR]/suffix") == "prefix/some/value/suffix" 

437 assert expand_env_vars("prefix/ENV['NAVDICT_TEST_VAR']/suffix") == "prefix/some/value/suffix" 

438 assert expand_env_vars('prefix/ENV["NAVDICT_TEST_VAR"]/suffix') == "prefix/some/value/suffix" 

439 

440 os.environ["NAVDICT_TEST_HOME_VAR"] = "~" 

441 

442 assert expand_env_vars("ENV[NAVDICT_TEST_HOME_VAR]/data") == str(Path.home() / "data") 

443 

444 del os.environ["NAVDICT_TEST_VAR"] 

445 del os.environ["NAVDICT_TEST_HOME_VAR"] 

446 

447 with pytest.raises(ValueError): 

448 expand_env_vars("ENV[NAVDICT_TEST_VAR_NOT_SET]/data") 

449 

450 

451YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR = """ 

452root: 

453 sample: csv//ENV[NAVDICT_TEST_DATA_DIR]/sample.csv 

454 sample_kwargs: 

455 header_rows: 1 

456""" 

457 

458YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR_DISABLED = """ 

459root: 

460 sample: csv//ENV[NAVDICT_TEST_DATA_DIR]/sample.csv 

461 sample_kwargs: 

462 header_rows: 1 

463 expand_env: false 

464""" 

465 

466YAML_STRING_LOADS_CSV_FILE_WITH_MISSING_ENV_VAR = """ 

467root: 

468 sample: csv//ENV[NAVDICT_TEST_VAR_NOT_SET]/sample.csv 

469""" 

470 

471 

472def test_load_csv_with_env_var(): 

473 os.environ["NAVDICT_TEST_DATA_DIR"] = str(HERE / "data") 

474 

475 try: 

476 with ( 

477 create_text_file(HERE / "load_csv_env.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR) as fn, 

478 create_test_csv_file(HERE / "data/sample.csv"), 

479 ): 

480 data = navdict.from_yaml_file(fn) 

481 

482 csv_data = data.root.sample 

483 

484 assert isinstance(csv_data, list) 

485 assert csv_data[0][0] == "1001" 

486 assert csv_data[0][8] == "john.smith@company.com" 

487 finally: 

488 del os.environ["NAVDICT_TEST_DATA_DIR"] 

489 

490 

491def test_load_csv_env_var_expansion_disabled(): 

492 os.environ["NAVDICT_TEST_DATA_DIR"] = str(HERE / "data") 

493 

494 try: 

495 with ( 

496 create_text_file(HERE / "load_csv_env_disabled.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR_DISABLED) as fn, 

497 create_test_csv_file(HERE / "data/sample.csv"), 

498 ): 

499 data = navdict.from_yaml_file(fn) 

500 

501 with pytest.raises(FileNotFoundError): 

502 _ = data.root.sample 

503 finally: 

504 del os.environ["NAVDICT_TEST_DATA_DIR"] 

505 

506 

507def test_load_csv_missing_env_var(): 

508 with create_text_file(HERE / "load_csv_missing_env.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_MISSING_ENV_VAR) as fn: 

509 data = navdict.from_yaml_file(fn) 

510 

511 with pytest.raises(ValueError): 

512 _ = data.root.sample 

513 

514 

515def test_directive_registration(): 

516 def inspect_directive(value, parent_location, *args, **kwargs) -> dict: 

517 return dict(value=value, parent_location=parent_location, args=args, kwargs=kwargs) 

518 

519 # The following should overwrite the default `csv//` directive 

520 register_directive("csv", inspect_directive) 

521 

522 with ( 

523 create_text_file(HERE / "load_csv.yaml", YAML_STRING_LOADS_CSV_FILE) as fn, 

524 create_test_csv_file(HERE / "data/sample.csv"), 

525 ): 

526 data = navdict.from_yaml_file(fn) 

527 

528 csv_data = data.root.sample 

529 

530 assert isinstance(csv_data, dict) 

531 assert "value" in csv_data 

532 assert csv_data["value"] == "data/sample.csv" 

533 assert csv_data["parent_location"] == HERE 

534 

535 assert "kwargs" in csv_data 

536 assert "header_rows" in csv_data["kwargs"] 

537 assert csv_data["kwargs"]["header_rows"] == 1 

538 

539 

540YAML_STRING_WITH_ENV_VAR = """ 

541config: 

542 token: env//AUTH_TOKEN 

543""" 

544 

545 

546def test_env_var(): 

547 data = navdict.from_yaml_string(YAML_STRING_WITH_ENV_VAR) 

548 

549 assert data.config.token is None 

550 

551 os.environ["AUTH_TOKEN"] = "this-is-my-token" 

552 

553 data.config.del_memoized_key("token") 

554 

555 assert data.config.token == "this-is-my-token" 

556 

557 del os.environ["AUTH_TOKEN"] 

558 

559 

560def test_memoized_keys(): 

561 # NOTE: 

562 # * memoized keys are only for directives. 

563 # * a key is memoized only after it was accessed. 

564 

565 data = navdict.from_yaml_string(YAML_STRING_WITH_ENV_VAR) 

566 

567 assert data.config.get_memoized_keys() == [] 

568 

569 os.environ["AUTH_TOKEN"] = "this-is-my-token-too" 

570 

571 assert data.config.token == "this-is-my-token-too" 

572 

573 assert data.config.get_memoized_keys() == ["token"] 

574 

575 assert data.config.del_memoized_key("token") 

576 

577 assert "token" not in data.config.get_memoized_keys() 

578 

579 # returns False when a key is not memoized and could not be deleted 

580 assert not data.config.del_memoized_key("unknown") 

581 

582 

583def test_non_string_keys(): 

584 x = navdict({"A": {1: "one", 2: "two", (3,): "three-tuple"}}) 

585 

586 assert x.A[1] == "one" 

587 assert x.A[2] == "two" 

588 assert x.A[(3,)] == "three-tuple" 

589 

590 

591def test_invalid_yaml(): 

592 # This would normally raise a ScannerError from the ruamel.yaml package 

593 # - ruamel.yaml.scanner.ScannerError: mapping values are not allowed in this context 

594 

595 with pytest.raises(IOError): 

596 _ = navdict.from_yaml_file(__file__) 

597 

598 

599def test_alias_hook(): 

600 x = navdict({"letters": {"a": "A", "b": "B", "c": "C"}, "numbers": [1, 2, 3, 4, 5]}) 

601 

602 def greek(letter: str): 

603 greek_to_latin = {"alpha": "a", "beta": "b", "gamma": "c"} 

604 return greek_to_latin[letter] 

605 

606 assert x.letters.a == "A" 

607 assert x.numbers[2] == 3 

608 

609 with pytest.raises(AttributeError): 

610 _ = x.letters.alpha 

611 

612 with pytest.raises(KeyError): 

613 _ = x.letters["alpha"] 

614 

615 x.letters.set_alias_hook(greek) 

616 

617 assert x.letters.alpha == "A" 

618 assert x.letters["alpha"] == "A" 

619 

620 assert x.numbers[2] == 3 

621 

622 

623YAML_STRING_HOUSE = """ 

624House: 

625 Cameras: 

626 cam_1: 

627 location: front door 

628 type: XYZ-A123 

629 cam_2: 

630 location: front garage 

631 type: XYZ-B123 

632""" 

633 

634 

635def test_alias_hook_from_yaml_string(): 

636 iot = navdict.from_yaml_string(YAML_STRING_HOUSE) 

637 

638 assert iot.House.Cameras.cam_1.type == "XYZ-A123" 

639 assert iot.House.Cameras.cam_2.type == "XYZ-B123" 

640 

641 def abbrev(name: str) -> str: 

642 aliases = {"front_door": "cam_1", "front_garage": "cam_2"} 

643 return aliases[name] 

644 

645 iot.House.Cameras.set_alias_hook(abbrev) 

646 

647 assert iot.House.Cameras.cam_1.type == "XYZ-A123" 

648 assert iot.House.Cameras.cam_2.type == "XYZ-B123" 

649 

650 assert iot.House.Cameras.front_door.type == "XYZ-A123" 

651 assert iot.House.Cameras.front_garage.type == "XYZ-B123" 

652 

653 assert "_alias_hook" not in iot.House.Cameras 

654 

655 

656def test_rich_print_with_alias_hook(): 

657 iot = navdict.from_yaml_string(YAML_STRING_HOUSE) 

658 

659 def abbrev(name: str) -> str: 

660 aliases = {"front_door": "cam_1", "front_garage": "cam_2"} 

661 return aliases[name] 

662 

663 iot.House.Cameras.set_alias_hook(abbrev) 

664 

665 with pytest.raises(AttributeError, match="'NavigableDict' object has no attribute 'no_such_camera'"): 

666 # The following line will cause a KeyError in the abbrev()` hook 

667 # Any Exception from the alias hook should not propagate to the top 

668 # but should raise an AttributeError as expected. 

669 _ = iot.House.Cameras.no_such_camera 

670 

671 with pytest.raises(KeyError, match="'NavigableDict' has no key 'no_such_camera'"): 

672 # The following line will cause a KeyError in the abbrev()` hook 

673 # Any Exception from the alias hook should not propagate to the top 

674 # but should raise a KeyError as expected. 

675 _ = iot.House.Cameras["no_such_camera"]