Coverage for tests/test_navdict.py: 96%
132 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-06 22:50 +0200
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-06 22:50 +0200
1import enum
2from pathlib import Path
4import pytest
6from navdict import navdict
7from tests.helpers import create_test_csv_file
8from tests.helpers import create_text_file
11class TakeTwoOptionalArguments:
12 """Test class for YAML load and save methods."""
14 def __init__(self, a=23, b=24):
15 super().__init__()
16 self._a = a
17 self._b = b
19 def __str__(self):
20 return f"a={self._a}, b={self._b}"
23class TakeOneKeywordArgument:
24 def __init__(self, *, sim: bool):
25 self._sim = sim
27 def __str__(self):
28 return f"sim = {self._sim}"
31YAML_STRING_SIMPLE = """
32Setup:
33 site_id: KUL
35 gse:
36 hexapod:
37 id: PUNA_01
39"""
41YAML_STRING_WITH_CLASS = """
42root:
43 defaults:
44 dev: class//test_navdict.TakeTwoOptionalArguments
45 with_args:
46 dev: class//test_navdict.TakeTwoOptionalArguments
47 dev_args: [42, 73]
48 with_kwarg:
49 dev: class//test_navdict.TakeOneKeywordArgument
50 dev_kwargs:
51 sim: true
52"""
54YAML_STRING_WITH_INT_ENUM = """
55F_FEE:
56 ccd_sides:
57 enum: int_enum//FEE_SIDES
58 content:
59 E:
60 alias: ['E_SIDE', 'RIGHT_SIDE']
61 value: 1
62 F:
63 alias: ['F_SIDE', 'LEFT_SIDE']
64 value: 0
65"""
67YAML_STRING_WITH_UNKNOWN_CLASS = """
68root:
69 part_one:
70 cls: class//navdict.navdict
71 part_two:
72 cls: class//unknown.navdict
73"""
75YAML_STRING_INVALID_INDENTATION = """
76name: test
77 age: 30
78description: invalid indentation
79"""
81YAML_STRING_MISSING_COLON = """
82name test
83age: 30
84"""
86YAML_STRING_EMPTY = """"""
89def test_construction():
91 setup = navdict()
93 assert setup == {}
94 assert setup.label is None
96 setup = navdict(label="Setup")
97 assert setup.label == "Setup"
100def test_from_yaml_string():
102 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE)
104 assert "Setup" in setup
105 assert "site_id" in setup.Setup
106 assert "gse" in setup.Setup
107 assert setup.Setup.gse.hexapod.id == "PUNA_01"
109 with pytest.raises(ValueError, match="Invalid YAML string: mapping values are not allowed in this context"):
110 setup = navdict.from_yaml_string(YAML_STRING_INVALID_INDENTATION)
112 with pytest.raises(ValueError, match="Invalid YAML string: mapping values are not allowed in this context"):
113 setup = navdict.from_yaml_string(YAML_STRING_MISSING_COLON)
115 with pytest.raises(ValueError, match="Invalid argument to function: No input string or None given"):
116 setup = navdict.from_yaml_string(YAML_STRING_EMPTY)
119def test_from_yaml_file():
121 with create_text_file("simple.yaml", YAML_STRING_SIMPLE) as fn:
122 setup = navdict.from_yaml_file(fn)
123 assert "Setup" in setup
124 assert "site_id" in setup.Setup
125 assert "gse" in setup.Setup
126 assert setup.Setup.gse.hexapod.id == "PUNA_01"
128 with create_text_file("with_unknown_class.yaml", YAML_STRING_WITH_UNKNOWN_CLASS) as fn:
129 # The following line shall not generate an exception, meaning the `class//`
130 # shall not be evaluated on load!
131 data = navdict.from_yaml_file(fn)
133 assert "root" in data
134 assert isinstance(data.root.part_one.cls, navdict)
136 # Only when accessed, it will generate an exception.
137 with pytest.raises(ModuleNotFoundError, match="No module named 'unknown'"):
138 _ = data.root.part_two.cls
141def test_to_yaml_file():
142 """
143 This test loads the standard Setup and saves it without change to a new file.
144 Loading back the saved Setup should show no differences.
145 """
147 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE)
148 setup.to_yaml_file("simple.yaml")
150 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS)
151 setup.to_yaml_file("with_class.yaml")
153 Path("simple.yaml").unlink()
154 Path("with_class.yaml").unlink()
157def test_class_directive():
159 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS)
161 obj = setup.root.defaults.dev
162 assert isinstance(obj, TakeTwoOptionalArguments)
163 assert str(obj) == "a=23, b=24"
165 obj = setup.root.with_args.dev
166 assert isinstance(obj, TakeTwoOptionalArguments)
167 assert str(obj) == "a=42, b=73"
169 obj = setup.root.with_kwarg.dev
170 assert isinstance(obj, TakeOneKeywordArgument)
171 assert str(obj) == "sim = True"
174def test_from_dict():
176 setup = navdict.from_dict({"ID": "my-setup-001", "version": "0.1.0"}, label="Setup")
177 assert setup["ID"] == setup.ID == "my-setup-001"
179 assert setup._label == "Setup"
181 # If not all keys are of type 'str', the navdict will not be navigable.
182 setup = navdict.from_dict({"ID": 1234, 42: "forty two"}, label="Setup")
183 assert setup["ID"] == 1234
185 with pytest.raises(AttributeError):
186 _ = setup.ID
188 # Only the (sub-)dictionary that contains non-str keys will not be navigable.
189 setup = navdict.from_dict({"ID": 1234, "answer": {"book": "H2G2", 42: "forty two"}}, label="Setup")
190 assert setup["ID"] == setup.ID == 1234
191 assert setup.answer["book"] == "H2G2"
193 with pytest.raises(AttributeError):
194 _ = setup.answer.book
197def get_enum_metaclass():
198 """Get the enum metaclass in a version-compatible way."""
199 if hasattr(enum, 'EnumMeta'): 199 ↛ 201line 199 didn't jump to line 201 because the condition on line 199 was always true
200 return enum.EnumMeta
201 elif hasattr(enum, 'EnumType'): # Python 3.11+
202 return enum.EnumType
203 else:
204 # Fallback: get it from a known enum
205 return type(enum.IntEnum)
208def test_int_enum():
210 setup = navdict.from_yaml_string(YAML_STRING_WITH_INT_ENUM)
212 assert "enum" in setup.F_FEE.ccd_sides
213 assert "content" in setup.F_FEE.ccd_sides
214 assert "E" in setup.F_FEE.ccd_sides.content
215 assert "F" in setup.F_FEE.ccd_sides.content
217 assert setup.F_FEE.ccd_sides.enum.E.value == 1
218 assert setup.F_FEE.ccd_sides.enum.E_SIDE.value == 1
219 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.value == 1
220 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.name == 'E'
222 assert setup.F_FEE.ccd_sides.enum.F.value == 0
223 assert setup.F_FEE.ccd_sides.enum.F_SIDE.value == 0
224 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.value == 0
225 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.name == 'F'
227 assert issubclass(setup.F_FEE.ccd_sides.enum, enum.IntEnum)
228 assert isinstance(setup.F_FEE.ccd_sides.enum, get_enum_metaclass())
229 assert isinstance(setup.F_FEE.ccd_sides.enum, type)
230 assert isinstance(setup.F_FEE.ccd_sides.enum.E, enum.IntEnum) # noqa
233YAML_STRING_LOADS_YAML_FILE = """
234root:
235 simple: yaml//enum.yaml
236"""
239def test_recursive_load():
241 with (
242 create_text_file("load_yaml.yaml", YAML_STRING_LOADS_YAML_FILE) as fn,
243 create_text_file("enum.yaml", YAML_STRING_WITH_INT_ENUM)
244 ):
245 data = navdict.from_yaml_file(fn)
246 assert data.root.simple.F_FEE.ccd_sides.enum.E.value == 1
249YAML_STRING_LOADS_CSV_FILE = """
250root:
251 sample: csv//sample.csv
252 sample_kwargs:
253 header_rows: 2
254"""
257def test_load_csv():
259 with (
260 create_text_file("load_csv.yaml", YAML_STRING_LOADS_CSV_FILE) as fn,
261 create_test_csv_file("sample.csv")
262 ):
264 data = navdict.from_yaml_file(fn)
266 header, csv_data = data.root.sample
268 assert len(header) == 2
269 assert len(header[0]) == 9
270 assert isinstance(header, list)
271 assert isinstance(header[0], list)
273 assert len(csv_data[0]) == 9
274 assert isinstance(csv_data, list)
275 assert isinstance(csv_data[0], list)
277 assert header[0][3] == "department"
278 assert header[1][0] == "# a comment line"
280 assert csv_data[0][0] == '1001'
281 assert csv_data[0][8] == "john.smith@company.com"