Coverage for tests/test_navdict.py: 21%

327 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-07-13 14:01 +0200

1import enum 

2import os 

3from pathlib import Path 

4 

5import pytest 

6 

7from helpers import create_test_csv_file 

8from helpers import create_test_csv_file_semicolon 

9from helpers import create_text_file 

10from navdict import navdict 

11from navdict.directive import Directive 

12from navdict.directive import get_directive_plugin 

13from navdict.directive import is_directive 

14from navdict.directive import register_directive 

15from navdict.navdict import expand_env_vars 

16from navdict.navdict import get_resource_location 

17 

18HERE = Path(__file__).parent 

19 

20 

21class TakeTwoOptionalArguments: 

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

23 

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

25 super().__init__() 

26 self._a = a 

27 self._b = b 

28 

29 def __str__(self): 

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

31 

32 

33class TakeOneKeywordArgument: 

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

35 self._sim = sim 

36 

37 def __str__(self): 

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

39 

40 

41YAML_STRING_SIMPLE = """ 

42Setup: 

43 site_id: KUL 

44 

45 gse: 

46 hexapod: 

47 id: PUNA_01 

48 

49""" 

50 

51YAML_STRING_WITH_RELATIVE_YAML = """ 

52Setup: 

53 camera: 

54 fm01: yaml//cameras/fm01.yaml 

55""" 

56 

57YAML_STRING_WITH_CLASS = """ 

58root: 

59 defaults: 

60 dev: class//test_navdict.TakeTwoOptionalArguments 

61 with_args: 

62 dev: class//test_navdict.TakeTwoOptionalArguments 

63 dev_args: [42, 73] 

64 with_kwarg: 

65 dev: class//test_navdict.TakeOneKeywordArgument 

66 dev_kwargs: 

67 sim: true 

68""" 

69 

70YAML_STRING_WITH_INT_ENUM = """ 

71F_FEE: 

72 ccd_sides: 

73 enum: int_enum//FEE_SIDES 

74 content: 

75 E: 

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

77 value: 1 

78 F: 

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

80 value: 0 

81""" 

82 

83YAML_STRING_WITH_USER_DEFINED_FUNCTION = """ 

84telemetry: 

85 dictionary: pandas//data/telemetry.csv 

86 dictionary_kwargs: 

87 separator: ; 

88""" 

89 

90YAML_STRING_WITH_UNKNOWN_CLASS = """ 

91root: 

92 part_one: 

93 cls: class//navdict.navdict 

94 part_two: 

95 cls: class//unknown.navdict 

96""" 

97 

98YAML_STRING_INVALID_INDENTATION = """ 

99name: test 

100 age: 30 

101description: invalid indentation 

102""" 

103 

104YAML_STRING_MISSING_COLON = """ 

105name test 

106age: 30 

107""" 

108 

109YAML_STRING_EMPTY = """""" 

110 

111 

112def test_is_directive(): 

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

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

115 assert is_directive("my_directive//value") 

116 

117 assert not is_directive("just a string") 

118 assert not is_directive("relative/path") 

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

120 

121 assert not is_directive(42) 

122 assert not is_directive(23.7) 

123 

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

125 

126 

127def test_get_directive_plugin(): 

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

129 

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

131 

132 

133def test_use_a_directive_plugin(): 

134 yaml_string = """ 

135 Setup: 

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

137 info_args: [1, 2] 

138 info_kwargs: 

139 x: X 

140 y: Y 

141 """ 

142 data = navdict.from_yaml_string(yaml_string) 

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

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

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

146 

147 

148def test_get_resource_location(): 

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

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

151 assert get_resource_location(Path("~/project/"), "data") == Path.home() / "project/data" 

152 assert get_resource_location(Path("~"), None) == Path.home() 

153 

154 

155def test_construction(): 

156 setup = navdict() 

157 

158 assert setup == {} 

159 assert setup.get_label() is None 

160 

161 setup = navdict(label="Setup") 

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

