Coverage for tests/test_navdict.py: 98%
349 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-07-17 16:04 +0200
« prev ^ index » next coverage.py v7.8.2, created at 2026-07-17 16:04 +0200
1import enum
2import os
3from pathlib import Path
4from typing import Protocol
6import pytest
8from helpers import create_test_csv_file
9from helpers import create_test_csv_file_semicolon
10from helpers import create_text_file
11from navdict import navdict
12from navdict.directive import Directive
13from navdict.directive import get_directive_plugin
14from navdict.directive import is_directive
15from navdict.directive import register_directive
16from navdict.navdict import expand_env_vars
17from navdict.navdict import get_resource_location
19HERE = Path(__file__).parent
22class TakeTwoOptionalArguments:
23 """Test class for YAML load and save methods."""
25 def __init__(self, a=23, b=24):
26 super().__init__()
27 self._a = a
28 self._b = b
30 def __str__(self):
31 return f"a={self._a}, b={self._b}"
34class TakeOneKeywordArgument:
35 def __init__(self, *, sim: bool):
36 self._sim = sim
38 def __str__(self):
39 return f"sim = {self._sim}"
42class _HexapodSchema(Protocol):
43 id: str
46class _GSESchema(Protocol):
47 hexapod: _HexapodSchema
50class _SetupSchema(Protocol):
51 site_id: str
52 gse: _GSESchema
55class _RootSchema(Protocol):
56 Setup: _SetupSchema
59YAML_STRING_SIMPLE = """
60Setup:
61 site_id: KUL
63 gse:
64 hexapod:
65 id: PUNA_01
67"""
69YAML_STRING_WITH_RELATIVE_YAML = """
70Setup:
71 camera:
72 fm01: yaml//cameras/fm01.yaml
73"""
75YAML_STRING_WITH_CLASS = """
76root:
77 defaults:
78 dev: class//test_navdict.TakeTwoOptionalArguments
79 with_args:
80 dev: class//test_navdict.TakeTwoOptionalArguments
81 dev_args: [42, 73]
82 with_kwarg:
83 dev: class//test_navdict.TakeOneKeywordArgument
84 dev_kwargs:
85 sim: true
86"""
88YAML_STRING_WITH_INT_ENUM = """
89F_FEE:
90 ccd_sides:
91 enum: int_enum//FEE_SIDES
92 content:
93 E:
94 alias: ['E_SIDE', 'RIGHT_SIDE']
95 value: 1
96 F:
97 alias: ['F_SIDE', 'LEFT_SIDE']
98 value: 0
99"""
101YAML_STRING_WITH_USER_DEFINED_FUNCTION = """
102telemetry:
103 dictionary: pandas//data/telemetry.csv
104 dictionary_kwargs:
105 separator: ;
106"""
108YAML_STRING_WITH_UNKNOWN_CLASS = """
109root:
110 part_one:
111 cls: class//navdict.navdict
112 part_two:
113 cls: class//unknown.navdict
114"""
116YAML_STRING_INVALID_INDENTATION = """
117name: test
118 age: 30
119description: invalid indentation
120"""
122YAML_STRING_MISSING_COLON = """
123name test
124age: 30
125"""
127YAML_STRING_EMPTY = """"""
130def test_is_directive():
131 assert is_directive("yaml//sample.yaml")
132 assert is_directive("class//navdict.navdict")
133 assert is_directive("my_directive//value")
135 assert not is_directive("just a string")
136 assert not is_directive("relative/path")
137 assert not is_directive("my-directive//value")
139 assert not is_directive(42)
140 assert not is_directive(23.7)
142 assert not is_directive("my-setup-001")
145def test_get_directive_plugin():
146 assert isinstance(get_directive_plugin("yaml"), Directive)
148 assert not isinstance(get_directive_plugin("not-a-plugin"), Directive)
151def test_use_a_directive_plugin():
152 yaml_string = """
153 Setup:
154 info: my_yaml//../use/this/file.yaml
155 info_args: [1, 2]
156 info_kwargs:
157 x: X
158 y: Y
159 """
160 data = navdict.from_yaml_string(yaml_string)
161 # print(f"{data.Setup.info=}")
162 assert data.Setup.info.startswith("my_yaml//")
163 assert data.Setup.info.endswith("use/this/file.yaml")
166def test_get_resource_location():
167 assert get_resource_location(None, None) == Path(".")
168 assert get_resource_location(None, "../data") == Path(".") / "../data"
169 assert get_resource_location(Path("~/project/"), "data") == Path.home() / "project/data"
170 assert get_resource_location(Path("~"), None) == Path.home()
173def test_construction():
174 setup = navdict()
176 assert setup == {}
177 assert setup.get_label() is None
179 setup = navdict(label="Setup")
180 assert setup.get_label() == "Setup"
183def test_label():
184 setup = navdict()
186 assert setup == {}
187 assert setup.get_label() is None
189 setup.set_label("Setup")
191 assert setup == {}
192 assert setup.get_label() == "Setup"
195def test_navigation():
196 data = navdict.from_yaml_string(YAML_STRING_SIMPLE)
198 assert isinstance(data, navdict)
199 assert isinstance(data.Setup, navdict)
201 assert data.Setup.site_id == "KUL"
202 assert data.Setup.gse.hexapod.id == "PUNA_01"
205def test_as_typed_helper_for_static_typing():
206 data = navdict.from_yaml_string(YAML_STRING_SIMPLE)
208 typed_data = data.as_typed(_RootSchema)
210 assert typed_data.Setup.site_id == "KUL"
211 assert typed_data.Setup.gse.hexapod.id == "PUNA_01"
212 assert typed_data is data
215def test_dir_includes_identifier_keys():
216 data = navdict({"valid_key": 1, "not-valid": 2, "also.not.valid": 3})
217 names = dir(data)
219 assert "valid_key" in names
220 assert "not-valid" not in names
221 assert "also.not.valid" not in names
224def test_from_yaml_string():
225 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE)
227 assert "Setup" in setup
228 assert "site_id" in setup.Setup
229 assert "gse" in setup.Setup
230 assert setup.Setup.gse.hexapod.id == "PUNA_01"
232 with pytest.raises(
233 ValueError,
234 match="Invalid YAML string: mapping values are not allowed in this context",
235 ):
236 setup = navdict.from_yaml_string(YAML_STRING_INVALID_INDENTATION)
238 with pytest.raises(
239 ValueError,
240 match="Invalid YAML string: mapping values are not allowed in this context",
241 ):
242 setup = navdict.from_yaml_string(YAML_STRING_MISSING_COLON)
244 with pytest.raises(ValueError, match="Invalid argument to function: No input string or None given"):
245 setup = navdict.from_yaml_string(YAML_STRING_EMPTY)
248def test_from_yaml_file():
249 with pytest.raises(
250 ValueError,
251 match=r"Invalid argument to function, filename does not exist: "
252 r".*/simple.yaml",
253 ):
254 navdict.from_yaml_file("~/simple.yaml")
256 with create_text_file("simple.yaml", YAML_STRING_SIMPLE) as fn:
257 setup = navdict.from_yaml_file(fn)
258 assert "Setup" in setup
259 assert "site_id" in setup.Setup
260 assert "gse" in setup.Setup
261 assert setup.Setup.gse.hexapod.id == "PUNA_01"
263 with create_text_file("with_unknown_class.yaml", YAML_STRING_WITH_UNKNOWN_CLASS) as fn:
264 # The following line shall not generate an exception, meaning the `class//`
265 # shall not be evaluated on load!
266 data = navdict.from_yaml_file(fn)
268 assert "root" in data
269 assert isinstance(data.root.part_one.cls, navdict)
271 # Only when accessed, it will generate an exception.
272 with pytest.raises(ModuleNotFoundError, match="No module named 'unknown'"):
273 _ = data.root.part_two.cls
276def test_to_yaml_file():
277 """
278 This test loads the standard Setup and saves it without change to a new file.
279 Loading back the saved Setup should show no differences.
280 """
282 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE)
284 with pytest.raises(ValueError, match="No filename given or known, can not save navdict."):
285 setup.to_yaml_file()
287 setup.to_yaml_file("simple.yaml")
289 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS)
290 setup.to_yaml_file("with_class.yaml")
292 Path("simple.yaml").unlink()
293 Path("with_class.yaml").unlink()
296def test_class_directive():
297 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS)
299 obj = setup.root.defaults.dev
300 assert isinstance(obj, TakeTwoOptionalArguments)
301 assert str(obj) == "a=23, b=24"
303 obj = setup.root.with_args.dev
304 assert isinstance(obj, TakeTwoOptionalArguments)
305 assert str(obj) == "a=42, b=73"
307 obj = setup.root.with_kwarg.dev
308 assert isinstance(obj, TakeOneKeywordArgument)
309 assert str(obj) == "sim = True"
312def test_user_defined_function_directive():
313 setup = navdict.from_yaml_string(YAML_STRING_WITH_USER_DEFINED_FUNCTION)
315 # By default, the 'pandas' directive is not registered, so, the 'dictionary' value will be returned as is.
316 result = setup.telemetry.dictionary
317 assert result == "pandas//data/telemetry.csv"
319 # Now we register a simple 'pandas' directive for testing purposes.
320 def mock_pandas_directive(value: str, parent_location: Path | None, *args, **kwargs):
321 return f"Mocked pandas loading of '{value}' with args={args} and kwargs={kwargs}"
323 register_directive("pandas", mock_pandas_directive)
325 result = setup.telemetry.dictionary
326 print(f"{result=}")
327 assert result == "Mocked pandas loading of 'data/telemetry.csv' with args=() and kwargs={'separator': ';'}"
330def test_from_dict():
331 setup = navdict.from_dict({"ID": "my-setup-001", "version": "0.1.0"}, label="Setup")
332 assert setup["ID"] == setup.ID == "my-setup-001"
334 assert setup._label == "Setup"
336 # If not all keys are of type 'str', the navdict will not be navigable.
337 setup = navdict.from_dict({"ID": 1234, 42: "forty two"}, label="Setup")
338 assert setup["ID"] == 1234
340 with pytest.raises(AttributeError):
341 _ = setup.ID
343 # Only the (sub-)dictionary that contains non-str keys will not be navigable.
344 setup = navdict.from_dict({"ID": 1234, "answer": {"book": "H2G2", 42: "forty two"}}, label="Setup")
345 assert setup["ID"] == setup.ID == 1234
346 assert setup.answer["book"] == "H2G2"
348 with pytest.raises(AttributeError):
349 _ = setup.answer.book
352def get_enum_metaclass():
353 """Get the enum metaclass in a version-compatible way."""
354 if hasattr(enum, "EnumMeta"): 354 ↛ 356line 354 didn't jump to line 356 because the condition on line 354 was always true
355 return enum.EnumMeta
356 elif hasattr(enum, "EnumType"): # Python 3.11+
357 return enum.EnumType
358 else:
359 # Fallback: get it from a known enum
360 return type(enum.IntEnum)
363def test_int_enum():
364 setup = navdict.from_yaml_string(YAML_STRING_WITH_INT_ENUM)
366 assert "enum" in setup.F_FEE.ccd_sides
367 assert "content" in setup.F_FEE.ccd_sides
368 assert "E" in setup.F_FEE.ccd_sides.content
369 assert "F" in setup.F_FEE.ccd_sides.content
371 assert setup.F_FEE.ccd_sides.enum.E.value == 1
372 assert setup.F_FEE.ccd_sides.enum.E_SIDE.value == 1
373 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.value == 1
374 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.name == "E"
376 assert setup.F_FEE.ccd_sides.enum.F.value == 0
377 assert setup.F_FEE.ccd_sides.enum.F_SIDE.value == 0
378 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.value == 0
379 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.name == "F"
381 assert issubclass(setup.F_FEE.ccd_sides.enum, enum.IntEnum)
382 assert isinstance(setup.F_FEE.ccd_sides.enum, get_enum_metaclass())
383 assert isinstance(setup.F_FEE.ccd_sides.enum, type)
384 assert isinstance(setup.F_FEE.ccd_sides.enum.E, enum.IntEnum) # noqa
387YAML_STRING_LOADS_YAML_FILE = """
388root:
389 simple: yaml//enum.yaml
390"""
393def test_recursive_load():
394 with (
395 create_text_file("load_yaml.yaml", YAML_STRING_LOADS_YAML_FILE) as fn,
396 create_text_file("enum.yaml", YAML_STRING_WITH_INT_ENUM),
397 ):
398 data = navdict.from_yaml_file(fn)
399 assert data.root.simple.F_FEE.ccd_sides.enum.E.value == 1
402def test_relative_load():
403 with (
404 create_text_file(HERE / "data/conf/load_relative_yaml.yaml", YAML_STRING_WITH_RELATIVE_YAML) as fn,
405 ):
406 data = navdict.from_yaml_file(fn)
407 assert data.Setup.camera.fm01.calibration.temperature.T1.name == "TRP99"
410def test_relative_load_from_string():
411 """
412 The YAML string contains a directive to load another YAML file, but since
413 we will load this navdict from the string instead of the file, it doesn't
414 have a location and therefore the directive will be loaded relative to the
415 working directory.
417 When we change the current working directory to the expected location, things
418 work just fine.
420 """
421 data = navdict.from_yaml_string(YAML_STRING_WITH_RELATIVE_YAML)
423 assert "fm01" in data.Setup.camera
425 with pytest.raises(FileNotFoundError, match="No such file or directory: 'cameras/fm01.yaml'"):
426 assert data.Setup.camera.fm01
428 cwd = os.getcwd()
430 os.chdir(HERE / "data/conf")
432 assert data.Setup.camera.fm01.name == "FM01"
433 assert "T1" in data.Setup.camera.fm01.calibration.temperature
435 os.chdir(cwd)
438YAML_STRING_LOADS_CSV_FILE = """
439root:
440 sample: csv//data/sample.csv
441 sample_kwargs:
442 header_rows: 1
443"""
446def test_load_csv():
447 """
448 The sample.csv file will be read using the standard load_csv directive.
450 - one header row will be skipped (header_rows=1)
451 - the comment line will be filtered
452 - a list of list[str] will be returned
454 """
455 with (
456 create_text_file(HERE / "load_csv.yaml", YAML_STRING_LOADS_CSV_FILE) as fn,
457 create_test_csv_file(HERE / "data/sample.csv"),
458 ):
459 data = navdict.from_yaml_file(fn)
461 csv_data = data.root.sample
463 assert len(csv_data[0]) == 9
464 assert isinstance(csv_data, list)
465 assert isinstance(csv_data[0], list)
467 assert csv_data[0][0] == "1001"
468 assert csv_data[0][8] == "john.smith@company.com"
471def test_expand_env_vars():
472 os.environ["NAVDICT_TEST_VAR"] = "some/value"
474 assert expand_env_vars("prefix/ENV[NAVDICT_TEST_VAR]/suffix") == "prefix/some/value/suffix"
475 assert expand_env_vars("prefix/ENV['NAVDICT_TEST_VAR']/suffix") == "prefix/some/value/suffix"
476 assert expand_env_vars('prefix/ENV["NAVDICT_TEST_VAR"]/suffix') == "prefix/some/value/suffix"
478 os.environ["NAVDICT_TEST_HOME_VAR"] = "~"
480 assert (
481 expand_env_vars("ENV[NAVDICT_TEST_HOME_VAR]/data") == "~/data"
482 ) # environment variable is expanded, but not the '~' character
484 del os.environ["NAVDICT_TEST_VAR"]
485 del os.environ["NAVDICT_TEST_HOME_VAR"]
487 with pytest.raises(ValueError):
488 expand_env_vars("ENV[NAVDICT_TEST_VAR_NOT_SET]/data")
491YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR = """
492root:
493 sample: csv//ENV[NAVDICT_TEST_DATA_DIR]/sample.csv
494 sample_kwargs:
495 header_rows: 1
496"""
498YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR_DISABLED = """
499root:
500 sample: csv//ENV[NAVDICT_TEST_DATA_DIR]/sample.csv
501 sample_kwargs:
502 header_rows: 1
503 expand_env: false
504"""
506YAML_STRING_LOADS_CSV_FILE_WITH_MISSING_ENV_VAR = """
507root:
508 sample: csv//ENV[NAVDICT_TEST_VAR_NOT_SET]/sample.csv
509"""
512def test_load_csv_with_env_var():
513 os.environ["NAVDICT_TEST_DATA_DIR"] = str(HERE / "data")
515 try:
516 with (
517 create_text_file(HERE / "load_csv_env.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR) as fn,
518 create_test_csv_file(HERE / "data/sample.csv"),
519 ):
520 data = navdict.from_yaml_file(fn)
522 csv_data = data.root.sample
524 assert isinstance(csv_data, list)
525 assert csv_data[0][0] == "1001"
526 assert csv_data[0][8] == "john.smith@company.com"
527 finally:
528 del os.environ["NAVDICT_TEST_DATA_DIR"]
531def test_load_csv_env_var_expansion_disabled():
532 os.environ["NAVDICT_TEST_DATA_DIR"] = str(HERE / "data")
534 try:
535 with (
536 create_text_file(
537 HERE / "load_csv_env_disabled.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_ENV_VAR_DISABLED
538 ) as fn,
539 create_test_csv_file(HERE / "data/sample.csv"),
540 ):
541 data = navdict.from_yaml_file(fn)
543 with pytest.raises(FileNotFoundError):
544 _ = data.root.sample
545 finally:
546 del os.environ["NAVDICT_TEST_DATA_DIR"]
549def test_load_csv_missing_env_var():
550 with create_text_file(HERE / "load_csv_missing_env.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_MISSING_ENV_VAR) as fn:
551 data = navdict.from_yaml_file(fn)
553 with pytest.raises(ValueError):
554 _ = data.root.sample
557YAML_STRING_LOADS_CSV_FILE_WITH_DELIMITER = """
558root:
559 products: csv//data/sample_semicolon.csv
560 products_kwargs:
561 header_rows: 1
562 delimiter: ';'
563"""
566def test_load_csv_with_delimiter():
567 """
568 Test that the CSV directive correctly handles custom delimiters.
569 The sample CSV file uses semicolons as delimiters instead of the default comma.
570 """
571 with (
572 create_text_file(HERE / "load_csv_delimiter.yaml", YAML_STRING_LOADS_CSV_FILE_WITH_DELIMITER) as fn,
573 create_test_csv_file_semicolon(HERE / "data/sample_semicolon.csv"),
574 ):
575 data = navdict.from_yaml_file(fn)
577 csv_data = data.root.products
579 assert isinstance(csv_data, list)
580 assert isinstance(csv_data[0], list)
582 assert len(csv_data[0]) == 5
583 assert csv_data[0][0] == "101"
584 assert csv_data[0][1] == "Laptop"
585 assert csv_data[0][4] == "15"
588def test_directive_registration():
589 def inspect_directive(value, parent_location, *args, **kwargs) -> dict:
590 return dict(value=value, parent_location=parent_location, args=args, kwargs=kwargs)
592 # The following should overwrite the default `csv//` directive
593 register_directive("csv", inspect_directive)
595 with (
596 create_text_file(HERE / "load_csv.yaml", YAML_STRING_LOADS_CSV_FILE) as fn,
597 create_test_csv_file(HERE / "data/sample.csv"),
598 ):
599 data = navdict.from_yaml_file(fn)
601 csv_data = data.root.sample
603 assert isinstance(csv_data, dict)
604 assert "value" in csv_data
605 assert csv_data["value"] == "data/sample.csv"
606 assert csv_data["parent_location"] == HERE
608 assert "kwargs" in csv_data
609 assert "header_rows" in csv_data["kwargs"]
610 assert csv_data["kwargs"]["header_rows"] == 1
613YAML_STRING_WITH_ENV_VAR = """
614config:
615 token: env//AUTH_TOKEN
616"""
619def test_env_var():
620 data = navdict.from_yaml_string(YAML_STRING_WITH_ENV_VAR)
622 assert data.config.token is None
624 os.environ["AUTH_TOKEN"] = "this-is-my-token"
626 data.config.del_memoized_key("token")
628 assert data.config.token == "this-is-my-token"
630 del os.environ["AUTH_TOKEN"]
633def test_memoized_keys():
634 # NOTE:
635 # * memoized keys are only for directives.
636 # * a key is memoized only after it was accessed.
638 data = navdict.from_yaml_string(YAML_STRING_WITH_ENV_VAR)
640 assert data.config.get_memoized_keys() == []
642 os.environ["AUTH_TOKEN"] = "this-is-my-token-too"
644 assert data.config.token == "this-is-my-token-too"
646 assert data.config.get_memoized_keys() == ["token"]
648 assert data.config.del_memoized_key("token")
650 assert "token" not in data.config.get_memoized_keys()
652 # returns False when a key is not memoized and could not be deleted
653 assert not data.config.del_memoized_key("unknown")
656def test_non_string_keys():
657 x = navdict({"A": {1: "one", 2: "two", (3,): "three-tuple"}})
659 assert x.A[1] == "one"
660 assert x.A[2] == "two"
661 assert x.A[(3,)] == "three-tuple"
664def test_invalid_yaml():
665 # This would normally raise a ScannerError from the ruamel.yaml package
666 # - ruamel.yaml.scanner.ScannerError: mapping values are not allowed in this context
668 with pytest.raises(IOError):
669 _ = navdict.from_yaml_file(__file__)
672def test_alias_hook():
673 x = navdict({"letters": {"a": "A", "b": "B", "c": "C"}, "numbers": [1, 2, 3, 4, 5]})
675 def greek(letter: str):
676 greek_to_latin = {"alpha": "a", "beta": "b", "gamma": "c"}
677 return greek_to_latin[letter]
679 assert x.letters.a == "A"
680 assert x.numbers[2] == 3
682 with pytest.raises(AttributeError):
683 _ = x.letters.alpha
685 with pytest.raises(KeyError):
686 _ = x.letters["alpha"]
688 x.letters.set_alias_hook(greek)
690 assert x.letters.alpha == "A"
691 assert x.letters["alpha"] == "A"
693 assert x.numbers[2] == 3
696YAML_STRING_HOUSE = """
697House:
698 Cameras:
699 cam_1:
700 location: front door
701 type: XYZ-A123
702 cam_2:
703 location: front garage
704 type: XYZ-B123
705"""
708def test_alias_hook_from_yaml_string():
709 iot = navdict.from_yaml_string(YAML_STRING_HOUSE)
711 assert iot.House.Cameras.cam_1.type == "XYZ-A123"
712 assert iot.House.Cameras.cam_2.type == "XYZ-B123"
714 def abbrev(name: str) -> str:
715 aliases = {"front_door": "cam_1", "front_garage": "cam_2"}
716 return aliases[name]
718 iot.House.Cameras.set_alias_hook(abbrev)
720 assert iot.House.Cameras.cam_1.type == "XYZ-A123"
721 assert iot.House.Cameras.cam_2.type == "XYZ-B123"
723 assert iot.House.Cameras.front_door.type == "XYZ-A123"
724 assert iot.House.Cameras.front_garage.type == "XYZ-B123"
726 assert "_alias_hook" not in iot.House.Cameras
729def test_rich_print_with_alias_hook():
730 iot = navdict.from_yaml_string(YAML_STRING_HOUSE)
732 def abbrev(name: str) -> str:
733 aliases = {"front_door": "cam_1", "front_garage": "cam_2"}
734 return aliases[name]
736 iot.House.Cameras.set_alias_hook(abbrev)
738 with pytest.raises(AttributeError, match="'NavigableDict' object has no attribute 'no_such_camera'"):
739 # The following line will cause a KeyError in the abbrev()` hook
740 # Any Exception from the alias hook should not propagate to the top
741 # but should raise an AttributeError as expected.
742 _ = iot.House.Cameras.no_such_camera
744 with pytest.raises(KeyError, match="'NavigableDict' has no key 'no_such_camera'"):
745 # The following line will cause a KeyError in the abbrev()` hook
746 # Any Exception from the alias hook should not propagate to the top
747 # but should raise a KeyError as expected.
748 _ = iot.House.Cameras["no_such_camera"]