163 

164 

165def test_label(): 

166 setup = navdict() 

167 

168 assert setup == {} 

169 assert setup.get_label() is None 

170 

171 setup.set_label("Setup") 

172 

173 assert setup == {} 

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

175 

176 

177def test_navigation(): 

178 data = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

179 

180 assert isinstance(data, navdict) 

181 assert isinstance(data.Setup, navdict) 

182 

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

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

185 

186 

187def test_from_yaml_string(): 

188 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

189 

190 assert "Setup" in setup 

191 assert "site_id" in setup.Setup 

192 assert "gse" in setup.Setup 

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

194 

195 with pytest.raises( 

196 ValueError, 

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

198 ): 

199 setup = navdict.from_yaml_string(YAML_STRING_INVALID_INDENTATION) 

200 

201 with pytest.raises( 

202 ValueError, 

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

204 ): 

205 setup = navdict.from_yaml_string(YAML_STRING_MISSING_COLON) 

206 

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

208 setup = navdict.from_yaml_string(YAML_STRING_EMPTY) 

209 

210 

211def test_from_yaml_file(): 

212 with pytest.raises( 

213 ValueError, 

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

215 r".*/simple.yaml", 

216 ): 

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

218 

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

220 setup = navdict.from_yaml_file(fn) 

221 assert "Setup" in setup 

222 assert "site_id" in setup.Setup 

223 assert "gse" in setup.Setup 

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

225 

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

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

228 # shall not be evaluated on load! 

229 data = navdict.from_yaml_file(fn) 

230 

231 assert "root" in data 

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

233 

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

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

236 _ = data.root.part_two.cls 

237 

238 

239def test_to_yaml_file(): 

240 """ 

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

242 Loading back the saved Setup should show no differences. 

243 """ 

244 

245 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

246 

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

248 setup.to_yaml_file() 

249 

250 setup.to_yaml_file("simple.yaml") 

251 

252 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

253 setup.to_yaml_file("with_class.yaml") 

254 

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

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

257 

258 

259def test_class_directive(): 

260 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

261 

262 obj = setup.root.defaults.dev 

263 assert isinstance(obj, TakeTwoOptionalArguments) 

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

265 

266 obj = setup.root.with_args.dev 

267 assert isinstance(obj, TakeTwoOptionalArguments) 

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

269 

270 obj = setup.root.with_kwarg.dev 

271 assert isinstance(obj, TakeOneKeywordArgument) 

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

273 

274 

275def test_user_defined_function_directive(): 

276 setup = navdict.from_yaml_string(YAML_STRING_WITH_USER_DEFINED_FUNCTION) 

277 

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

279 result = setup.telemetry.dictionary 

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

281 

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

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

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

285 

286 register_directive("pandas", mock_pandas_directive) 

287 

288 result = setup.telemetry.dictionary 

289 print(f"{result=}") 

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

291 

292 

293def test_from_dict(): 

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

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

296 

297 assert setup._label == "Setup" 

298 

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

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

301 assert setup["ID"] == 1234 

302 

303 with pytest.raises(AttributeError): 

304 _ = setup.ID 

305 

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

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

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

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

310 

311 with pytest.raises(AttributeError): 

312 _ = setup.answer.book 

313 

314 

315def get_enum_metaclass(): 

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

317 if hasattr(enum, "EnumMeta"): 

318 return enum.EnumMeta 

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

320 return enum.EnumType 

321 else: 

322 # Fallback: get it from a known enum 

323 return type(enum.IntEnum) 

324 

325 

326def test_int_enum(): 

327 setup = navdict.from_yaml_string(YAML_STRING_WITH_INT_ENUM) 

328 

329 assert "enum" in setup.F_FEE.ccd_sides 

330 assert "content" in setup.F_FEE.ccd_sides 

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

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

333 

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

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

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

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

338 

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

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

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

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

343 

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

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

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

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

348 

349 

350YAML_STRING_LOADS_YAML_FILE = """ 

351root: 

352 simple: yaml//enum.yaml 

353""" 

354 

355 

356def test_recursive_load(): 

357 with ( 

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

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

360 ): 

361 data = navdict.from_yaml_file(fn) 

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

363 

364 

365def test_relative_load(): 

366 with ( 

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

368 ): 

369 data = navdict.from_yaml_file(fn) 

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

371 

372 

373def test_relative_load_from_string(): 

374 """ 

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

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

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

378 working directory. 

379 

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

381 work just fine. 

382 

383 """ 

384 data = navdict.from_yaml_string(YAML_STRING_WITH_RELATIVE_YAML) 

385 

386 assert "fm01" in data.Setup.camera 

387 

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

389 assert data.Setup.camera.fm01 

390 

391 cwd = os.getcwd() 

392 

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

394 

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

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

397 

398 os.chdir(cwd) 

399 

400 

401YAML_STRING_LOADS_CSV_FILE = """ 

402root: 

403 sample: csv//data/sample.csv 

404 sample_kwargs: 

405 header_rows: 1 

406""" 

407 

408 

409def test_load_csv(): 

410 """ 

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

412 

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

414 - the comment line will be filtered 

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

416 

417 """ 

418 with ( 

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

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

421 ): 

422 data = navdict.from_yaml_file(fn) 

423 

424 csv_data = data.root.sample 

425 

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

427 assert isinstance(csv_data, list) 

428 assert isinstance(csv_data[0], list) 

429 

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

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

432 

433 

434def test_expand_env_vars(): 

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

436 

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 assert expand_env_vars('prefix/ENV["NAVDICT_TEST_VAR"]/suffix') == "prefix/some/value/suffix" 

440 

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

442 

443 assert ( 

444 expand_env_vars("ENV[NAVDICT_TEST_HOME_VAR]/data") == "~/data" 

445 ) # environment variable is expanded, but not the '~' character 

446 

447 del os.environ["NAVDICT_TEST_VAR"] 

448 del os.environ["NAVDICT_TEST_HOME_VAR"] 

449 

450 with pytest.raises(ValueError): 

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

452 

453 

454YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR = """ 

455root: 

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

457 sample_kwargs: 

458 header_rows: 1 

459""" 

460 

461YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR_DISABLED = """ 

462root: 

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

464 sample_kwargs: 

465 header_rows: 1 

466 expand_env: false 

467""" 

468 

469YAML_STRING_LOADS_CSV_FILE_WITH_MISSING_ENV_VAR = """ 

470root: 

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

472""" 

473 

474 

475def test_load_csv_with_env_var(): 

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

477 

478 try: 

479 with ( 

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

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

482 ): 

483 data = navdict.from_yaml_file(fn) 

484 

485 csv_data = data.root.sample 

486 

487 assert isinstance(csv_data, list) 

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

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

490 finally: 

491 del os.environ["NAVDICT_TEST_DATA_DIR"] 

492 

493 

494def test_load_csv_env_var_expansion_disabled(): 

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

496 

497 try: 

498 with ( 

499 create_text_file( 

500 HERE / "load_csv_env_disabled.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR_DISABLED 

501 ) as fn, 

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

503 ): 

504 data = navdict.from_yaml_file(fn) 

505 

506 with pytest.raises(FileNotFoundError): 

507 _ = data.root.sample 

508 finally: 

509 del os.environ["NAVDICT_TEST_DATA_DIR"] 

510 

511 

512def test_load_csv_missing_env_var(): 

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

514 data = navdict.from_yaml_file(fn) 

515 

516 with pytest.raises(ValueError): 

517 _ = data.root.sample 

518 

519 

520YAML_STRING_LOADS_CSV_FILE_WITH_DELIMITER = """ 

521root: 

522 products: csv//data/sample_semicolon.csv 

523 products_kwargs: 

524 header_rows: 1 

525 delimiter: ';' 

526""" 

527 

528 

529def test_load_csv_with_delimiter(): 

530 """ 

531 Test that the CSV directive correctly handles custom delimiters. 

532 The sample CSV file uses semicolons as delimiters instead of the default comma. 

533 """ 

534 with ( 

535 create_text_file(HERE / "load_csv_delimiter.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_DELIMITER) as fn, 

536 create_test_csv_file_semicolon(HERE / "data/sample_semicolon.csv"), 

537 ): 

538 data = navdict.from_yaml_file(fn) 

539 

540 csv_data = data.root.products 

541 

542 assert isinstance(csv_data, list) 

543 assert isinstance(csv_data[0], list) 

544 

545 assert len(csv_data[0]) == 5 

546 assert csv_data[0][0] == "101" 

547 assert csv_data[0][1] == "Laptop" 

548 assert csv_data[0][4] == "15" 

549 

550 

551def test_directive_registration(): 

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

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

554 

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

556 register_directive("csv", inspect_directive) 

557 

558 with ( 

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

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

561 ): 

562 data = navdict.from_yaml_file(fn) 

563 

564 csv_data = data.root.sample 

565 

566 assert isinstance(csv_data, dict) 

567 assert "value" in csv_data 

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

569 assert csv_data["parent_location"] == HERE 

570 

571 assert "kwargs" in csv_data 

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

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

574 

575 

576YAML_STRING_WITH_ENV_VAR = """ 

577config: 

578 token: env//AUTH_TOKEN 

579""" 

580 

581 

582def test_env_var(): 

583 data = navdict.from_yaml_string(YAML_STRING_WITH_ENV_VAR) 

584 

585 assert data.config.token is None 

586 

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

588 

589 data.config.del_memoized_key("token") 

590 

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

592 

593 del os.environ["AUTH_TOKEN"] 

594 

595 

596def test_memoized_keys(): 

597 # NOTE: 

598 # * memoized keys are only for directives. 

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

600 

601 data = navdict.from_yaml_string(YAML_STRING_WITH_ENV_VAR) 

602 

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

604 

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

606 

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

608 

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

610 

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

612 

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

614 

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

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

617 

618 

619def test_non_string_keys(): 

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

621 

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

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

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

625 

626 

627def test_invalid_yaml(): 

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

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

630 

631 with pytest.raises(IOError): 

632 _ = navdict.from_yaml_file(__file__) 

633 

634 

635def test_alias_hook(): 

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

637 

638 def greek(letter: str): 

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

640 return greek_to_latin[letter] 

641 

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

643 assert x.numbers[2] == 3 

644 

645 with pytest.raises(AttributeError): 

646 _ = x.letters.alpha 

647 

648 with pytest.raises(KeyError): 

649 _ = x.letters["alpha"] 

650 

651 x.letters.set_alias_hook(greek) 

652 

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

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

655 

656 assert x.numbers[2] == 3 

657 

658 

659YAML_STRING_HOUSE = """ 

660House: 

661 Cameras: 

662 cam_1: 

663 location: front door 

664 type: XYZ-A123 

665 cam_2: 

666 location: front garage 

667 type: XYZ-B123 

668""" 

669 

670 

671def test_alias_hook_from_yaml_string(): 

672 iot = navdict.from_yaml_string(YAML_STRING_HOUSE) 

673 

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

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

676 

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

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

679 return aliases[name] 

680 

681 iot.House.Cameras.set_alias_hook(abbrev) 

682 

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

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

685 

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

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

688 

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

690 

691 

692def test_rich_print_with_alias_hook(): 

693 iot = navdict.from_yaml_string(YAML_STRING_HOUSE) 

694 

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

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

697 return aliases[name] 

698 

699 iot.House.Cameras.set_alias_hook(abbrev) 

700 

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

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

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

704 # but should raise an AttributeError as expected. 

705 _ = iot.House.Cameras.no_such_camera 

706 

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

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

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

710 # but should raise a KeyError as expected. 

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